godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.12k stars 69 forks source link

Simplify smooth camera movement in pixel perfect games #6389

Closed fractalcounty closed 6 months ago

fractalcounty commented 1 year ago

Describe the project you are working on

A low-res 2D platformer with a high-res UI, smooth camera movement, and zoom.

Describe the problem or limitation you are having in your project

In 3.0x+, the common method for making pixel perfect games with a high resolution UI was fairly straightforward. You would toggle the 2D stretch mode, set your project resolution to something like 1920 x 1080, and throw your game inside a viewport node at a desired base resolution (such as 640 x 360). If you wanted take it a step further and implement a smooth camera, a common solution was to use a framebuffer shader with custom camera logic. Although a bit cumbersome, it did the trick well enough.

In 4.0, it's no longer this simple. Implementing a high-res UI with a low-res game is doable, but every known method for smooth camera movement I could find no longer works as effectively in 4.0. Additionally, 4.0 introduces even more factors that impact how pixels are displayed in 4.0, each creating dozens of different combinations that produce vastly different, loosely documented, and often undesirable effects. While not all specific to 4.0, here are some factors I can think of:

To me, this is a painful amount of variables to deal with for such a common use case. I have tried every combination of these options alongside upscaling shaders, pixel buffers, custom camera smoothing, viewport textures, and more. The resulting process is a maddening game of cat-and-mouse in which you're constantly balancing jitter, blur, and sprite distortion while never quite eliminating one or the other.

This is the closest I was able to get before throwing in the towel. I achieved smooth camera movement on the sides of the screen, but it introduces pixel distortion. Enabling pixel/vertices snap gets rid of the pixel distortion, but introduces blur on everything. I'm not sure which is worse, so I just avoided using it.

https://user-images.githubusercontent.com/121070110/222317317-b9291247-93ce-490e-9b0f-3cfdf3f63916.mp4

Describe the feature / enhancement and how it helps to overcome the problem or limitation

The exact solution to this problem is complicated, as it is most likely a complex combination of intentional engine design, user error, and lack of documentation. However, I think a few things would help in this regard.

Before mentioning any in-engine solutions, it's worth mentioning that a detailed and precise collection in the docs outlining the best practices for pixel perfect games, especially in regards to smooth camera movement or zoom, would help a great deal without needing to touch the engine itself. It wouldn't fix the inherent issue of complexity here, but combined with some sort of "hybrid pixel perfect" starting template, it would be a decent remedy.

In terms of in-engine solutions, a few preexisting proposals such as integrated integer scaling would help alleviate this problem to some degree. However, I think a "PixelCamera2D" node or something similar designed specifically for this common use case would be wildly beneficial.

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

An example would be a camera node called "PixelCamera2D" that would allow functionality for sub-pixel perfect smooth scrolling in 2D games. This concept isn't new or particularly complicated, and this write up by Daniel Ludwig is a perfect starting point for how it could be implemented. In essence, it would just be a standard camera with extended framebuffer capabilities that would be used for low-res subviewports. The issue with this approach is that it isn't perfect (especially with parallax) and doesn't sufficiently mitigate jitter or distortion in 4.0+ for some reason.

An alternate approach may be necessary that avoids using a pixel buffer entirely that utilizes the core engine to achieve smooth camera movement in some other way. This goes well beyond my scope, but definitely worth looking into.

If this enhancement will not be used often, can it be worked around with a few lines of script?

I imagine it would be used very frequently, at least by indie developers who work with pixel art a lot. It could very easily optionally just not be used.

Is there a reason why this should be core and not an add-on in the asset library?

Normally just implementing that method on a per-project basis or as an addon would be more than sufficient, but as previously mentioned, things have changed quite a bit. Here's a few reasons why I believe this should be a core feature:

fractalcounty commented 1 year ago

Little update for anyone stumbling upon this via search engines: Reddit user /u/golddotasksquestions provided an excellent step by step write-up on how to combine low res UI with low-res games using viewports in 4.0.

As you can see, the process is pretty janky and unintuitive by Godot standards- especially for such a common practice in indie games. It's also worth mentioning that this probably won't help with smooth camera movement or alleviating jitter unless you're willing to implement a cocktail of features on top of it such as physics interpolation, custom camera logic, as well as a pixel buffer. I've tried everything I could find so far and haven't had much luck, but your mileage may vary.

My biggest concern about this whole process is the amount of new users that are going to inevitably google something as basic as "how to set up pixel perfect smooth camera in godot 4" and be faced with 3 or 4 results consisting of wildly different workarounds that don't even work anymore. For that, I think this is a pretty crucial usability issue that needs to be addressed in some shape or form.

Calinou commented 1 year ago

Can you upload the project that was used to create the video somewhere? This could be useful to try to make it work correctly.

Also, was the video recorded with integer window scaling (or a sharp bilinear shader applied on a SubViewport)?

fractalcounty commented 1 year ago

Also, was the video recorded with integer window scaling (or a sharp bilinear shader applied on a SubViewport)?

Working on cleaning up the project now. The video was recorded with manual integer window scaling; aka setting the project resolution to 1920x1080, setting the subviewport size to 640x360, and setting the subviewport scale to 3x. Bypassing the subviewport scale entirely and using the built in editor scaling in the settings achieves the same effect.

