godotengine / godot

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

2D Sprite "jittering", very noticeable when using camera smoothing #35606

Closed forbjok closed 3 years ago

forbjok commented 4 years ago

Godot version: 3.1.2 stable, GitHub master (3.2), Vulkan branch (4.0)

OS/device including version: Windows 10 x64 v1909

Issue description: Pretty much as soon as I made my test level larger than a single screen, I noticed something was off. After a bit of testing, I've found out what it is. It seems like any time the camera moves, there is some sort of inconsistency in the positions sprites are being rendered at, causing them to "jitter" back and forth by one pixel even though the camera is only moving in one direction.

Example gifs showing the issue: A5aCVeO EhPwu5N

How noticeable it is depends on circumstances. Generally, from what I've found, it's most noticeable if you have camera smoothing turned on and the smoothing speed set to a very low value, but the problem isn't related to the camera smoothing. It's also present with camera smoothing turned off, just harder to notice.

In case it was a bug that had already been fixed, I tried compiling Godot myself from both the master (presumably what will become 3.2) and vulkan (4.0?) branches to try them out, but unfortunately the problem still seems to be present in both of those. It looks a bit different in the Vulkan branch, due to what I assume is some sort of smoothing/blur filter being applied (there may be a way to turn that off, but I couldn't find it) to the background, but it is definitely still there.

Steps to reproduce:

  1. Start minimal reproduction project
  2. Run far enough to the right that the camera starts scrolling and stop.
  3. Watch the vertical lines of the background brick wall or the ground near the player to see the sprite jittering back and forth by one pixel until the camera fully stops

Minimal reproduction project: godot-jitter-repro-project.zip

Seel commented 4 years ago

Seems to me like the camera itself isn't respecting the pixel snap setting.

forbjok commented 4 years ago

The issue happens regardless of whether Pixel Snap is enabled or not. If the issue was caused by the camera, shouldn't everything be jittering? As far as I can tell, it's only the sprites that jitter. The background tilemap and text label does not.

Seel commented 4 years ago

The issue happens regardless of whether Pixel Snap is enabled or not. If the issue was caused by the camera, shouldn't everything be jittering? As far as I can tell, it's only the sprites that jitter. The background tilemap and text label does not.

The tilemap is one cohesive unit so it does jitter, but individual tiles within might not. I tried this right now with multiple tilemap layers, background and foreground, and that way you can really see them jitter too.

Since pixel snap doesn't seem to affect the camera, it's position.x can be something like 10.375940 and relative to this position one object that's pixel snapped might get it's position rounded up to the nearest pixel, while another one's gets rounded down.

At least that's my interpretation of what's happening.

forbjok commented 4 years ago

The tilemap is one cohesive unit so it does jitter, but individual tiles within might not.

I tried looking at the edge of the screen during scrolling, and I was not able to spot any jittering on the background itself relative to the edge. Shouldn't it be possible to see it jitter if comparing it to the edge?

In my example project the background and foreground actually are two separate tilemaps (although they use the same tileset), and I've never been able to spot any jittering between them.

Maybe pixel snap works differently than I thought, but I would have thought that if this was caused by pixel snapping, it would stop happening when Pixel Snap was turned off?

it's position.x can be something like 10.375940 and relative to this position one object that's pixel snapped might get it's position rounded up to the nearest pixel, while another one's gets rounded down.

My first thought was also that it was caused by some sort of weird inconsistent rounding of pixel coordinates. Basically, some sort of rounding being applied somewhere that's either RNG or just inconsistent in that sometimes higher values get rounded down instead of up. I couldn't find anything obvious when poking around in the source code though.

Seel commented 4 years ago

Not saying pixel snap is the cause, rather it doesn't really do anything.

pixeltest-isodemo.zip

I messed with the 2D isometric demo a bit, you can really see the jitter between walls and pillars and the floor tiles in this.

forbjok commented 4 years ago

Ah, yes. There's definitely some jittering going on there. I wonder why I'm only seeing it with the sprites in my game.

lawnjelly commented 4 years ago

I've not done any 2D stuff with Godot but it looks like the problem is something like this : your character sprite position is not snapped relative to your background.

i.e. if snapping is every 1.0 in units, and your background is at position 4.0, and your sprite is at 6.7:

At camera position 2.0

(assuming we are using a floor function to snap, but same will happen with round) background = 4.0 - 2.0 = 2.0 = 2 sprite = 6.7 - 2.0 = 4.7 = 4

at camera position 1.6

background = 4.0 - 1.6 = 2.4 = 2 sprite = 6.7 - 1.6 = 5.1 = 5

Thus as the camera moves you are getting relative movement between the two by 1 pixel. The solution is to snap the background and the sprite positions relative to each other.

Seel commented 4 years ago

Thus as the camera moves you are getting relative movement between the two by 1 pixel. The solution is to snap the background and the sprite positions relative to each other.

Pixel snap should be handling this, shouldn't it? And it does, if you have an integer camera position, but since the camera is not snapped you get unpredictable results.

If for example you put this in the camera's _process function: position = player.position.round() force_update_scroll() Everything works and there's no jitter with this, but you cannot use the camera node's smoothing function.

forbjok commented 4 years ago

I tried manually .floor()-ing the positions of the camera and sprite in various gdscript handlers yesterday, and so far I haven't found anything that actually fixes the jitter. Maybe I could try implementing a custom camera that just sets the underlying render position directly (pretty sure I saw some way to do that somewhere, but I forget what it's called) and .floor()s it. Will definitely try that later.

