godotengine / godot

Godot Engine – Multi-platform 2D and 3D game engine
https://godotengine.org
MIT License
91.16k stars 21.2k forks source link

Rotation plus motion causes jitter in pixel-perfect mode #57221

Closed Cerno-b closed 10 months ago

Cerno-b commented 2 years ago

Godot version

3.4.2.stable.official

System information

Win10, NVIDIA GeForce GTX 960

Issue description

I am working in pixel-perfect mode (viewport). When I rotate a sprite it follows the pixel grid perfectly, just as I want. Motion is also as expected, even if I use float velocities, the character is always fixed to the pixel grid, which is exactly what I would expect.

Now this:

If I move my rotated character with integer speed, this works fine (here: 1 px per frame):

ok

But if I move my character with float speeds (here: 1.01 px per frame), the rotation is not stable and I get jittery pixel shifts all across the sprite:

not_ok

This effect seems to stem from the interaction between the float position and the float angle.

This is a version with float speed (1.01 px per frame) and int (90°) rotation, which is fine:

ok_90_degree

It seems like Godot maps the float position and angle to the pixel grid in one go, which accumulates rounding errors between the two parameters into a single step. This could lead to the fractional part of the position influencing the outcome of the rotation, which should be independent from it.

If that is the case, a solution might lie in the direction of first rounding the position before applying the rotation.

EDIT

The same seems to be happing for scaling. I was able to see a problem with the combination of float scale and float position. But I was also able to see the problem with int position (px/frame) and a very specific scale (1.5 or 2.5), other float scalings seem to work with int positions.

Steps to reproduce

This is the minimal example code I used (also see Godot project below). It's pretty standard. Default settings except these two:

Change the values in the code as the comment states to get the different behaviors:

func _physics_process(delta):

    var SPEED_INT = 1.0
    var SPEED_FLOAT = 1.01

    var ANGLE_90 = PI/2
    var ANGLE_float = 1

    # change these to reproduce:
    # SPEED_INT / ANGLE_float --> ok
    # SPEED_INT / ANGLE_90 --> ok
    # SPEED_FLOAT / ANGLE_90 --> ok
    # SPEED_FLOAT / ANGLE_float --> not ok
    var speed = SPEED_INT  
    var angle = ANGLE_float

    if Input.is_key_pressed(KEY_A):
        self.position.x -= speed
    if Input.is_key_pressed(KEY_D):
        self.position.x += speed
    if Input.is_key_pressed(KEY_W):
        self.position.y -= speed
    if Input.is_key_pressed(KEY_S):
        self.position.y += speed
    if Input.is_key_pressed(KEY_E):
        self.rotate(angle)
    if Input.is_key_pressed(KEY_Q):
        self.rotate(-angle)
    if Input.is_key_pressed(KEY_SPACE):
        self.rotation = 0

Minimal reproduction project

game.zip

Calinou commented 2 years ago

cc @lawnjelly

Many fixes to pixel snap were attempted in the 3.3 cycle, but they were reverted because they always ended up causing regressions in some cases.

Cerno-b commented 2 years ago

@Calinou Wow, thanks for the quick reply!

So since we are at 3.4 now, are there any plans to finally go through with the changes? I remember that pixel snap used to be much worse than it currently is, so I am really happy with the progress so far.

It's just that this rotation bug is so obvious that it's a complete show-stopper for anyone making a pixel-precise game and wants rotation (which is not a rare feature I would guess).

So I am by no means complaining. I highly respect the work you guys and gals do here

It's just that I feel Godot is so close to being a perfect fit for pixel art games that it would be a shame if things like this couldn't be ironed out. Especially since this issue just works in Game Maker, and I think Godot is superior to that engine in many ways.

If this is a regression issue, is the main problem that it breaks existing games or does it break other features of Godot? In the former case, would it be possible to re-introduce the changes with an optional parameter or is this something you think can be fixed permanently given enough dev time?

If this is not currently on the map, is there anything I can do to help speed up this process? To be honest, I won't have time to dive into the code myself, but if this could be achieved by other means (a moderate bounty maybe), I might be able to chip in (within my means). Of course I would be willing to test changes if that helps

Calinou commented 2 years ago

So since we are at 3.4 now, are there any plans to finally go through with the changes? I remember that pixel snap used to be much worse than it currently is, so I am really happy with the progress so far.

See https://github.com/godotengine/godot/pull/43813, https://github.com/godotengine/godot/pull/43554 and https://github.com/godotengine/godot/pull/44690 (which were all fully or partially reverted, IIRC).