bitbrain commented 1 year ago

The current workaround with sub viewport is not optimal, as the editor only displays content in the bounding box of the sub viewport and it is not possible to drag & drop nodes into the scene. Having a large scene (e.g. TileMap) is very difficult to edit/deal with on a pixel perfect setup. The workaround is to navigate to the scene inside the SubViewport directly and edit the level from there (needs to be its own scene file). Especially for larger games this process is painful.

PixelPerfectCamera2D hopefully addresses that issue, just like Camera2D from an editor perspective, so the scene itself is still editable/fully viewable.

HybridEidolon commented 1 year ago

This issue encompasses far more individual issues than what the title suggests.

The viewport-upscaled-to-screen technique still works just fine in Godot 4.0, for both 3D and 2D, so this convention is untouched.

For @bitbrain's problem, this is easily solved by a runtime Autoload script which arranges the SceneTree to handle your viewport setup, so that it's not part of your scenes at all. You'd want autoloads in a larger project that needs to manage game state in a consistent way anyway (think scene transitions, persistent state across scenes, networking, etc). I usually set up a global autoloaded state machine that bootstraps the selected play-in-editor scene into itself to manage the game while still being able to start from a specific scene, 2D or otherwise.

The "smooth 2D camera" technique is actually fairly specific to very small viewport resolutions and the specifics of how it's implemented could practically vary between games (custom camera solutions?). I'm not sure if I would want such an effect for something that is mimicking a real retro console. This specific method only applies to a particular subcategory of a category of 2D games and thus I don't think it makes sense to have it as a core feature.

For coordinate snapping, I use this script in addition to the vertex snapping option, and it mostly works fine. In combination with Nearest filtering, you will get about 90% of the way there. Counterintuitively, you do not want to snap the Camera2D's coordinates when using this approach, otherwise you will get the jittering seen in those issues because the rounding directions across different items will not always match.

extends Node2D

class_name PixelSnapped

# usage: make this node the parent of a CanvasItem you want snapped to worldspace integer boundaries
# only apply this to purely visual node hierarchies

func _process(delta: float) -> void:
    var parent = get_parent()
    if parent == null:
        return
    position.x = round(parent.global_position.x) - parent.global_position.x
    position.y = round(parent.global_position.y) - parent.global_position.y

The editor failing to snap to integer coordinates in some scenarios is definitely a bug. If you need to, you can write tests in your game's test framework to verify that scenes have no nodes on subpixel coordinates, which will catch any project member's mistakes too.

I can't imagine how having collision shapes snap to pixels would work. Not even retro console games did this; they often used fixed point arithmetic ("subpixels") when integrating physics, but didn't allow static objects to exist at subpixel coordinates. Changing this would require a completely different physics model. A given project might want that, but I don't think this makes sense as a core feature.

The problem with sprite texture filtering exists regardless of engine. In Godot 4 you can set the default filtering to Nearest No-Repeat to alleviate this, but you're still ultimately on the hook to ensure your materials have the proper filtering settings. Once again, since Godot is general purpose, there are things you are ultimately responsible for deciding how to accomplish.

