Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

transform snap yields 1px offset below and to the right of kinematicbody2d for pixel platformer #46504

Closed
securas opened this issue Feb 28, 2021 · 27 comments

Comments

@securas
Copy link

securas commented Feb 28, 2021

Godot version:
3.2.4 RC3

OS/device including version:
Windows 10, NVIDIA GTX 1070

Issue description:
Enabling transform snap leads to display of 2d character 1px above and to the right of a pixel platformer, as shown below.
image

Other relevant settings:
image

In addition, strong fluctuations of the character on screen can be observed.
platformer_positioning

Steps to reproduce:
run project.

Minimal reproduction project:

test_character_positioning.zip

@lawnjelly
Copy link
Member

Just for reference this is continuing discussion from twitter:
https://twitter.com/Securas2010/status/1365255834926604290

Ok I'll take a look and run the numbers! 👍

@lawnjelly
Copy link
Member

lawnjelly commented Feb 28, 2021

I slightly modified your project to use a single centred sprite to eliminate other sources of error:
test_character_positioning2.zip

And print out the y coordinate. As you can see what happens is because of physics, the player is at y 57.98 (this may be determined by collision margins etc). This fraction, no matter how small, means with the current transform snapping, this is floored to 57.0.

In Godot 2D, y increases down the screen, so the effect is that the object is raised by a pixel above 'ground' level. The tilemap is not positioned by physics so is exactly on a pixel boundary.

pixel_gap

My attempt at a general solution to this problem is to change the rounding from floor to round in #43813. In my tests this seems to deal with this common problem, providing that you build your game on whole number coordinates. It also deals with the problem of physics 'vibration' where objects minutely vibrate across a whole number boundary and shift by a whole pixel as a result.

You can alternatively shift your tilemap to deal with this (at least in the y axis). If you set your tilemap Transform->Position->y to 0.5 you can see the sprite now sits at the right level. Although I think using round is a more general solution. Or you could apply a small offset to your sprite relative to the collision shape.

With things like this, to debug, always print the numbers, as it will make clear what is happening. All the transform_snapping does currently is apply floor to the x and y coordinates prior to rendering.

@lawnjelly lawnjelly added the bug label Feb 28, 2021
@securas securas changed the title transform span yields 1px offset below and to the right of kinematicbody2d for pixel platformer transform snap yields 1px offset below and to the right of kinematicbody2d for pixel platformer Feb 28, 2021
@securas
Copy link
Author

securas commented Feb 28, 2021

@lawnjelly Thanks for taking the time and effort to check it out. Much appreciated.

@lawnjelly
Copy link
Member

It sounds as though we are going to try with the rounding for the next RC which should help these issues. So well worth reporting, as I think others will have had the same problem. 👍

@akien-mga akien-mga added this to the 3.2 milestone Mar 1, 2021
@akien-mga
Copy link
Member

Fixed by #43813.

@lawnjelly
Copy link
Member

@securas Just out of interest, now I have #46569 working, it's helping to form some ideas in my head for recommendations when we finally update the docs.

2D stretch mode

My initial impressions are that transform_snap and camera_snap are best used when window/stretch_mode is set to 2D.

viewport stretch mode

When window/stretch_mode is set to viewport, I get the impression it is effectively already quantizing to pixels (similar to the effect if you created a viewport node at a smaller resolution, rendered to this, then blitted it to a texture_rect (yes I tried this..)). So with transform_snap etc off, you do get a good result (with the new camera).

The only snag is you still get the pixel gap. This is predictable because of the small gap due to collision margin, and the integer pixels effectively flooring the float coordinate:
i.e. 57.98 becomes 57.0
and the player rises by a pixel because of the godot convention that y is down. This makes the convention somewhat of a pain as it complicates matters. What we really want to do (in a sensible situation) is force rounding in the other direction (downwards in height) which is using ceiling instead of floor.