If that fixes it, this kinda seems like something that should just be done automatically by default. I can't think of any reason you'd ever actually want objects arbitrarily jittering relative to each other during scrolling.

Seel commented 4 years ago

I tried manually .floor()-ing the positions of the camera and sprite in various gdscript handlers yesterday, and so far I haven't found anything that actually fixes the jitter.

Did you try without camera smoothing enabled? Otherwise the smoothing happens after your script and turns the camera position into a float again.

my scene tree is ordered like this:

player
    sprite
    collision shape etc.
camera

This way the camera gets the player position after the player moved.

onready var player = get_node("../Player")

func _process(delta):
    position = player.position.round()
    force_update_scroll()

This should work.

lawnjelly commented 4 years ago

Ok I downloaded your demo project and got it working:

1) In your player scene, create a new Node2D and call it Player 2) Make this new Node2D the scene root (so that the kinematic thing is a child of it) 3) Move the Sprite to be a child of this root node. 4) Change the

onready var sprite: = get_node("Sprite")

to

onready var sprite: = get_node("../Sprite")

5) In _physics_process at the end add:

    sprite.position = Vector2(floor(position.x), floor(position.y))

Obviously you'll want to tweak it a bit but the general idea is this .. to separate the physics representation (kinematic) from the visual representation. The physics rep can have float coordinates, but the visual representation should be floored or rounded to whole numbers.

With this working, there will always be an integer relative relationship between the background and the sprites, and you won't get this jitter.

forbjok commented 4 years ago

Did you try without camera smoothing enabled?

I don't remember if I tried rounding the camera position with smoothing turned off, but the jitter is present without smoothing. It's just more noticeable when smoothing is enabled.

This should work.

I'll try that when I have the opportunity, although I'll probably still have to either implement a custom camera or just implement custom smoothing on the existing one.

Ok I downloaded your demo project and got it working:

  1. In your player scene, create a new Node2D and call it Player
  2. Make this new Node2D the scene root (so that the kinematic thing is a child of it)
  3. Move the Sprite to be a child of this root node.
  4. Change the
onready var sprite: = get_node("Sprite")

to

onready var sprite: = get_node("../Sprite")
  1. In _physics_process at the end add:
  sprite.position = Vector2(floor(position.x), floor(position.y))

Obviously you'll want to tweak it a bit but the general idea is this .. to separate the physics representation (kinematic) from the visual representation. The physics rep can have float coordinates, but the visual representation should be floored or rounded to whole numbers.

With this working, there will always be an integer relative relationship between the background and the sprites, and you won't get this jitter.

I'll try this as well when I have the opportunity, but wouldn't rounding the position set to the viewport's canvas_transform (I believe this is what Camera2D sets internally?) also fix the issue?