Overall I feel the story here is that Godot is a flexible engine serving a lot of interests and there is no one-size-fits-all option when it comes to rendering in 2D (IMO there never has been, but Godot comes closer than any other option I've seen). This is a problem that existed before 4 and will continue to exist forever. Asking it to serve a specific niche like this introduces a lot of unnecessary complexity to the core.

bitbrain commented 1 year ago

@HybridEidolon a GDExtension providing a PixelPerfectCamera2D node can always be an alternative, in case it is a too specific use case for Godot itself. However, I am not sure if the Godot C++ API allows for that (yet).

Asking it to serve a specific niche like this

I would not call this problem niche, considering how many pixel perfect games are out there. I have no quantifiable data to back this up, though.

EDIT @HybridEidolon a bit offtopic but could you share an example of how you implemented this?

I usually set up a global autoloaded state machine that bootstraps the selected play-in-editor scene into itself to manage the game while still being able to start from a specific scene, 2D or otherwise.

fractalcounty commented 1 year ago

@HybridEidolon I would like to clarify that the list I included in the post isn't a collection "changes the core engine needs" or "things that need to be fixed", it was just a list of factors that the end user has to battle with when approaching a common use case like this. Some of them may or may not be impacted by open issues, which is why I linked them as they only add to the complexity. This list has only grown since 3.x, which complicates things considerably.

The "smooth 2D camera" technique is actually fairly specific to very small viewport resolutions and the specifics of how it's implemented could practically vary between games

I mentioned this specific implementation because it covered a wide variety of use cases in 3.x, but is no longer as effective under identical conditions in 4.0. Like I said, it isn't perfect and is in no way a comprehensive solution. However, I did find success in tweaking it for a wide variety of different resolutions for what it's worth.

This specific method only applies to a particular subcategory of a category of 2D games and thus I don't think it makes sense to have it as a core feature.

I'm not suggesting that it specifically should be implemented into the engine as a core feature, it's just an example of one of many common techniques that previously worked. Whether or not it's a common use case ultimately comes down to opinion, but that still doesn't impact the issue of complexity that many will have to deal with.

In terms of the other suggestions you made such as using an autoloaded state machine to dynamically arrange and handle viewports or using a script to handle vertex snapping- I would personally argue that these are cumbersome workarounds that shouldn't be required in the first place. These aren't just practices for very specific and niche scenarios, these are practices that apply to pixel perfect games as a whole- which highlights the whole usability problem even more.

This is a problem that existed before 4 and will continue to exist forever. Asking it to serve a specific niche like this introduces a lot of unnecessary complexity to the core.

This isn't a matter of asking "I want Godot to implement a core engine feature that that fits the exact needs of my game". It's a specific example of usability that wasn't great to begin with that was worsened in 4.0. Adding complexity to the core is the opposite of what I'm suggesting- this is more about improving usability in general.

Additionally, it's not just the camera smoothing, it's that the Camera2D and viewport nodes are just flat out frustrating to work with in pixel perfect games. There's too many points of failures and long-standing "unsolvable" issues that have only worsened in 4.0. If these usability issues are inherent to the engine and will continue to exist forever, then a dedicated solution makes sense in my opinion. That goes beyond just having a smooth camera.

At the end of the day, the workflow for working with pixel perfect games is significantly more complicated in 4.0, and borderline unfeasible for more specific use cases like the one I initially described. The line between user error and unintended behavior is far too blurry at the moment, as issues with jitter seem to affect low-res games across the board to some degree.

I now realize that this problem likely goes beyond what I initially created this issue for. If I could guess, it's probably a cacophony of bugs, user error, and lack of documentation that all contribute to the same exceedingly complicated problem. However, I still think it's a crucial issue that needs to be addressed in some way.

HybridEidolon commented 1 year ago

I would not call this problem niche, considering how many pixel perfect games are out there.

Niche in the sense that it has very specific rendering requirements that don't gel well with any other "style", not necessarily in popularity. This has always been a difficult problem with modern rendering and will continue to be. Even moreso for super low-res art.

a bit offtopic but could you share an example of how you implemented this?

I don't have an example on hand, but the pieces you need are described in the documentation. The SceneTree's root node has 1 child node which is the "current scene" and then all of your configured Autoload child nodes when the game starts. You can then move the nodes around however you need them arranged at runtime and implement your own "change scene" functions to accommodate. In Godot 4, the SceneTree root node is a Window. It may help to open the remote scene tree debugger while the game is running to visualize how the root hierarchy is arranged.

In terms of the other suggestions you made such as using an autoloaded state machine to dynamically arrange and handle viewports or using a script to handle vertex snapping- I would personally argue that these are cumbersome workarounds that shouldn't be required in the first place.

The former is what makes Godot uniquely powerful compared to contemporaries and I disagree that it is a "workaround". No other engine I've seen gives you as much control over the runtime scene tree as Godot. Learning how these tools work makes Godot significantly more powerful than it initially appears. The current documentation is a little sparse, but it does point out this flexibility.

The latter is just one method of implementing what you need; you could perform the same logic by putting all your snapped visual nodes into a SceneTree group and iterating over them in an Autoload so you don't have to litter your scene configurations with these nodes. I would favor solutions that don't require specialized node setups if I was starting a project today, and Godot absolutely grants you the power to do that.

Additionally, it's not just the camera smoothing, it's that the Camera2D and viewport nodes are just flat out frustrating to work with in pixel perfect games.

While these tools require a deep understanding of Godot's scene model to work well for 2D pixel art, it is also a massive boon to the engine that they are individually simple and flexible and have relatively unsurprising behavior.

If these usability issues are inherent to the engine and will continue to exist forever, then a dedicated solution makes sense in my opinion.

They're inherent to any engine that uses real numbers, triangle rasterization and a scene graph to represent the rendered world. The linear algebra becomes less intuitive when you start needing to round numbers at specific points. There is inherent complexity introduced by being able to "attach" sprites to other sprites and apply linear transformations to compose the scene. You would experience these same sorts of issues in a hand-rolled engine if you were using hardware rendering too. In some ways 2D pixel art games are easier done with simple masked blitting against a framebuffer and a flat array of "objects" with update and draw callbacks (which I'll add, you absolutely can do in GDScript if you really wanted to).

I think the problems outlined here are ultimately solved by better learning material and a template to demonstrate how to do it correctly. It is absolutely possible to get Godot 4 to do 2D pixel art well without significant hurdles; I have not really experienced regressions relative to Godot 3 in this respect, even porting Godot 3 projects into 4. Maybe I could do a write-up on what is specifically needed and why certain issues occur when implementing it.

fractalcounty commented 1 year ago

I think the problems outlined here are ultimately solved by better learning material and a template to demonstrate how to do it correctly.

While I do completely agree with this, I still think the elephant in the room is the lost functionality from 4.0. It's not that making pixel perfect games are too hard, it's downright infeasible after a certain point as far as I can tell. There's just too many new issues that go far beyond this feature suggestion unfortunately.

HybridEidolon commented 1 year ago

