microsoft / DirectXTK

The DirectX Tool Kit (aka DirectXTK) is a collection of helper classes for writing DirectX 11.x code in C++
https://walbourn.github.io/directxtk/
MIT License
2.55k stars 506 forks source link

Sprite position issue #355

Closed star69rem closed 1 year ago

star69rem commented 1 year ago

If a sprite's position happens to fall exactly halfway between pixels (ie. a position of something like 0.5, 1.5, 2.5, etc.), the sprite graphics get distorted. I think internally sprite batch should just make a decision about rounding these. It's already rounding down when you set positions like 1.4 and rounding up when you set positions like 1.6 or 1.51f;

star69rem commented 1 year ago

Position = 36.51f:

image

Position = 36.50f;

image

shawnhar commented 1 year ago

I think you're just seeing the nature of how texture filtering works on GPUs, rather than anything specific to DirectXTK. From these images it looks like you are using point sampling, which tells the GPU to round each sample location to the nearest integer. If you are drawing exactly halfway between integer positions, which way the hardware rounds will be determined by the low bits of interpolator positions, which may not round in the same direction for every pixel.

Solution: use bilinear sampling in place of point if you want the GPU to filter your images, or don't draw them at fractional locations if you want clearly defined pixel snapping.

star69rem commented 1 year ago

I think you're just seeing the nature of how texture filtering works on GPUs, rather than anything specific to DirectXTK. From these images it looks like you are using point sampling, which tells the GPU to round each sample location to the nearest integer. If you are drawing exactly halfway between integer positions, which way the hardware rounds will be determined by the low bits of interpolator positions, which may not round in the same direction for every pixel.

Solution: use bilinear sampling in place of point if you want the GPU to filter your images, or don't draw them at fractional locations if you want clearly defined pixel snapping.

If that's the case, why does it look like only the bottom right triangle the sprite's made of distorted?

And the behavior seems inconsistent to me. Why isn't 1.51f broken?

star69rem commented 1 year ago

I'm having to put a hack in my game right now where if I find that the position is >= 0.5 and < 0.51, I add 0.01f to it just to get it to round up. Really sucks and doesn't feel like the correct default behavior to me. It's weird for fractional positions to magically work themselves out other than this range.

walbourn commented 1 year ago

Maybe it's because Direct3D 11 considers '0.5' to be the pixel center per Microsoft Docs?

star69rem commented 1 year ago

@shawnhar and FYI linear filtering on the sprite batch doesn't fix it. It just blurs the distorted graphics. This is a bug.

star69rem commented 1 year ago

This is with linear clamp: image

image

star69rem commented 1 year ago

I get that Direct3D wasn't designed for this, but it is "SpriteBatch," and this is a basic scenario. Spritebatch should never draw distorted sprites.

shawnhar commented 1 year ago

I'm having to put a hack in my game right now where if I find that the position is >= 0.5 and < 0.51, I add 0.01f to it just to get it to round up.

You are overthinking this. If you want pixel art drawn at precise pixel locations without any filtering, there's no need to check for specific numeric ranges, just round all your coordinates to the nearest integer.

If you specify point sampling plus draw exactly halfway between pixels, results will depend on the vagaries of numeric precision, which differ across GPUs as well as across a triangle on a single GPU. So don't do that! This isn't specific to Direct3D or SpriteBatch, it's just an inherent characteristic of how GPUs work.

star69rem commented 1 year ago

I'm having to put a hack in my game right now where if I find that the position is >= 0.5 and < 0.51, I add 0.01f to it just to get it to round up.

You are overthinking this. If you want pixel art drawn at precise pixel locations without any filtering, there's no need to check for specific numeric ranges, just round all your coordinates to the nearest integer.

If you specify point sampling plus draw exactly halfway between pixels, results will depend on the vagaries of numeric precision, which differ across GPUs as well as across a triangle on a single GPU. So don't do that! This isn't specific to Direct3D or SpriteBatch, it's just an inherent characteristic of how GPUs work.

You can't just round positions in a native resolution pixel art game BECAUSE of the fact that internally it already rounds SOME numbers but not others, and because every computer convention in the world treats >= .5 as rounding up OTHER than this one, you have rounding conflicts that make sprites subtly jitter and not move as smoothly as the convoluted workaround I described. It BLOWS.

I'm not overthinking it, because the game has more complicated scenarios than I've gone into here and there's a reason it is the way it is, but if you're not going to do anything about this obvious bug, go ahead and close it I guess.

star69rem commented 1 year ago

For posterity's sake, this animated GIF shows just how blatant this bug is. https://imgur.com/a4Xm08m

walbourn commented 1 year ago

What is happening in your code that results in the animation you provided?

I'm trying to understand where you believe the rounding should occur here, but have you tried modifying the SpriteBatch yourself to add rounding to see if it resolves the issue?

star69rem commented 1 year ago

What is happening in your code that results in the animation you provided?

I'm trying to understand where you believe the rounding should occur here, but have you tried modifying the SpriteBatch yourself to add rounding to see if it resolves the issue?

For that example I just manually set the sprite position to 36.5f, but you'll get sprite distortion on any value from around x.499f to x.51f. If I have a 1:1 framebuffer and set a position, I would expect the rounding to happen like any normal computer function where >= .5 gets rounded up. It already rounds up a value of 35.6f and rounds down a value of 35.4f. The behavior is inconsistent.

walbourn commented 1 year ago

Can you check which version of DirectXMath you are using? It's in the DirectXMath.h header as #define DIRECTX_MATH_VERSION?

walbourn commented 1 year ago

There's no extra logic at all in the SpriteBatch implementation for pixel centers. It simply renders the exact positions you provide it transformed by the viewport matrix. As such, Direct3D 11 and Direct3D 12 consider the "0.5, 0.5" location to be the "pixel center" for 'nearest pixel' sampling, so it does not move the sampled pixel until the location is PAST the center to something like "0.6".

walbourn commented 1 year ago

You could do something like:

XMVECTOR pos = XMLoadFloat2(&m_position);

static const XMVECTORF32 s_half = { { { 0.5f, 0.5f, 0.f, 0.f } } };

pos = XMVectorAdd(XMVectorRound(pos), s_half);

m_spriteBatch->( ..., pos, ...)

XMVectorRound is equivalent to SSE4 round and ARM-NEON vector round which does "Rounding half to even" or "Banker's rounding". There really isn't "any normal computer function" as you state it for rounding. Instead, you specifically expected "Rounding half away from zero" which is actually quite hard to achieve with SIMD math due to the dependance on the hardware rounding mode (which again is normally set to "Banker's rounding").

See Wikipedia

You would probably be happier with:

XMVECTOR pos = XMLoadFloat2(&m_position);

static const XMVECTORF32 s_half = { { { 0.5f, 0.5f, 0.f, 0.f } } };

pos = XMVectorAdd(XMVectorTruncate(pos), s_half);

m_spriteBatch->( ..., pos, ...)

Which at least make sures that ".0" values land exactly on the pixel center, and then it moves when you hit the next whole number.

walbourn commented 1 year ago

Updated the doc pages in the wiki to note this for future reference:

https://github.com/microsoft/DirectXTK/wiki/SpriteBatch#pixel-centers

https://github.com/microsoft/DirectXTK12/wiki/SpriteBatch#pixel-centers