17cupsofcoffee / tetra

🎮 A simple 2D game framework written in Rust
MIT License
910 stars 63 forks source link

Sprites sliced from sheets sometimes displays off by 1 pixel #197

Closed taotao54321 closed 4 years ago

taotao54321 commented 4 years ago

Summary: I'm managing my sprites with spritesheets. I slice sprites from sheets with DrawParams::clip(), and draw them on a position with DrawParams::position(). But, when I let the sprites move around, I noticed that they sometimes displays off by 1 pixel.

Steps to reproduce: I wrote a minimal case, and here is the screenshot:

issue

I found this seems to be caused by passing DrawParams::position() some real numbers.

Expected behavior: I expected position() does not affect sprite clipping, but actually it seems do. Is it a intended feature? If so, how can I ensure that the sprites are clipped pixel-perfectly?

Additional info: system info: Debian testing, nvidia-driver 440.100-2

17cupsofcoffee commented 4 years ago

Thank you for the report!

This seems to only happen specifically when drawing at (or very close to) .5 of a pixel - if you change the Y position to 39.49 or 39.51, the issue disappears. It also only seems to happen on some devices - someone on Discord said that your example works fine for them as is on an AMD graphics card.

I'm not 100% sure why this is happening, but it doesn't seem to be a Tetra-specific issue - I'm able to replicate it in Love2D as well, so it's probably something to do with OpenGL. My hunch is that it's due to floating point error in the texture sampling, but I don't know for sure.

In general, though, rendering at non-integer co-ordinates can cause artifacts in OpenGL, even without this weird platform-specific behaviour. Quoting the Love2D docs:

Quads 'bleed' when scaled, rotated or drawn at non-integer coordinates, even within SpriteBatches, to compensate for this, use 1px borders around the textures inside the texture atlas (preferably with the same colors as the actual border)

So if rendering at non-integer co-ordinates is something you need to do, that would probably be the best fix. Otherwise, just draw at integer co-ordinates and you shouldn't see these issues.

Other things that could be helpful:

taotao54321 commented 4 years ago

Thank you! I didn't know much about OpenGL and studied a good thing this time. I fixed my drawing code to call Vec2::floor(), and it works all fine for my purpose.

Just FYI: actually my code was drawing also on Canvas, and still I had the same problem. So, this behavior also seems graphics-card dependent (I'm using GeForce GTX 650).

17cupsofcoffee commented 4 years ago

Ah, the Canvas thing might have been a fix for a different issue, then - might have to dig deeper into that.

While I don't think this is a 'bug', so to speak, there is definitely a documentation gap here, so I will leave this issue open to track improvements on that front.

puppetmaster- commented 4 years ago

This is what it looks like when the camera position is a non-integer . Landscape and player graphics are from a single spritesheet. camera_with_tileset Flooring the camera position will fix it.

17cupsofcoffee commented 4 years ago

Aha, yeah, that demonstrates the problem really well - this definitely needs documenting better. Where would you expect to find a warning about this - on the clip parameter of DrawParams? Or is there a better place for it...

As an aside, those tiles/sprites are extremely cute 😄

puppetmaster- commented 4 years ago

hmm... good question... Shouldn't this be handled internally?

There are several places where it is hidden and only if you know the problem you know that you have to floor the position to avoid the problem.

places

The only place you will definitely see is if you create a DrawParams with clip.

17cupsofcoffee commented 4 years ago

The problem I have with trying to handle this internally is that, as far as I know, there's not a one-size-fits-all approach - while flooring positions is probably the best/easiest way to handle this in a pixel art game, there's other approaches you might want to take if you were doing something more high resolution (for example, extending the edges of the texture out by 1 pixel and switching to FilterMode::Linear). I'm wary of taking that choice away from the user unless I know I'm making the right choice.

For context, most other frameworks seem to leave this up to the user as well (XNA, Love2D, LibGDX).

17cupsofcoffee commented 4 years ago

I've added a note to the docs:

image

I've also changed Texture to use CLAMP_TO_EDGE wrapping, so you should at least not see issues with stuff bleeding from one side of a texture atlas to the other.

Does this seem sufficient?

taotao54321 commented 4 years ago

I think this doc is helpful to newbies like me. Thank you!

17cupsofcoffee commented 4 years ago

Cool! I'll leave this issue open until 0.4.2 is published, at which point the new docs should be available on docs.rs.

17cupsofcoffee commented 4 years ago

0.4.2 is released! Docs should update in the next few minutes, depending on how busy the docs.rs queue is.