Here is a project to serve as a test case: PixelPerfect.zip

  1. A SubViewport is created at runtime and arranged inside a SubViewportContainer and AspectContainer. The current scene is added to the SubViewport. The pixel-perfect configuration is set up on the SubViewport instead of the project, which consists of a forced resolution and both snapping settings enabled. This subviewport is at 320x180 (matching 16:9 320-width). This is all handled by an Autoload.
  2. None of the snapping settings are changed on the project itself.
  3. level.tscn contains a simple 2d platformer with a parallax background. No special snapping scripts are attached.
  4. A Label is added next to the AspectContainer to demonstrate full-viewport resolution rendering alongside the low-res sub viewport.
  5. This project is running in GL Compatibility renderer but is identical between all 3 renderers.

Notably, no scripts are needed to snap transforms on pixel alignment. This is a departure from approaches I've used for Godot 3. Everything became a lot simpler when I stopped trying to hack in my own solution!

For the most part, the behavior of the snapping is exactly as desired. The only thing that isn't correct is when the player begins moving for the first time, there are noticeable rounding issues between the camera and the player sprite, but as soon as the player's Y changes, this disappears entirely. I think that is probably a rounding issue in the canvas renderer. As far as I'm aware, this is basically identical to Godot 3.5, except that Godot 4 also grants us the ability to control the snapping settings on the SubViewport node rather than the entire project, which is a pretty significant improvement.

Hopefully this can serve as a useful basis to implement the smooth scrolling behavior described above; the actual Camera transforms aren't affected by scripts, so their global positions can be used in a shader on top of the viewport texture.

bitbrain commented 1 year ago

@HybridEidolon great solution right there! However, I was wondering how we could achieve "smooth" camera movement with this (e.g. camera following the player smoothly, with everything still pixel-perfect but the camera itself is not stuttering). A great example is Celeste: https://youtu.be/qyOapJgLcEI?t=997 they have pixel-perfect viewport (all the pixels always align on the screen) but the camera is smooth.

I tried to do it with the example you attached but the camera stutters once I enable position smoothing.

HybridEidolon commented 1 year ago

You need to make sure your camera is updating at the same frequency as the physics (i.e. switch the camera process callback to Physics)

malaVydra commented 1 year ago

Thank you for the suggestion. However, this doesn't really achieve the desired result. The camera still doesn't move smoothly like it does in the Celeste video shown above. In order for camera to be smooth it would need to move by sub-pixel amount units. This was easier to bypass by creating a Viewport with a shader that moves it on sub-pixel level while the camera loads normally.

However, although not the perfect solution, this was a lot easier to achieve in the earlier version, and I agree with everyone else here that this is something that would be really useful if made easier and more straight forward, as most of 2D indie games are pixel-art games.

HybridEidolon commented 1 year ago

The camera still doesn't move smoothly like it does in the Celeste video shown above. In order for camera to be smooth it would need to move by sub-pixel amount units.

The camera is not on subpixel alignment in Celeste. The camera appears smooth because the resolution is relatively high and the camera is much more complex than the built-in smoothing option and doesn't directly lag behind the player in most situations.

This was easier to bypass by creating a Viewport with a shader that moves it on sub-pixel level while the camera loads normally. ... this was a lot easier to achieve in the earlier version

This is still possible and there are no regressions preventing this. All you need to do is apply a shader to a quad using the SubViewportTexture and displace it by its UVs, with the SubViewport being a few pixels larger than the target resolution. Same as Godot 3.

lorenzo-arena commented 1 year ago

All you need to do is apply a shader to a quad using the SubViewportTexture and displace it by its UVs, with the SubViewport being a few pixels larger than the target resolution. Same as Godot 3.

Did anyone manage to get this working?

fractalcounty commented 1 year ago

Did anyone manage to get this working?

No, the subpixel method produces jittering and blur that isn't otherwise present in identical 3.x setups. I spent weeks testing and looking for a solution, but I haven't seen a single working implementation of it that plays nicely with viewports, character movement, and cameras in Godot 4.0.

On a slightly unrelated note, after a few more weeks of testing I've found that the issue here is that you can absolutely set up a blank project with a subviewport set to optimal settings and it will work as expected for a pixel perfect setup... up until you add any amount of complexity to your project. After awhile, the blur and jitter becomes so unmanageable that it often isn't worth dealing with anymore. There are just too many factors that induce it- whether that be user error, obscure bugs, or just the nature of your project. I heavily recommend avoiding the pixel perfect setup entirely if that's not something you want to deal with.

That's the big reason why this is a suggestion and not a bug report- there isn't a specific bug or regression responsible for this and it's not reproducible whatsoever due to sheer complexity. Otherwise, it would likely have been fixed years ago. Godot just kind of stinks when it comes to pixel perfect games at the moment and the workflow could really use some improvement.

Proggle commented 1 year ago

Godot just kind of stinks when it comes to pixel perfect games at the moment and the workflow could really use some improvement.