Godot 4.0 exposes different project settings. There is no longer an Use Pixel Snap option, but there's now Snap 2d Transforms To Pixel and Snap 2d Vertices To Pixel which can be enabled independently. However, these don't appear to work well currently: https://github.com/godotengine/godot/issues/56793

If this is a regression issue, is the main problem that it breaks existing games or does it break other features of Godot? In the former case, would it be possible to re-introduce the changes with an optional parameter or is this something you think can be fixed permanently given enough dev time?

The current behavior in 3.4 isn't a regression from earlier releases. It's just that rotating sprites when using the viewport stretch mode will always be a contentious issue, and one that cannot be fully resolved without using complex algorithms such as RotSprite.

If you don't mind rotated pixels not matching the pixel grid (which is what many Game Maker games do), use the 2d stretch mode instead of viewport. An alternative is to pre-generate rotated versions of sprites using the RotSprite algorithm (or use a shader to do this in real-time). (You don't need to do this for 90° increments, only for intermediate values such as 22.5°, 45°, …)

Cerno-b commented 2 years ago

@Calinou Apologies in advance if I misunderstood your answer, but I have the feeling we are talking about different things.

My problem is not that the rotation itself looks pixely. This is something I fully expect, since pixels must snap to the grid. The problem is that for a fixed angle, the actual pixel pattern of the sprite changes when I move the sprite around (e.g. the white pixel appearing and disappearing on Chrono's left foot in the second gif).

Please let me clarify with still images. These three examples have the exact same rotation and only differ in the sprite's position

image image image

This may be subtle in the still image but it is very noticeable in motion.

I am not sure about the connection to the RotSprite algorithm. The rotation itself looks fine to me. The problem is that it's not stable when I add floating point motion. The same occurs with scaling, so it's definitely not purely a rotation issue. I still think there might be something wrong with the rounding, but as I said, I don't know the code enough to verify. I might look into the tickets you posted to check if I can spot something.

I found an interesting angle that produces this scanline-like behavior which enforces my assumption that this could be a rounding issue, I rebuilt the game in 30 fps, since my gif recorder maxes out at 60fps, so the effect should be more apparent now. This is only a very slight rotation, but it's there. The angle is fixed, only the translation changes:

not_ok_scanlines

About Game Maker: I have completed 4 Ludum Dare games with GM, two of which made heavy use of rotation, and I had absolutely no issues with GM breaking the pixel grid. For all of GM's failings, this was ironclad behavior that I was always able to depend upon. It's possible to disable pixel snap in GM, so I am sure there are games that do not obey the grid, but once you enable it, it just works. 2d is not an option for me as making pixel-perfect games is kinda my thing. ;)

These two games I made feature pixel-precision and rotation in GameMaker:

https://ldjam.com/events/ludum-dare/38/a-perfectly-lovely-adventure https://ldjam.com/events/ludum-dare/40/boiler-room-defense

This image is a screenshot from A Perfectly Lovely Adventure, loaded in Aseprite with grid enabled for illustration. The whole bottom part is actually a sphere that rotates as the player walks on it and it behaves exactly as I expect:

image

I marked two objects that correspond to one another. One is the original, the other is rotated. It looks pixely but it obeys the grid.

Cerno-b commented 2 years ago

@Calinou

There is no longer an Use Pixel Snap option, but there's now Snap 2d Transforms To Pixel and Snap 2d Vertices To Pixel which can be enabled independently.

I haven't found these settings in my version of Godot, were they reverted as well?

Forget my comment, I didn't read properly, you said Godot 4, I am still using 3.

Edit

Okay I got the latest Godot 4 alpha and there the mechanism works differently. Not sure if I did everything correctly, because now it looks like this:

No rotation: image

Rotation: image

From what I can see, the motion artifacts have disappeared, but now another problem occurs:

Purists will not only want their pixel perfect games to obey the grid (which this version does). They will also want the games to obey the palette, which this version does not.

I can totally see how this behavior is desirable, and I am not advocating to change it, but I think it would be a good idea to also offer the option to get the old behavior (pixel-perfectness and palette-perfectness). The Godot 3 behavior has this, but as I stated, it also has the nasty jitter problem. Having both the old behavior but with the (apparent) fix of the jitter would be great!

Do you think this should go into a new ticket because it is now more of a Godot 4 topic?

lawnjelly commented 2 years ago

You are getting jiggling in your project because when at an angle, as a sprite moves in floating point units, the texels that will be sampled at a particular pixel will "jiggle" in relation to those sampled at neighbouring pixels. This is a consequence of maths and filtering and how GPUs work.