lawnjelly commented 4 years ago

I'll try this as well when I have the opportunity, but wouldn't rounding the position set to the viewport's canvas_transform (I believe this is what Camera2D sets internally?) also fix the issue?

I'm not sure exactly what you mean, but it may be possible to achieve the same math effect via a slightly different mechanism (again, I am not familiar with 2D). But once you have a moving camera, snapping positions to pixels on the screen no longer works because you will get this jiggling, you need to snap relative to the SCENE.

golddotasksquestions commented 4 years ago

I don't use Camera smoothing with pixelart for that reason. Which is a shame, because if it wasn't for this issue, there would be no reason not to use smoothing.

forbjok commented 4 years ago

I tried manually .floor()-ing the positions of the camera and sprite in various gdscript handlers yesterday, and so far I haven't found anything that actually fixes the jitter.

Did you try without camera smoothing enabled? Otherwise the smoothing happens after your script and turns the camera position into a float again.

my scene tree is ordered like this:

player
    sprite
    collision shape etc.
camera

This way the camera gets the player position after the player moved.

onready var player = get_node("../Player")

func _process(delta):
  position = player.position.round()
  force_update_scroll()

This should work.

I can confirm that this fixes the jitter, although it makes the built-in smoothing unusable, so I'll have to reimplement that manually.

Ok I downloaded your demo project and got it working:

  1. In your player scene, create a new Node2D and call it Player
  2. Make this new Node2D the scene root (so that the kinematic thing is a child of it)
  3. Move the Sprite to be a child of this root node.
  4. Change the
onready var sprite: = get_node("Sprite")

to

onready var sprite: = get_node("../Sprite")
  1. In _physics_process at the end add:
  sprite.position = Vector2(floor(position.x), floor(position.y))

Obviously you'll want to tweak it a bit but the general idea is this .. to separate the physics representation (kinematic) from the visual representation. The physics rep can have float coordinates, but the visual representation should be floored or rounded to whole numbers.

With this working, there will always be an integer relative relationship between the background and the sprites, and you won't get this jitter.

This does fix the jitter for the player, but would have to be implemented separately for every sprite, making it rather impractical.

Ultimately, this is the solution (or workaround) I arrived at:

extends Camera2D

# Smoothing duration in seconds
const SMOOTHING_DURATION: = 0.2

# The node to follow
var target: Node2D = null

# Current position of the camera
var current_position: Vector2

# Position the camera is moving towards
var destination_position: Vector2

func _ready() -> void:
    current_position = position

func _process(delta: float) -> void:
    destination_position = target.position
    current_position += Vector2(destination_position.x - current_position.x, destination_position.y - current_position.y) / SMOOTHING_DURATION * delta

    position = current_position.round()
    force_update_scroll()

It's a combination of Seel's suggestion with a custom reimplementation of camera smoothing based on one I created in a MonoGame-based engine years ago. This completely fixes the issue for all sprites without having to do anything specific in each sprite, while still having a nice smooth camera.

This still seems like something Camera2D should be doing internally by default though, as I can't imagine the current behavior ever being desirable.

Calinou commented 4 years ago

It seems Camera2D currently doesn't have specific code to handle the Use Pixel Snap project setting. It makes sense to also snap the camera's coordinates when that setting is enabled.

Someone should try to modify scene/2d/camera_2d.cpp to snap the camera's coordinates when pixel snap is enabled and check whether the issue is still here (when Use Pixel Snap is enabled).

lawnjelly commented 4 years ago

Agree, the simple camera snapping may be good enough in this case :+1:. The snapping per sprite works with the more general case, because it allows things like variable size pixel blocks (e.g. 1 sprite pixel is 4x4 on screen), and zooming, non-snapped camera etc. But it may be overkill in this situation, and could of course be custom implemented in a game that needed this.

forbjok commented 4 years ago

I made a pull request that rounds the Camera2D origin to nearest pixel before setting the canvas transform. This fixes the jittering without any special camera script.

Not sure if it's implemented in the most idiomatic way possible, but it seems to work at least.

fossegutten commented 4 years ago