Yeah, the main blocker I have on adopting godot 4 is that it has become very difficult to make my game look the way I want it to, (and despite 2d sprite games in general being a very common use case, it doesn't feel like there are many people on the dev team who are focused on them.)

afk-mario commented 1 year ago

Great rundown on this subject if it's useful for someone to understand what's happening.

https://www.youtube.com/watch?v=Yu8k7a1hQuU

MitrB commented 1 year ago

On the topic of smooth camera: it can be achieved by having 2 cameras. One subviewport camera that is snapped to pixel coords. (Child of Subviewport) And a camera that will capture the full resolution. By giving the subviewport a border of 1 pixel, you can offset the full res camera by the decimal value of the subviewport camera on each update. Something like this setup:

- SubViewportContainer
- - SubViewport
- - - Scene
- - - - Player
- - - - - ViewportCamera
- FullResCamera

The Viewport Camera can be set to have position smoothing. You won't have any incremental movement now.

Script for camera offset:

    var viewport_camera = $"../SubViewportContainer/SubViewport/Player/ViewportCamera"
    var offset_x = fmod(viewport_camera.get_screen_center_position().x, 1.0)
    var offset_y = fmod(viewport_camera.get_screen_center_position().y, 1.0)

    self.offset = Vector2(offset_x, offset_y)
MitrB commented 1 year ago

I quickly threw together a prototype for what I explained in my comment above. I added camera panning too. One problem that I have is that for speeds that are not a multiple of 60, you can notice jitter on the character movement. I guess this could be fixed with some interpolation?

Also there is a staircasing effect when moving diagonally. This could be fixed by offsetting the sprite depending on velocity and previous offset.

GodotPixelPerfectSetup

Calinou commented 1 year ago

One problem that I have is that for speeds that are not a multiple of 60, you can notice jitter on the character movement. I guess this could be fixed with some interpolation?

Indeed, you need to use physics interpolation with https://github.com/lawnjelly/smoothing-addon/tree/4.x.

MitrB commented 1 year ago

@Calinou

Indeed, you need to use physics interpolation with https://github.com/lawnjelly/smoothing-addon/tree/4.x.

I think it doesn't work that well with a pixel perfect setup. As you can see in the recording, the red sprite is being smoothed. But it jitters around.

https://user-images.githubusercontent.com/43727673/233953127-3844e7f2-dd59-469e-adb6-9e4bbb208058.mp4

MitrB commented 1 year ago

I have shitty hardware, so the recording is choppy. Either way, this is how to achieve smooth camera movement. Feel free to check out the setup yourself: Github Repository

Jittery movement still needs to be fixed. I noticed that speeds that are a multiple of 60 produce smooth vertical and horizontal movement. Diagonal movement is jittery.

https://user-images.githubusercontent.com/43727673/234346425-1455624c-18fd-41a6-a206-6fe5d79f100d.mp4

bitbrain commented 1 year ago

Reading through the comments I believe there are two different use cases here that we need to be mindful of not to mix up:

  1. some people want to build pixelart games where sprites move smoothly between pixels (subpixel blending). For this usecase, a shader like this can be helpful
  2. some people want to build pixelart games that are 'pixel perfect', meaning that all pixels on the screen have the exact same size and are aligned perfectly in a raster. This can be achieved in Godot by using a SubViewport

My understanding is that this proposal is supposed to solve 2. via a new node "out of the box" while it does not try to also solve 1.

Exerionius commented 1 year ago

I'm pretty sure people just want this:

How to make a silky smooth camera for pixelart games in Godot

Pixel-perfect game + high-res UI + smooth camera. So it's all at once. This video perfectly explains how to achieve this in 3.5

jordanlis commented 1 year ago

Reading through the comments I believe there are two different use cases here that we need to be mindful of not to mix up:

1. some people want to build pixelart games where sprites move smoothly between pixels (subpixel blending). For this usecase, [a shader like this](https://www.shadertoy.com/view/MlB3D3) can be helpful

2. some people want to build pixelart games that are 'pixel perfect', meaning that all pixels on the screen have the exact same size and are aligned perfectly in a raster. This can be achieved in Godot by using a `SubViewport`

My understanding is that this proposal is supposed to solve 2. via a new node "out of the box" while it does not try to also solve 1.

I'm not sure I fully agree with your summary.

For me, people just want smooth camera, no matter if the game is in pixel art or not. Pixel art is not really the subject according to me. The subject is whether the game is downscaled or not, and whether people want camera movement that is not related to the base resolution of the game or not.

This doesn't mean a new node, just a simple option "Real smooth camera" in the current camera node. By simple, I doesn't mean simple in the code, but simple in the solution for the user.

I don't think it can be easily done by script, but it appears to be possible with script and node hierarchy after some amount of time.

h0lley commented 1 year ago

Trying to achieve smooth camera panning without any jittering of game entities in my pixel art topdown game that has a base resolution of 640x360 has perhaps been one of the greatest (or rather most time consuming and annoying) challenges so far, and it's still not exactly how I'd like it be. I strongly feel that users should not have to struggle to this extend to get this common need dealt with, and it certainly shouldn't require anything like nested viewports and custom camera shaders.

Norrox commented 1 year ago

Does this not work for everyone? Or am I missing something? I'm all for better and simpler solutions for everyone but this is achievable without code :)

https://github.com/godotengine/godot-proposals/assets/13393087/d275f622-e354-452c-b54d-fd9dc14fbfcc

golddotasksquestions commented 1 year ago

@Norrox The latter, you are missing something. See title: "Simplify smooth camera movement in pixel perfect games."

jordanlis commented 1 year ago

I'm pretty sure people just want this:

How to make a silky smooth camera for pixelart games in Godot

Pixel-perfect game + high-res UI + smooth camera. So it's all at once. This video perfectly explains how to achieve this in 3.5

For information, this is not working anymore on godot 4. Don't know why, but I adapted the project on my side and it doesn't work. On the other side it was working perfectly fine in godot 3.5.2.

greycheeked commented 1 year ago

I'm pretty sure people just want this: How to make a silky smooth camera for pixelart games in Godot Pixel-perfect game + high-res UI + smooth camera. So it's all at once. This video perfectly explains how to achieve this in 3.5

For information, this is not working anymore on godot 4. Don't know why, but I adapted the project on my side and it doesn't work. On the other side it was working perfectly fine in godot 3.5.2.

My camera script is also based on the "Silky Smooth" concept. In the meantime it has changed significantly, but I tried to make a version of it that is as similar as possible to the "Silky Smooth" script. I haven't tested it in this version, but I hope it's helpful. Especially the "align()" in the last line stopped the jerking for me under Godot 4.

extends Camera2D

@export var player: Node

@onready var game_size := Vector2(1280.0, 720.0)
var window_scale: float

@onready var actual_cam_pos := global_position

func _ready():
    var gamewin_to_vp = get_viewport_rect().size / game_size
    window_scale = min(gamewin_to_vp.x, gamewin_to_vp.y)

func _process(delta):
    var mouse_pos: Vector2 = get_viewport().get_mouse_position()
    mouse_pos -= get_viewport_rect().size * 0.5
    mouse_pos /= window_scale
    mouse_pos += player.global_position

    actual_cam_pos = lerp(actual_cam_pos, mouse_pos, 5 * delta)

    var actual_cam_pos_vp: Vector2 = get_viewport_transform() * actual_cam_pos

    var rounded_cam_pos_vp: Vector2 = actual_cam_pos_vp.round()
    var cam_subpixel_pos: Vector2 = rounded_cam_pos_vp - actual_cam_pos_vp
    _global.viewport_container.material.set_shader_parameter("cam_offset", cam_subpixel_pos)

    global_position = get_viewport_transform().affine_inverse() * rounded_cam_pos_vp
    align()
paxer commented 11 months ago

Interestingly enough, when I worked on the prototype of our pixel-art game, as a programmer, I did not even notice the issue, however, when I showed the prototype to our pixel artist - he immediately spotted jittering and responded he would not tolerate it for the released game until it is fixed... and yes, I've tried all possible solutions available for Godot 4 to solve this problem - nothing works like our artist wants, and how it should be, e.g. jitter-free, pixel-perfect, crisp clear.

I wish Godot had an option to donate to fix some specific issue so I can vote with my dollar. Otherwise, when I donate to the general fund - there is no guarantee that some contributor with the knowledge of how to fix this issue would be interested in working on it. Godot is trying to improve in 3D, which is understandable, yet this core for 2D pixel-art games issue was not properly fixed in 3x, and now it is even more an issue for 4x, and who knows when it will become a priority...

wilfredjonathanjames commented 11 months ago

Bug bounties have been successful for other FOSS projects. They may be worth considering for Godot.

Gnumaru commented 11 months ago

I wish Godot had an option to donate to fix some specific issue so I can vote with my dollar

Since this is open source, you can set up a "bounty" for fixing this and publish it wherever, like on social media, discord, forums etc. Even though integrating a changing in mainstream needs consensus and approval, actually changing anything in whichever way does not. So the person that accepts the bounty makes a fork of the project (except if he does it as a gdextension or something like this) and fixes the problem in a manner acceptable to the bounty offeror and that's it. Even if the fix never get's integrated in mainstream, the bounty offeror can continue using that fork forever, or keep rebasing it to remain in sync with mainstream.

bitbrain commented 11 months ago

Reminder to keep the discussion on the topic of this proposal which is about creating a better camera system for pixel perfect games.

PPillau commented 11 months ago

I wish Godot had an option to donate to fix some specific issue so I can vote with my dollar

Since this is open source, you can set up a "bounty" for fixing this and publish it wherever, like on social media, discord, forums etc. Even though integrating a changing in mainstream needs consensus and approval, actually changing anything in whichever way does not. So the person that accepts the bounty makes a fork of the project (except if he does it as a gdextension or something like this) and fixes the problem in a manner acceptable to the bounty offeror and that's it. Even if the fix never get's integrated in mainstream, the bounty offeror can continue using that fork forever, or keep rebasing it to remain in sync with mainstream.

What does „consensus“ actually mean in the context of this project? Is there a certain amount of upvotes that a solution/comment must get? Do a certain number of project maintainers need to agree with the proposal? Can the community overrule/outvote an official project maintainer? How long is the waiting time until „consensus“ is formed?

It all just seems kinda arbitrary…

paxer commented 11 months ago

So, back to the topic, without a proper pixel-perfect 2D camera node to solve this problem, it means jitter-free, crispy clear, pixel-perfect scrolling - we can't say that Godot 4 is production-ready for pixel art games, at least pixel art games with the moving camera because every possible workaround described here is a sacrificing of quality in one way or another.

On top of that, no physics interpolation has been added yet (causing another jitter issue on screens with higher than 60Hz refresh rate), but at least it is hopefully coming after it is mered to 3.6.. still not guaranteed of course https://www.reddit.com/r/godot/comments/11gfoy0/comment/jb552aq/

It's hard to commit a team to work on a pixel art game in Godot 4 knowing it has this critical problem. I personally would be happy to continue working with Godot 3 on 2D games. However, the Switch export was announced only for Godot 4.x, which is an important reason not to consider Godot 3, at least for our team.

I love Godot, especially 4.x, but the lack of proper pixel-art camera support is really a deal breaker for many developers to commit to the development of new production-quality pixel-art projects with it.

fractalcounty commented 11 months ago

and yes, I've tried all possible solutions available for Godot 4 to solve this problem - nothing works like our artist wants, and how it should be, e.g. jitter-free, pixel-perfect, crisp clear.

It's a huge bummer returning to this proposal for the first time in 7 months and seeing that this is still an issue.

At risk of sounding dramatic, not being able to achieve something as basic as getting sprites to reliably render correctly effectively killed all motivation I had for immersing myself in Godot as a beginner, and I haven't really touched the engine since- which is such a shame because I loved absolutely everything about it.

I understand that this isn't a trivial problem to solve and that most engines have their unique issues when it comes to pixel perfect workflows. And looking back at my proposal, I can definitely see potential issues with a one-size-fits-all pixel camera node. Gauging the feasibility of something like that is well above my skill level. However, at the very least, some reliable method of working with low-res sprites is incredibly important for an engine like Godot. From what I can gather, it doesn't really seem like that's the case yet.

Hopefully more attention is drawn to this as more users inevitably run into this roadblock.

neilfranci commented 11 months ago

My camera script is also based on the "Silky Smooth" concept. In the meantime it has changed significantly, but I tried to make a version of it that is as similar as possible to the "Silky Smooth" script. I haven't tested it in this version, but I hope it's helpful. Especially the "align()" in the last line stopped the jerking for me under Godot 4.

extends Camera2D

@export var player: Node

@onready var game_size := Vector2(1280.0, 720.0)
var window_scale: float

@onready var actual_cam_pos := global_position

func _ready():
  var gamewin_to_vp = get_viewport_rect().size / game_size
  window_scale = min(gamewin_to_vp.x, gamewin_to_vp.y)

func _process(delta):
  var mouse_pos: Vector2 = get_viewport().get_mouse_position()
  mouse_pos -= get_viewport_rect().size * 0.5
  mouse_pos /= window_scale
  mouse_pos += player.global_position

  actual_cam_pos = lerp(actual_cam_pos, mouse_pos, 5 * delta)

  var actual_cam_pos_vp: Vector2 = get_viewport_transform() * actual_cam_pos

  var rounded_cam_pos_vp: Vector2 = actual_cam_pos_vp.round()
  var cam_subpixel_pos: Vector2 = rounded_cam_pos_vp - actual_cam_pos_vp
  _global.viewport_container.material.set_shader_parameter("cam_offset", cam_subpixel_pos)

  global_position = get_viewport_transform().affine_inverse() * rounded_cam_pos_vp
  align()

Has anybody tried this? Mine is messing up everything, just blank scenes. @greycheeked do you still have the project file? Tks!

orangelimester commented 11 months ago

extends Camera2D

@export var player: Node

@onready var game_size := Vector2(1280.0, 720.0) var window_scale: float

@onready var actual_cam_pos := global_position

func _ready(): var gamewin_to_vp = get_viewport_rect().size / game_size window_scale = min(gamewin_to_vp.x, gamewin_to_vp.y)

func _process(delta): var mouse_pos: Vector2 = get_viewport().get_mouse_position() mouse_pos -= get_viewport_rect().size * 0.5 mouse_pos /= window_scale mouse_pos += player.global_position

actual_cam_pos = lerp(actual_cam_pos, mouse_pos, 5 * delta)

var actual_cam_pos_vp: Vector2 = get_viewport_transform() * actual_cam_pos

var rounded_cam_pos_vp: Vector2 = actual_cam_pos_vp.round() var cam_subpixel_pos: Vector2 = rounded_cam_pos_vp - actual_cam_pos_vp _global.viewport_container.material.set_shader_parameter("cam_offset", cam_subpixel_pos)

global_position = get_viewport_transform().affine_inverse() * rounded_cam_pos_vp align()

I'm getting: Identifier "_global" not declared in the current scope. Does anyone know how to fix that?

greycheeked commented 11 months ago

I now went back to pictster's YT channel and downloaded the project from his GitHub page. After the conversion to Godot 4 I added the script listed above. Then I had to make some adjustments and now it looks like this:

https://github.com/godotengine/godot-proposals/assets/92092511/0d04965b-3244-49df-ba22-441733d6a471

Whether that is good enough is for everyone to judge for themselves. In the second half, you can see a problem with the player character: some pixels disappear at times. How exactly this comes about, I don't know. How to fix it, I would like to know myself. As for the settings I had to change, I ask for some patience. I'll have to take another look at what exactly I changed and get back to you.

greycheeked commented 11 months ago

After running "convert full project", open the project and make the following changes:

Project Settings

SubViewport

Camera2D (in Scene.tscn)

camera_script.gd

neilfranci commented 11 months ago

@greycheeked tks! But yeah, it jiggles very weird, my eye can still see it.

Gnumaru commented 11 months ago

@greycheeked , could you please attach a zip file with the project here? I know you already mentioned you downloaded the project from pictster's github, converted it form 3 to 4 and did all the changes you already mentioned. But sharing the converted project makes it even easier for others to test it out.

neilfranci commented 11 months ago

Godot-Tutorial-SmoothCamera.zip @Gnumaru, here you are

Calinou commented 11 months ago

Regarding the video shown in OP, has someone managed to recreate it in Godot 3.x or another engine in a smooth pixel-perfect way? (This is mostly to check for regressions in how 4.x renders things.)

@nullcreek would need to upload the project files somewhere though.

fractalcounty commented 11 months ago

@nullcreek would need to upload the project files somewhere though.

The project was originally created in 3.x using the subpixel method outlined in pixster's video. I then remade it from scratch and couldn't get it to work in 4.x whatsoever without some combination of jitter or sprite distortion. The sprite distortion is especially problematic and noticeable with larger sprites with outlines that are sensitive to distortion.

Unfortunately, I don't have access to either of the original projects as I suffered data loss from a hard drive failure and poor version management practices. Some of it is still recoverable from my repository, so I'll try to get some work done recovering it.

greycheeked commented 11 months ago

@neilfranci Thanks for providing the zip-file

You can still improve the granularity of the camera movement. At least that is my impression. In the project, the viewport is set to 640 x 360, and the initial window size is set to double (1280 x 720). If you directly double the viewport size and increase the SubViewport scale from 8 to 16 (position also set to -16, -16), then the camera moves at pixel resolution; I think previously it moved at 2-pixel resolution.

greycheeked commented 11 months ago

There is one more improvement that concerns the temporary disappearance of some pixels on the player sprite (is this called pixel distortion?). If you activate "Snap 2D Vertices to Pixel" in the subViewport, this is gone!

Then I tried to find out the really necessary changes to picster's original script. With these changes I would like to present it here again. My own script still contains too many things that are unnecessary here.

extends Camera2D

@export var player: Node

@onready var game_size := Vector2(80, 45)
@onready var window_scale := (Vector2(get_window().size) / game_size).x

@onready var actual_cam_pos := global_position

func _process(delta):
    # First we get the "real" position of the mouse cursor and then the offset to the player
    # To do that, we need to divide the mouse position inside the local viewport by the game window scale
    # Then substract half of the game size and add the player position
    var mouse_pos = DisplayServer.mouse_get_position() - get_window().position
    mouse_pos = mouse_pos / window_scale - (game_size/2) + player.global_position

    # Using a lerp, the cameras position is moved towards the mouse position
    var cam_pos = lerp( player.global_position, mouse_pos, 0.7)

    # Use another lerp to make the movement smooth
    actual_cam_pos = lerp(actual_cam_pos, cam_pos, 5*delta)

    # Calculate the "subpixel" position of the new camera position
    var cam_subpixel_pos = actual_cam_pos.round() - actual_cam_pos

    # Update the Main ViewportContainer's shader uniform
    _global.viewport_container.material.set_shader_parameter("cam_offset", cam_subpixel_pos )

    # Set the camera's position to the new position and round it.
    global_position = actual_cam_pos.round()
    align()
Exerionius commented 11 months ago

There is one more improvement that concerns the temporary disappearance of some pixels on the player sprite (is this called pixel distortion?). If you activate "Snap 2D Vertices to Pixel" in the subViewport, this is gone!

Then I tried to find out the really necessary changes to picster's original script. With these changes I would like to present it here again. My own script still contains too many things that are unnecessary here.

extends Camera2D

@export var player: Node

@onready var game_size := Vector2(80, 45)
@onready var window_scale := (Vector2(get_window().size) / game_size).x

@onready var actual_cam_pos := global_position

func _process(delta):
  # First we get the "real" position of the mouse cursor and then the offset to the player
  # To do that, we need to divide the mouse position inside the local viewport by the game window scale
  # Then substract half of the game size and add the player position
  var mouse_pos = DisplayServer.mouse_get_position() - get_window().position
  mouse_pos = mouse_pos / window_scale - (game_size/2) + player.global_position

  # Using a lerp, the cameras position is moved towards the mouse position
  var cam_pos = lerp( player.global_position, mouse_pos, 0.7)

  # Use another lerp to make the movement smooth
  actual_cam_pos = lerp(actual_cam_pos, cam_pos, 5*delta)

  # Calculate the "subpixel" position of the new camera position
  var cam_subpixel_pos = actual_cam_pos.round() - actual_cam_pos

  # Update the Main ViewportContainer's shader uniform
  _global.viewport_container.material.set_shader_parameter("cam_offset", cam_subpixel_pos )

  # Set the camera's position to the new position and round it.
  global_position = actual_cam_pos.round()
  align()

Still unclear what _global is.