To do pixel games in Godot (especially if you hope to use any rotations or float offset) it is essential to understand texture filtering, especially nearest neighbour filtering. See e.g.: https://www.essentialmath.com/OtherPubs/Texture_Filtering.pdf

I would encourage you to work through the maths with a paper and pen, calculating the positions of few pixels as a sprite moves past and calculating where it will sample the texels from. Once you understand how the maths works, you will be better able to predict why and when these effects will happen and how to avoid them.

This is partly why I now avoid this area - there are two problems we have in Godot for 2d pixel perfect viewport stretch games which work in combination:

Things like floating point positioning, rotation, scaling, parallax are all things you will want to avoid for this kind of game in most cases, unless you have a deep understanding of the math.

Here's some tips on making pixel games: https://github.com/lawnjelly/godot-snapping-demo

lawnjelly commented 2 years ago

SPEED_FLOAT / ANGLE_float - Without GPU snap: (here you can see the nearest neighbour affecting filtering every frame)

https://user-images.githubusercontent.com/21999379/151131312-e00be50d-46be-40bb-bc4e-594c9229885b.mp4

SPEED_FLOAT / ANGLE_float - With GPU snap: (mostly stays stable until jumping over a boundary, due to not being on integer grid)

https://user-images.githubusercontent.com/21999379/151131347-c429ac4b-f968-469c-b44a-0e7ad6e73283.mp4

SPEED_INT / ANGLE_float - With GPU snap:

https://user-images.githubusercontent.com/21999379/151132074-25aacd91-de57-4055-812a-d6c64056a164.mp4

As you can see for a pixel game you will either want to move by integer pixel amounts each tick, or quantize to this after moving, otherwise you will get jiggle due to filtering.

Cerno-b commented 2 years ago

@lawnjelly Thanks for your extensive replies, I really appreciate it.

A few thoughts: From a pure user perspective, I think it would be really nice if Godot would be able to offer the different options in a more user-friendly and systematic manner. Of course I am an absolute newb in Godot and what I am asking for could be very hard or impossible to do, please bear with my naivete for a moment.

I have some background in image processing so the link you posted was a nice throwback to my uni time. I am familiar with the concepts, the question to me is how we can apply them in a more user-friendly way. You refer to all the complexities of mapping texels to pixels and note that the users won't want to dive into the nitty-gritty, and I agree. But then again, they shouldn't have to either.

An example: You gave me a very important hint in your last post (3rd video). I implemented something following that concept and it works like a charm:

func _ready():
    var position = Vector2(self.position.x, self.position.y)

func _physics_process(delta):
        var speed = 1.01

    if Input.is_key_pressed(KEY_A):
        position[0] -= speed
    if Input.is_key_pressed(KEY_D):
        position[0] += speed
    if Input.is_key_pressed(KEY_W):
        position[1] -= speed
    if Input.is_key_pressed(KEY_S):
        position[1] -= speed

        # this rounding fixes the problem
    self.position.x = round(position[0])
    self.position.y = round(position[1])

So basically I store my float position in a member variable and round it before writing it to the actual sprite's position.

Now my question is, why isn't this done in the engine automatically once we enable pixel snap? Wouldn't that be exactly what is expected by the user? They rotate by float and move by float and still their sprite robustly snaps to the pixel grid and does not cause any jitter. Sure the users can fix this themselves, but should they have to?