So at the moment I'm playing with the idea of having the transform_snap do different things depending on the stretch_mode. In 2d, round seems to work well. But in viewport mode, because of the weird y convention, it makes sense to treat x and y coordinates differently, and perform a ceiling on the y coordinate. This seems to give stable behaviour and it may not even be necessary to snap the x coordinate at all in this mode.

It is funny, that it is only possible to start making sense of what is going on now the camera is fixed with #46569. There were previously multiple bugs that conflicted with each other. But I'm gradually honing down on a system which I hope will give great results no matter the stretch_mode. 😁

@securas
Copy link
Author

securas commented Mar 2, 2021

Before adding to this discussion... Any chance I can get a compiled version with #46569 to test? I don't have the skill or know how to be able to compile godot by myself...

@lawnjelly
Copy link
Member

lawnjelly commented Mar 2, 2021

Before adding to this discussion... Any chance I can get a compiled version with #46569 to test? I don't have the skill or know how to be able to compile godot by myself...

Yup sure, just go to the PR page (#46569), next to the green arrow click 'show all checks'. Click 'details' on the one you want, then 'artifacts' in the top right hand corner and you can download.

The fixed camera helps immensely in all these problems but there are still some more problems, I'm working on fixes right now. With stretch_mode viewport the setting you are using, the snapping needs to do a slightly different thing that in 2d mode to get good results, and also I've discovered we also need snapping on the camera smoothing which I'm just working on.

But with the camera PR you (and snapping turned off) should get quite a good result (apart from the pixel gap, I don't recommend turning on snapping till I've got these later fixes merged).

@securas
Copy link
Author

securas commented Mar 2, 2021

Alright... I tried a version... hopefully the latest though my Github knowledge is very very limited.

Viewport mode is the bread and butter of pixel art games. But not all games are platformers. So the need for flooring or ceiling makes no sense. At best, rounding, as you said. In addition, treating X and Y differently is a very limiting option. I'm not sure if that is the case but take a look at the examples below:

  • example project, character moving horizontally, fine... moving vertical, much worse than before
    platformer_positioning_moving

  • example project with camera smoothing, moving vertical or horizontal... seems even worse than before. The gif is too slow to show the character moving back and forward though.
    platformer_positioning_moving_smoothed

  • the rounding issues (if those are really the issue) are exacerbated using a parallax background
    Here without smoothing
    platformer_positioning_parallax

And here even worse with smoothing
platformer_positioning_parallax_smoothed

In the case of the parallax background, after the jumps it seems to reach a given coordinate and then turn back... I can't possibly understand the reasons for it but it for sure is incorrect.

@securas
Copy link
Author

securas commented Mar 2, 2021

And the new project, if you want to try it for yourself...
test_character_positioning.zip

@lawnjelly
Copy link
Member

Yup, as I said, the only one you can effectively test so far is with the new camera and snap_transforms and snap_cameras off. The rest won't work yet, I'm reworking this entire area currently, in light of the results from the new camera.

@securas
Copy link
Author

securas commented Mar 2, 2021 via email

@lawnjelly
Copy link
Member

@securas If you want to give the PR #46569 another try it has now been updated.

Bad news is I could not get satisfied with any of the options for snapping for stretch_mode viewport. So I've made them all customizable by the user. However I suspect you should be using stretch_mode 2D, the results are like 100x better.

  • stretch_mode 2D works with sub-texel accuracy, i.e it can snap at pixels on screen rather than the blocky texels.
  • It works perfectly with smoothed cameras
  • It works well with the parallax background

With stretch_mode viewport I suspect you will never get the results you want:

  • transform_snapping results in judder with a smoothed camera. I can't see a way around this.
  • Your parallax background uses a scale of 0.9. This results in aliasing with the snapping to the blocky texels.

@securas
Copy link
Author

securas commented Mar 3, 2021

  • It works well with the parallax background

Thanks! I'll give it another go.

I choose the 0.9 scale for the parallax background because it is a very common aesthetic and it emphasizes one of the issues that I and some other folks I've talked to would really like to see fixed... The fact that it seems to wander back and forth a little before settling into the final position when the camera is smoothed. I understand that 2d mode would be much better. But that is simply not an option for pure pixel art games. Of course, it is always an aesthetics choice. But pure pixel art games cannot have pixels drawn off grid. Of course, this limits the "smoothness" of cameras and that is understandable. What is not is the wandering back and forth like what you see on the parallax background. Although I'm completely alien to the techniques that are being used here, it is difficult for me to understand what rounding method can produce that effect. It would seem that, under a hypothetical case where the camera has to smoothly transition from X to X+50, at some point the it is showing the coordinate X+51 before returning to X+50.
Again... I have no grasp on the complexity that I can only imagine being done there. But it would seem to me that something is amiss.

@lawnjelly
Copy link
Member

It's probably due to physics, and the feedback loop inherent in the smoothed camera.

I had a think overnight and I'm now erring towards the idea we should hide these options. Having them available in project settings I can see as being problematic, because most people trying them won't understand them. In general from feedback I get the impression users grossly underestimate how complex snapping is to get right. People want a simple answer to a problem that may not have a simple answer. In the present state I fear we're going to get deluged by bogus support issues.

I'm currently thinking we should either delay the rollout for 3.2.4, or provide all the options, but through script only (e.g. engine calls Engine.set_transform_snap_2d). The improved camera I think on the other hand is good to go, but I think changing the default to be the old style camera would be wise. So I'm planning to split the PR into 2 today.

Having the snapping available via script-only would provide a barrier to entry, which means users are more likely to have read documentation and understand the issues involved, and actively sought out the functions.

@securas
Copy link
Author

securas commented Mar 3, 2021 via email

@lawnjelly
Copy link
Member

I made two different PRs in the end for the snapping for us to choose between, and Akien is keener on the one with project settings.

Every possible snapping combination will be available to the user in the advanced modes, but I can't say which will be best, you will have to try them out.

@securas
Copy link
Author

securas commented Mar 3, 2021

I'm happy to try them out. Can you please point those to me?

@lawnjelly
Copy link
Member

It's #46615. But you may have to wait a few mins for it to pass CI, before the builds can be downloaded.

@securas
Copy link
Author

securas commented Mar 3, 2021 via email

@lawnjelly
Copy link
Member

lawnjelly commented Mar 6, 2021

I'm getting there with doing this in the addon. The jiggle of objects relative to the background was easily solved by snapping to the background (which was pretty much my first suggestion on the subjects, see here: #35606 (comment) ).

The biggest problem I think is that the current smoothed camera just inherently doesn't work well with pixellated graphics. Instead here I've used a limit camera which locks a maximum range from the target. Using a hard limit removes the judder which I assume is a consequence of fractional feedback causing variation in the distance between the target and camera in the smoothed camera.

There's a bit of side to side with diagonals, I'll have a look at that next, perhaps something isn't matching up leading to a staircase effect. I also suspect physics is likely to be problematic.

2021-03-06.12-04-33.mp4

@securas
Copy link
Author

securas commented Mar 6, 2021

Given my limited understanding, the smoothed camera problem should be fixable as it is done extensively in other engines. I did notice that the smoothed camera does show issues when accelerating and decelerating. When it moves with constant velocity, it falls into the same conditions as the non-smoothed camera. This indicates that a part of the issue might be due to the smoothing function. As for the parallax backgrounds, there should also be no reason to jitter. The change of the corresponding canvas transform should be equivalent to using a slower camera, assuming that the parallax layers do not scale the textures within them.

@lawnjelly
Copy link
Member

lawnjelly commented Mar 6, 2021

I can't say the current smoothed camera implementation can't be done, just I've had no luck so far making it look good. All this is done in gdscript, so if you or anyone can figure a way of going it, let us know! I'm no expert in this, as I'm really a 3d guy. 😁

I suspect if it is possible to get working it will use slightly different logic inside the smoothing function.

There's a number of things that make such pixel based stuff more tricky in godot imo:

  • It sends positions as floats to the shader, then snaps in the shader. This is susceptible to float error. It would probably be easier if integer coords were sent to the shader.
  • Object positions are based on their centres rather than their bottom left coordinate. So you have to use math to adjust for this, which again is subject to error.
  • The whole mishmash between physics updates and frame updates.

@lawnjelly
Copy link
Member

lawnjelly commented Mar 6, 2021

Got the diagonals working fine now:
https://user-images.githubusercontent.com/21999379/110216502-6ab7ca00-7ea7-11eb-823a-aed6fe09c7bf.mp4

The reason for problem on the diagonals is staircasing. If the x and y coordinate are even slightly out of sync (e.g. x is 3.2 and y is 6.4) then on a boundary there will be staircasing - it will cross one axis on one frame, then the other axis the next. To get them to cross at the same time and get nice smooth motion they need to be in lockstep.

The simplest way I found to achieve this was to simply store all coordinates as integers on a grid based on the background bottom left.

There are a number of issues with this though:

  • Here I'm moving by 1 pixel every update (which is 60fps here).
    What happens if I want to move slower, or faster? Ah you think, I'll just store a fractional position. But then staircasing comes back. I tried this with fractions and fixed point, it always happens. You could maybe do something to lock them together when you press e.g. up and right simultaneously.

An alternative approach to speed is instead of storing a fractional position, you instead store a timer, and allow moves every x ticks. So to half the speed, you allow a pixel move every 2nd tick, something like that. This will give pixel perfect motion, but there are 2 big issues:

  • Your game speed is locked to a particular frame rate. This used to be fine, with 50fps PAL and 60fps NTSC, but with multispeed monitors it's a bit more annoying.
  • This is completely incompatible with real physics. Ye olde games wouldn't use real physics, they would literally move pixel by pixel, and do a collision check as they moved. Physics engine on the other hand are all over the place. They would be very difficult / impossible to coordinate with a pixel grid.
    So basically for any kind of nice looking result with large pixellated look (stretch_mode viewport), It would seem easier to not use physics engine. And if you want to use physics, it would seem better to use stretch_mode 2d, where the screen pixel judder will be less objectionable. Essentially you'll still get staircasing and things like single pixel judder, but most of the time you won't notice it.

So the question arises whether the retro games that are being emulated - do they actually use physics engines? Or are they doing per pixel movement / collision detection?

Another thing that you might try is some kind of AI interpolation on top of a physics engine, to smooth out staircasing and judder. Could work, but might add an extra delay on input.

@securas
Copy link
Author

securas commented Mar 7, 2021

Try also 1:2 diagonals. Those should be more problematic.

Here's one possible solution in GameMaker. Seems to correct the canvas transform on a per frame basis. I'll check this in a little more detail and report back here:
https://yal.cc/gamemaker-smooth-pixel-perfect-camera/

@lawnjelly
Copy link
Member

That looks like an interesting idea. 👍

Although there is no player in the centre of the screen. I suspect that with this method you will get player jiggle. But worth a go. You may possibly have to do the viewport longhand, use a viewport node, render to that, then shift e.g. a texture rect that uses the viewport as source texture.

I don't think godot's stretch_mode viewport inherently would support this, as it clamps to large pixels, and as the article states using 2d directly loses the pixellation on rotated sprites. So using a custom viewport (which fixes the pixellation) then altering the tex coords / position of a texture rect seems a way to get it to work.

That said, if it were me, I'd just cop out and use stretch_mode 2d every time, as it skirts around the issue, and just looks better imo. 😁

@securas
Copy link
Author

securas commented Mar 7, 2021

As I said before. Using 2D stretch mode is out of the question for pure pixel art games. If Godot cannot support it, it will not be pixel art friendly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants