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

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

Closed
Tracked by #86837
Cerno-b opened this issue Jan 25, 2022 · 19 comments
Closed
Tracked by #86837

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

Cerno-b opened this issue Jan 25, 2022 · 19 comments

Comments

@Cerno-b
Copy link
Contributor

Cerno-b commented Jan 25, 2022

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:

  • Display -> Window -> Size -> 320x180
  • Display -> Window -> Stretch -> viewport

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
Copy link
Member

Calinou commented Jan 25, 2022

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
Copy link
Contributor Author

Cerno-b commented Jan 25, 2022

@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
Copy link
Member

Calinou commented Jan 25, 2022

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 #43813, #43554 and #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: #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
Copy link
Contributor Author

Cerno-b commented Jan 25, 2022

@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
Copy link
Contributor Author

Cerno-b commented Jan 25, 2022

@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
Copy link
Member

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:

  • most 2D users do not understand and do not want to understand how GPUs work (understandable)
  • there are too many options for 2D beginners (the engine allows you to shoot yourself in the foot)

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
Copy link
Member

lawnjelly commented Jan 26, 2022

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

2022-01-26.08-46-08.mp4

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

2022-01-26.08-45-17.mp4

SPEED_INT / ANGLE_float - With GPU snap:

2022-01-26.08-51-20.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
Copy link
Contributor Author

Cerno-b commented Jan 26, 2022

@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:

  • pixel-perfect, palette-perfect (your third video above)
  • pixel-perfect, bilinear filtering (the behavior I have seen in Godot 4)
  • mixed resolution (Stardew Valley style)
  • high-resolution 2D (non-pixel art)

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
Copy link
Member

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
Copy link
Contributor Author

Cerno-b commented Jan 27, 2022

@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
Copy link
Member

Calinou commented Jan 27, 2022

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
Copy link
Contributor Author

Cerno-b commented Jan 27, 2022

@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
Copy link
Member

Calinou commented Jan 27, 2022

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
Copy link
Contributor Author

Cerno-b commented Jan 28, 2022

@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
Copy link
Contributor

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

@Cerno-b
Copy link
Contributor Author

Cerno-b commented Jan 28, 2022

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

@markdibarry
Copy link
Contributor

markdibarry commented Jan 5, 2024

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

@Cerno-b
Copy link
Contributor Author

Cerno-b commented Jan 5, 2024

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
Copy link
Member

clayjohn commented Jan 6, 2024

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

@clayjohn clayjohn closed this as completed Jan 6, 2024
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

7 participants