The reason that smoothing and custom camera scripts seem to increase jitter is because camera parent node will always be aligned to camera perfectly, if smoothing is disabled. Camera parent is usually player node, so player and background will draw fine, if smoothing is disabled. Other nodes with non-rounded positions will still jitter. If pixel snap in editor is used for backgrounds / non-moving sprites, you will only see jitter for moving sprites.

@forbjok commit seems to fix the issue for all cases. Very clever indeed! I was using a gdscript workaround hack for this.

Tuatarian commented 4 years ago

One fix for this that worked for me, when pixel snap and such did not, was setting the Camera's update mode in the inspector from Idle to Physics. Completely removed the jitter

fossegutten commented 4 years ago

I figured out a way to fix the jitter completely from gdscript, by coding a custom camera. Using physics mode or pixel 2D snap is not necessary at all.

I do this as a final step on my camera transform:

t.origin = t.origin.snapped(Vector2.ONE * global_scale)

To port this to the default camera, the position should be Snapped by the zoom value, instead of Vector2(1.0, 1.0) / rounding! Simple as that.

nihiluis commented 4 years ago

is this the same bug?

https://godotengine.org/qa/78937/remove-render-jitter-artifacts-from-moving-camera

The strategies laid out in this issue didn't work for me.

thejokertm commented 4 years ago

I am also having trouble with this... unfortunately the workarounds mentioned here didn't do it for me. There is still jittering. When I implement the code solution above my player character starts vibrating, jittering in very quick succession. Any other ideas?

https://streamable.com/rq0jka https://streamable.com/drg21f

Calinou commented 4 years ago

@thejokertm The player doesn't seem to vibrate on the second video. It's just that subpixel movement can't be displayed as a result of you using the viewport stretch mode (presumably). To display subpixel movement, you need to use the 2d stretch mode instead. See Multiple resolutions in the documentation for more information.

forbjok commented 4 years ago

I am also having trouble with this... unfortunately the workarounds mentioned here didn't do it for me. There is still jittering. When I implement the code solution above my player character starts vibrating, jittering in very quick succession. Any other ideas?

https://streamable.com/rq0jka https://streamable.com/drg21f

I don't see any evidence of the kind of jittering this issue was about in either of these videos. It looks to me like the movement just makes it look a bit fuzzy/unsmooth due to the camera only scrolling by whole pixels, presumably due to rendering to a low-resolution viewport and then upscaling the whole image. (this is what I was doing)

That's a problem the solutions in this issue did not really address. I think in order to avoid that, you'd have to render (and have the camera scroll at) a higher resolution, while simply upscaling the sprite art. The downside of this is that you'd get the potential for pixel-art sprites being misaligned with each other, due to rendering no longer operating with the pixel-art's resolution of pixels. The upside is that you get much smoother and more pleasant scrolling and movement.

There are various indie games using both of these approaches. The more orthodox (more accurate to real old-school pixel art games) method of rendering to a low-res viewport apprears to be used by Axiom Verge and Bloodstained: Curse of the Moon, and as a result they look a bit fuzzy and unsmooth whenever scrolling is happening. They do not use camera smoothing, and instead have the camera hard-locked to the player character, presumably to avoid the issue of the player character appearing to jitter while the camera is catching up.

Stardew Valley and VirtuaVerse (and most likely many others), on the other hand appear to embrace the higher resolution for the sake of the smoother and more pleasant to look at scrolling/movement, at the cost of losing guaranteed pixel alignment between sprites. Despite not being quite as accurate to real old-school low-resolution games, they still manage to preserve the general aesthetic very well. In all honestly, after seeing the downsides of low-res viewport rendering (fuzzy/unsmooth scrolling and movement, no really good clean way to avoid the focused object jittering if it is moving while camera is catching up), I'm personally leaning towards this approach being the better one.

m6502 commented 4 years ago

@forbjok I may have a present for you.

GIF 25-Aug-20 10-29-01

ujjawal49 commented 4 years ago

@m6502 please help me too I'm also facing this issue. In my case I'm not even moving the camera. Only Sprites are moving using this code.

public float speed = 2f;