I see that Game Maker does pixel-perfect rotation well and it just works. I see that Unity has a pixel-perfect mode (although I don't know how well this works). I believe Godot should have the same ambitions. I believe in this project enough to be a Patron, and I would also like to help improving it in more tangible ways, as much as my time allows.

Would you be willing to reopen this can of worms again? To me, it looks like Godot is very closely missing an important mark here.

To me it would make a lot of sense to take inventory of all possible 2D modes a user might want, then try to offer project settings that allow to pick exactly the behavior that the user wants. I can't imagine there are that many modes to take into consideration.

I could try and draft a first version of these modes, something like this:

I'm sure there are a few more, but I don't think there would be that many. I'd try and mock some example images for clarity. After that we could discuss problems with the concepts, what modes might be missing, or what corner cases we would have to address. It would probably help me understand what has been tried and why it failed.

This might be a little naive coming from someone who hasn't really looked into the code much, but I really think working out a concept about how more user-friendly pixel support could be established is a good idea. From what I can see in Godot 3.4, pixel perfectness is almost achieved already anyway.

lawnjelly commented 2 years ago

Would you be willing to reopen this can of worms again?

Not personally, no. I'm mostly on 3D now. But this is open source, anyone else can work on something which interests them.

See these PRs and the linked issues:

43554

43813

44690

46614

46615

46657

Also reduz has some CPU snapping in 4.0.

43194

Cerno-b commented 2 years ago

@lawnjelly Thanks for your input.

One question, since I'm really new to Godot's organisational structure: Are there any people who decide on higher-level concepts for 2D in Godot? Either officially or inofficialy by virtue of seniority or otherwise? It seems a little bit like there is not a clear definition what would be the expected behavior for certain things.

For instance, Godot 3 does pixel-perfect rotation with nearest-neighbor interpolation, while Godot 4 seems to have switched to bilinear (which does not work for purist pixel art). So before I start implementing something that may go against some existing design decisions, it would be nice to discuss this with people who have some weight in how things should be in Godot.

If I am right in my observation that some things are not formally defined, I think it would be a good idea to whitepaper these concepts first and then ensure the implementation follows that design.

So, do you know someone who might be interested and knowledgeable enough to nail down these concepts and then realize them in a structured approach? Or should I just go ahead and draft something for discussion? Is there a place for concept discussions, or should I just open another ticket?

Calinou commented 2 years ago

For instance, Godot 3 does pixel-perfect rotation with nearest-neighbor interpolation, while Godot 4 seems to have switched to bilinear (which does not work for purist pixel art). So before I start implementing something that may go against some existing design decisions, it would be nice to discuss this with people who have some weight in how things should be in Godot.

In Godot 4.0, the filter mode is no longer stored in the texture itself, but in the location where the texture is used instead. You can change the default 2D filter mode in the Project Settings.

Is there a place for concept discussions, or should I just open another ticket?

The Godot proposals repository should be used for feature proposals.

Cerno-b commented 2 years ago

@Calinou Thanks for the proposals link, I'll take a look at it.

About the bilinear thing, I am not talking about the import of sprites (although I welcome having the filter mode in one place instead of resetting it for each and every sprite).

What I was talking about is this:

Godot 3:

image

Godot 4:

image

Apparently, the pixels still fit the grid perfectly, but their colors are bilinearly interpolated. For purists that's a no-go, although I would understand how this would be a neat option to have it either way.

Calinou commented 2 years ago

Apparently, the pixels still fit the grid perfectly, but their colors are bilinearly interpolated. For purists that's a no-go, although I would understand how this would be a neat option to have it either way.

This sounds like a different issue, and is likely not intended behavior. Did you set the texture filter option on the sprite in Godot 4? You may also have to do this on the CanvasTexture resource used by the sprite (in case the default filter project setting doesn't work).

Cerno-b commented 2 years ago

@Calinou Thanks, that was it.

It seems like I haven't fully grasped the new settings.

So the global setting is CanvasTexture (I love that this can now be globally set!) Then each node along the tree can deviate from the global setting or just Inherit (which is the default).

So setting the CanvasTexture to Nearest and leaving everything else, works like a charm, thank you!

So, after all this journey I still have the feeling that we should try and make it easier for pixel art games to get to the setting in a more convenient way. I read that Godot allows providing configs as cfg files, which is nice. I'm thinking whether it might be a good idea to curate a set of commonly used configs for different tasks (2d pixel perfect, 2d mixed resolution, 2d hi resolution, etc) and to offer them as presets to the user in a convenient (but optional) way when they start a new project.

Do you know whether something like this already exists?

If not, I'll try to think a little more about some presets that may make sense and if something comes out of it I'll start a discussion about them. I think that's probably a way better solution than to have a fixed set of project settings because that is too limiting.

Zireael07 commented 2 years ago

@Cerno-b: AFAIK there is nothinh like a "commonly curated set of configs", but that sounds like a use case for https://github.com/godotengine/godot-proposals/issues/1481

Cerno-b commented 2 years ago

@Zireael07 Thank you, that looks like a good starting point

markdibarry commented 10 months ago

Should this issue be closed? I believe lawnjelly clarified the behavior, since this is unavoidable.

Cerno-b commented 10 months ago

It can be closed for the time being. It was a Godot 3 issue and I think that it does not occur in Godot 4 anymore. I think there is a number of issues related to pixel perfect games in 4, but they are better discussed elsewhere. The discussion is archived here so it's fine by me to close it.

clayjohn commented 10 months ago

Thanks for the discussion everyone! As a final closing note, we are very eager to improve the experience of making pixel art games in Godot 4. So please feel free to open a new issue if you stumble onto new problems