godotengine / godot

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

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

Closed securas closed 3 years ago

securas commented 3 years ago

Godot version: 3.2.4 RC3

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

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

Other relevant settings: image

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

Steps to reproduce: run project.

Minimal reproduction project:

test_character_positioning.zip

lawnjelly commented 3 years ago

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

Ok I'll take a look and run the numbers! :+1:

lawnjelly commented 3 years ago

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

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

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

pixel_gap

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

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

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

securas commented 3 years ago

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

lawnjelly commented 3 years ago

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

akien-mga commented 3 years ago

Fixed by #43813.

lawnjelly commented 3 years ago

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

2D stretch mode

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

viewport stretch mode

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

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

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

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

securas commented 3 years ago

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

lawnjelly commented 3 years ago

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

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

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

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

securas commented 3 years ago

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

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

And here even worse with smoothing platformer_positioning_parallax_smoothed

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

securas commented 3 years ago

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

lawnjelly commented 3 years ago

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

securas commented 3 years ago

Looking forward!

On Tue, Mar 2, 2021 at 9:09 PM lawnjelly notifications@github.com wrote:

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

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/godotengine/godot/issues/46504#issuecomment-788863188, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAV7RW66NVZYX5WWD5OCAO3TBTIP7ANCNFSM4YK3NY5A .

lawnjelly commented 3 years ago

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

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

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

securas commented 3 years ago
  • It works well with the parallax background

Thanks! I'll give it another go.

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

lawnjelly commented 3 years ago

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

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

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

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

securas commented 3 years ago

I'm afraid placing barriers has never yielded happy users. Documentation is the way to go IMHO. I have to say that it is a bit disappointing to read that you have essentially given up on the viewport mode. Godot will then remain a not so friendly pixel art engine. And it seems that nobody else is caring too much about it as well. So the current status of thing is likely to remain so for a long time.

On Wed, Mar 3, 2021, 15:22 lawnjelly notifications@github.com wrote:

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

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

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

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

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/godotengine/godot/issues/46504#issuecomment-789470268, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAV7RW7CSUP6LQUALCOPJ3TTBXITJANCNFSM4YK3NY5A .

lawnjelly commented 3 years ago

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

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

securas commented 3 years ago

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

lawnjelly commented 3 years ago

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

securas commented 3 years ago

I'm off to bed now... I'll test it tomorrow 😜

On Thu, Mar 4, 2021, 01:37 lawnjelly notifications@github.com wrote:

It's #46615 https://github.com/godotengine/godot/pull/46615. But you may have to wait a few mins for it to pass CI, before the builds can be downloaded.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/godotengine/godot/issues/46504#issuecomment-789854880, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAV7RW7LC7EZUUT2CC2U5CLTBZQULANCNFSM4YK3NY5A .

lawnjelly commented 3 years ago

I'm getting there with doing this in the addon. The jiggle of objects relative to the background was easily solved by snapping to the background (which was pretty much my first suggestion on the subjects, see here: https://github.com/godotengine/godot/issues/35606#issuecomment-578693701 ).

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

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

https://user-images.githubusercontent.com/21999379/110207167-9ae87400-7e79-11eb-92c9-860fb1f74937.mp4

securas commented 3 years ago

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

lawnjelly commented 3 years ago

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

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

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

lawnjelly commented 3 years ago

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

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

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

There are a number of issues with this though:

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

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

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

securas commented 3 years ago

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

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

lawnjelly commented 3 years ago

That looks like an interesting idea. :+1:

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

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

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

securas commented 3 years ago

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