void Update () 
{
transform.position += Vector2.left * speed * Time.deltaTime;
}

Please help!😣

m6502 commented 4 years ago

@ujjawal49 do you have a small test project I could try?

ujjawal49 commented 4 years ago

@m6502 sure let me make the project on a small scale and I'll give you the link for it.

ujjawal49 commented 4 years ago

@m6502 ok so here is the project :- https://drive.google.com/file/d/1DxWb-xBdVp1gf_G-HO70-Cz_HP7rk-r_/view?usp=drivesdk

In the project while playing, look at the pipes closely while they come towards the left. It looks so bad.

If you want to check how bad it feels while playing the APK it's here (ignore all the buttons, just click on start, other buttons are for my test purposes):- https://drive.google.com/file/d/1qe11Q3pib52yrS7LUdACTYMY1dilbmaw/view?usp=drivesdk

Thanks.

m6502 commented 4 years ago

@ujjawal49 there's a small problem with your project, in that it's an Unity 3D project. Am I missing something? 😅

ujjawal49 commented 4 years ago

@m6502 it's a .rar file and inside it's a unity project. I don't think you are missing something, what's the issue?

ujjawal49 commented 4 years ago

@m6502 oh yes I haven't saw that it's a godot topic. My bad, any ideas how to fix it in unity?

m6502 commented 4 years ago

(Insert joke about upgrading from Unity to Godot here) :stuck_out_tongue_winking_eye:

ujjawal49 commented 4 years ago

@m6502 no, I believe I'll be able to fix this issue within 5-6 thousand years so no chance of upgrading from unity to godot.😂😣

m6502 commented 4 years ago

Made the pull requests to fix this :-)

Zireael07 commented 4 years ago

You're trying to PR to your own fork's master, not Godot's master branch.

m6502 commented 4 years ago

Fantastic!

golddotasksquestions commented 4 years ago

Is it possible to get those fixes cherrypicked for the 3.X branch?

Zireael07 commented 4 years ago

One of the PRs is made for 3.2 branch

m6502 commented 4 years ago

@golddotasksquestions Yes, I created two pull requests, for 3.x and 4.x. Hopefully it'll be there for the 3.2.3 release. I'll be on 3.x for a while :-)

Calinou commented 4 years ago

Hopefully it'll be there for the 3.2.3 release

The 3.2.3 release is imminent (and we really want to avoid any regressions), so I'm afraid it can only be merged for 3.2.4.

neilfranci commented 3 years ago

@golddotasksquestions Yes, I created two pull requests, for 3.x and 4.x. Hopefully it'll be there for the 3.2.3 release. I'll be on 3.x for a while :-)

@forbjok I may have a present for you.

GIF 25-Aug-20 10-29-01

Hi, how you fix that problem I have the same when player stop it jitting, I dont know how to fix it at this time. v3.2.3

m6502 commented 3 years ago

@SyliawDeV It'll be corrected when you download the next Godot version including the pull request that fixes the bug :-)

neilfranci commented 3 years ago

@SyliawDeV It'll be corrected when you download the next Godot version including the pull request that fixes the bug :-)

oh maybe v3.2.4 stable, I check beta version everyday but not seem it fix or not. But hear u say that I can wait for new version. :)

Calinou commented 3 years ago

@SyliawDeV https://github.com/godotengine/godot/pull/41535 hasn't been merged yet, so it won't be in 3.2.4 if it's not merged by the time 3.2.4 is released.

neilfranci commented 3 years ago

@SyliawDeV https://github.com/godotengine/godot/pull/41535 hasn't been merged yet, so it won't be in 3.2.4 if it's not merged by the time 3.2.4 is released.

Hm, dont know how other people do with that. Until I know that I will sleep in freeze.

m6502 commented 3 years ago

@Calinou @SyliawDeV I'll try to add the required project setting and test it this weekend so it can be merged in the next revision.

neilfranci commented 3 years ago

@Calinou @SyliawDeV I'll try to add the required project setting and test it this weekend so it can be merged in the next revision.

You just warm me from freeze. 🤩

akien-mga commented 3 years ago

Fixed by #43194 (4.0) and #43554 (3.2.4).