Incosistent rasterization when rotating

Although it only needs to work up to the native resolution of 640x360 for me. Seems improbable that I would encounter issues with that with such tiny inaccuracies (small floats). Otherwise, it seems drawing at a lower resolution, the only option to avoid this issue is to draw a rotation to a render target in the same position first and then draw that to the screen.

When you rotate though, donā€™t you introduce floating point precision? To my knowledge, under the hood a sprite batch is just a textured quad, so unless you pre-rotate the texture and then draw, you could conceivably have a different final position, even at integer coordinates.

When I did these tests before, things usually looked fine, except when it didnā€™t. The only reason I had a position where it didnā€™t look fine is because we had a customer ticket with geometry that reproduced the problem (for that particular issue).

Well even if you donā€™t rotate you still have to deal with floats, but yeah, the vertices may not be positioned exactly the same way, but my hope was that the inaccuracies were so small it would not make a noticeable difference. It might help if I snapped to for example 1-degree steps in rotation.

A rare edge case of the arrow being rotated in exactly x degrees at exactly position y on the screen making it appear a bit different than when at other positions on the screen, is probably not the end of the world for me.

I think thatā€™s the problem though, you canā€™t know. Since the end result of the error is an errant pixel, it really just depends on what the resulting float value ends up being as it goes through rasterization.

Itā€™s weird to me that the scale up approach didnā€™t work though, I would have expected it to. I have a bit of time until someone gets back to me, let me see if I can whip up a quick test.

I scaled up the arrow texture 5x and used anisotropic sampling (also tried linear), otherwise, I used the code I posted originally. I did not change the resolution.

Ok, so I whipped up an example. Left hand side is the arrow rotated at native and the right hand side is the arrow scaled up to 4x resolution, then rotated with anisotropic.

At one degreeā€¦
image

At 45(ish) degreesā€¦
image

This is at a fixed position, but my intent was to demonstrate that the artifacts arenā€™t noticeable in the upscaled version, so even if you change the position, youā€™re not going to get the weird result youā€™re seeing.

Hereā€™s the code for thisā€¦ you should just be able to dump this into a new MonoGame project, but youā€™ll need to make sure arrow.png and debug.spritefont exist in your content. Click and drag left/right to change the rotation of the arrow. Right-click to reset the arrow rotation.

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using System;

namespace Game1
{
    public class Game1 : Game
    {
        private GraphicsDeviceManager _graphics;
        private SpriteBatch _spriteBatch;

        private SpriteFont _debugFont;
        private RenderTarget2D _target;
        private Texture2D _arrow;
        private Texture2D _arrowScaled;

        private float _arrowRotation = 0.0f;
        
        private MouseState _oldMouseState;
        private float? _mouseClickX;
        private float _startRotation;
        
        public Game1()
        {
            _graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";

            this.IsMouseVisible = true;
        }

        protected override void Initialize()
        {
            _graphics.PreferredBackBufferWidth = 1280;
            _graphics.PreferredBackBufferHeight = 720;
            _graphics.ApplyChanges();

            base.Initialize();
        }

        protected override void LoadContent()
        {
            _spriteBatch = new SpriteBatch(GraphicsDevice);

            _debugFont = this.Content.Load<SpriteFont>("debug");

            _arrow = this.Content.Load<Texture2D>("arrow");
            _target = new RenderTarget2D(this.GraphicsDevice, 320, 180);

            RenderTarget2D scaleTarget = new RenderTarget2D(this.GraphicsDevice, _arrow.Width * 4, _arrow.Height * 4);
            this.GraphicsDevice.SetRenderTarget(scaleTarget);
            this.GraphicsDevice.Clear(Color.Transparent);
            _spriteBatch.Begin(samplerState: SamplerState.PointClamp);
            _spriteBatch.Draw(_arrow, new Rectangle(0, 0, scaleTarget.Width, scaleTarget.Height), null, Color.White);
            _spriteBatch.End();
            this.GraphicsDevice.SetRenderTarget(null);
            _arrowScaled = scaleTarget;
        }
        protected override void Update(GameTime gameTime)
        {
            MouseState mouseState = Mouse.GetState();

            if (Keyboard.GetState().IsKeyDown(Keys.Escape))
                Exit();

            // Check if mouse rotation.
            if (mouseState.LeftButton == ButtonState.Pressed && _oldMouseState.LeftButton == ButtonState.Released)
            {
                if (new Rectangle(0, 0, this.Window.ClientBounds.Width, this.Window.ClientBounds.Height).Contains(mouseState.Position))
                {
                    _mouseClickX = mouseState.Position.X;
                    _startRotation = _arrowRotation;
                }
            }
            else if (mouseState.LeftButton == ButtonState.Released && _oldMouseState.LeftButton == ButtonState.Pressed)
            {
                _mouseClickX = null;
            }

            // Check if rotation reset.
            if (mouseState.RightButton == ButtonState.Pressed && _oldMouseState.RightButton == ButtonState.Released)
            {
                _arrowRotation = 0.0f;
            }

            if (_mouseClickX != null)
            {
                float pixelsPerDegree = 2f;
                _arrowRotation = _startRotation + (float)MathHelper.ToRadians(((float)mouseState.Position.X - _mouseClickX.Value) / pixelsPerDegree);
                if (_arrowRotation > MathHelper.TwoPi)
                    _arrowRotation -= MathHelper.TwoPi;
                else if (_arrowRotation < 0)
                    _arrowRotation += MathHelper.TwoPi;
            }

            _oldMouseState = mouseState;

            base.Update(gameTime);
        }

        protected override void Draw(GameTime gameTime)
        {
            // Draw to target
            this.GraphicsDevice.SetRenderTarget(_target);
            this.GraphicsDevice.Clear(Color.Transparent);
            _spriteBatch.Begin(samplerState: SamplerState.PointClamp);
            Vector2 arrowOrigin = Vector2.Zero; // new Vector2(_arrow.Width / 2, _arrow.Height / 2);
            _spriteBatch.Draw(_arrow, new Vector2(50, 50), null, Color.White, _arrowRotation, arrowOrigin, new Vector2(1.0f), SpriteEffects.None, 0.0f);
            _spriteBatch.End();
            this.GraphicsDevice.SetRenderTarget(null);

            // Scale target up to screen.
            GraphicsDevice.Clear(Color.CornflowerBlue);
            _spriteBatch.Begin(samplerState: SamplerState.PointClamp);
            _spriteBatch.Draw(_target, new Rectangle(0, 0, _graphics.PreferredBackBufferWidth, _graphics.PreferredBackBufferHeight), Color.White);
            _spriteBatch.End();

            // Draw scaled version.
            _spriteBatch.Begin(samplerState: SamplerState.AnisotropicClamp);
            Vector2 arrowOriginScaled = Vector2.Zero; // new Vector2(_arrowScaled.Width / 2, _arrowScaled.Height / 2);
            _spriteBatch.Draw(_arrowScaled, new Vector2(100 * 4, 50 * 4), null, Color.White, _arrowRotation, arrowOriginScaled, new Vector2(1.0f), SpriteEffects.None, 0.0f);
            _spriteBatch.End();

            // Draw debug text
            _spriteBatch.Begin();
            _spriteBatch.DrawString(_debugFont, $"Rotation: {Math.Round(MathHelper.ToDegrees(_arrowRotation), 2)}Ā°", new Vector2(2f, 2f), Color.White);
            _spriteBatch.End();

            base.Draw(gameTime);
        }
    }
}

1 Like

One more thing to note here. Obviously this changes the way a rotated sprite looks. It retains its pixelated look, but it loses the jagged line look when itā€™s on an angle. If this isnā€™t desirable (ie, you want the jagged line look), I thought of another option for youā€¦

You might try drawing your rotated arrow to an appropriately sized render target first, so you can always draw it at a constant location (ie, Vector2.Zero). Then you can draw your render target to the position the arrow is supposed to go. This will mean all rasterization for the rotated sprite gets done when you draw to the render target. After that, youā€™re drawing what effectively amounts to the pre-baked sprite we talked about above.

That might give you the consistency youā€™re looking for at least?

1 Like

Yeah, drawing the rotated arrow scaled up directly to a higher-than-native resolution screen will give smoother results. But like I said Iā€™m not sure I want to draw my entire scene directly to the screen and make the scene look different at different resolutions. Also if the user plays the game at native resolution, I will still have the problem, so it seems like Iā€™m neglecting lower resolutions where there will be visual glitches like this.

Yes, that is the approach I mentioned when I replied to KakCAT. That will work, but I will primarily rotate small projectiles like arrows or particles and I might want to draw 100s or 1000s of them on screen. This approach will prevent me from batching vertices, so I can draw many rotated particles/projectiles in one draw call instead of individual draw calls, which might be very bad for performance. Also, that will be twice the individual draw calls.

But yes, other than pre-baking this is the only 100% reliable way to eliminate the inconsistencies. So I will just have to choose between memory (having a texture with e.g. 360 arrow sprites each rotated at a 1-degree difference) and performance. Might be able to get away with a greater difference like 3 degrees.

Not necessarily. If you wanna go that route, it should be sufficient to just pre-render the rotated arrow onto a spritesheet and just render the one you need. In your desired pixel-res you can get along with 360 (or 180) for every 1Ā° (or 2Ā°) should handle every possible permutation of the rotated angle ā€¦ if your arrow is symmetrical itā€™s even a quarter of that, because you can just mirror it over x/y (negative scale)

This is basically how it was done in the older days - just that you can do it at run time when loading the game - as you end up with pixel aligned sprites and dont need to care about rotation, which is the actual culprit

Ah, yes. Youā€™re right I would only need 90 arrow sprites with a 1-degree difference because I can flip them on the x and y axis, as long as itā€™s symmetrical as you say. In practice, I am flipping them on the x-axis already rather than rotating them more than 180 degrees. I have some arrows that are not symmetrical on the y-axis, though. So it will probably be 180 sprites in general for 1-degree difference.

You know your project better than I do, but Iā€™m not sure I understand this need to support such a low native resolution. What devices do you plan to ship to?

So you did, but I think it can still work. You guys are now talking about the pre-baked solution again, where you generate all the rotations at the start of the game, but you do have another caching option that will work for an arbitrary rotation (not just limited to a 1-degree increment).

Again, I donā€™t know your project but you could do the pre-render step once, only when the rotation changes, and then cache the texture for subsequent frames. This could live in a shared cache so other arrows could use the same texture and wouldnā€™t need to re-render it either, though if you do that you might want some kind of time-based cache clear so textures donā€™t pile up. If your thousands of arrows change rotation often though, it might just be better to pre-bake them at the very start in those 1-degree increments.

Many pixel art games Iā€™ve played support low resolutions down to what I assume is their native resolution. Not 100% sure why. For 1 it could just be to play in windowed mode, 2 for fullscreen it is much faster to render to 640x360 than 1920x1080 and for games like these, there may not be many benefits to using a higher-than-native resolution.

Not quite sure what youā€™re suggesting here. The thousands of sprites could very well all have different rotations pretty much. How could they share the same texture, unless I have a big render target with a specific position reserved for each specific rotation? I wouldnā€™t be better off than pre-baking.

Pre-baking is fine for these small sprites, but it gets worse if they are animated. 36x3 means in the worst case a rotated arrow uses 36x36 pixels. If I just store them in 2-degree increments and flip on the x-axis thatā€™s 90 sprites that could fit in 360x360. But with just 4 frames of animation, it might need upwards of 720x720. This is not so bad, but if I have many different textures like this, or they are larger it could add up. But I think itā€™ll be alright if I use the pre-baking for small sprites and render targets in the case of larger ones (which there probably wonā€™t be that many of on-screen at a time)

Thatā€™s just an aesthetic choice but itā€™s not required. You can make your game so it looks like itā€™s that resolution, but use the trick I showed above to render more pixels on rotations for a cleaner, yet still pixelated, look. You donā€™t have to, but itā€™s entirely up to you. The only reason to strictly enforce a low resolution is if you wanted to support some obscure device. Like, thereā€™s a lot of custom hand-helds that people make for gaming purposes that just have low-rez screens as a part of the build. Past that though, I think youā€™d be hard pressed to find a phone that didnā€™t support 720pā€¦ probably also still hard pressed to find one that didnā€™t support 1080p.

What I mean is, have a cache that stores Dictionary<float, Texture2D>. When your spriteā€™s rotation changes, check the cache to see if it contains a key for that rotation (you might want to take steps to limit the impact of floating point error). If it doesnā€™t, in that frame, generate the texture and store it. If it does, just use that texture. The texture isnā€™t a sprite sheet, but the texture specifically for that rotation and that rotation only. You could probably further optimize, as you said earlier, by making sure the key is normalized to a pi/2 slice of the unit circle and using flipping to get the rest.

You could potentially still have a lot of rotation sprites, but youā€™d only generate them as you need them and all arrows of the same rotation could use them. You could also potentially dispose of them when you no longer need them.

However, if youā€™re going to have 1000+ arrows with potentially all different rotations all at the same time on the screen, thatā€™s a lot of texture data, at which point it doesnā€™t really offer much over pre-baking them at the start up sequence and just assuming set rotation increments.

Again, you know your project better than I do, I just wanted to provide an alternative :slight_smile:

1 Like

If you have an older PC with integrated graphics it might be necessary to reduce the resolution to get 60 FPS. In general, when I draw with my shaders to native resolution and just upscale one render target to the screen itā€™s good for performance. Are you sure games like Blasphemous and rogue legacy did it to support obscure devices only? Also to make such a decision only because I want rotating projectiles to be like this seems a bit questionable. When rendering directly to higher resolutions I would also need to ensure for all my shaders and stuff produces the same output as when drawing to native if I want to keep the aesthetic.

Sorry if I sound defensive, but Iā€™m just trying to justify either decision properly. Thanks for spending so much time helping me.

I would maybe consider tackling performance considerations from a different angle. Increasing resolution is usually one of the cheaper quality increases, but really, define some bar you want to meet and see if youā€™re meeting it. I suspect youā€™ll have more success with algorithms than you will with screen resolution here.

Oh no, definitely not, but itā€™s their aesthetic design choice. Thereā€™s no written rule for how pixel games should look, just that they have that retro vibe and look pixelly haha. You can do whatever you like for the game you want to make.

Hereā€™s an example I can think of for a pixel game where the size of a pixel isnā€™t locked to a specific dimension. You can see at various points in the demo video where things are drawn (especially particles) that are smaller than the pixels that make up the world and characters.

Having played it, thereā€™s a lot of scaling that happens too. Not sure about rotationā€¦ itā€™s been a while. Great game by the way, highly recommend! Itā€™s short, but creative and very entertaining :slight_smile:

No problem at all. At the end of the day this is all your call. Iā€™m just trying to offer you options and maybe challenge your assumptions a bit. Nothing is worse than someone who isnā€™t doing the work telling you how you need to do the work haha.

Not limiting yourself to a specific low resolution gives you more options with your visuals, but it might also take you away from the visuals you want.

I meant it more like itā€™s a benefit and a reason a user might want to lower the resolution. In my profiling, one of the biggest things for the CPU is the number of draw calls (batch drawing e.g. tons of particles makes a huge difference). The most important thing for the GPU was the number of pixels it had to draw. When testing with a bunch of lights in the scene it didnā€™t really matter so much what happened in the pixel shader, it was just the area (number of pixels) to pass over. Iā€™d say in most cases like mine, a somewhat standard 2D pixel art game, if youā€™re just a little smart about how you write your code, the number of pixels the GPU has to pass over will be the bottleneck for framerate (it is for me, mind you I donā€™t have a performance problem, itā€™s just the bottleneck) and you can only optimize so much. If you really need to draw a bunch of stuff on screen or do a bunch of passes it will cost you. 1280x720 vs. 640x360 is a 4x difference in the number of pixels the GPU/integrated graphics will have to pass over when drawing stuff.

Yeah, there are plenty of pixel art games where a pixel doesnā€™t have to be a certain size. I guess Iā€™m taking more of a purist approach. But either way, Iā€™m just not sure I want to leave out the possibility of playing at native resolution/low resolution.

Do you have a group of users (or just friends/family) that you prototype against? Sometimes itā€™s pretty easy to get it in your mind that something needs to be a certain way, but then your userbase has a different opinion. I know I had a lot of my own preconceptions challenged when I was developing my game.

Anyway, I hope it works out. I look forward to seeing your progress! :slight_smile:

Absolutely, but Iā€™m just thinking there are benefits and other games that are similar to mine offer a low resolution, and offering it as well canā€™t hurt. But if offering the option (or prioritizing the experience in native resolution at least) only helps <1% of my users and it means compromising the visuals for everyone else, then it might be better not to. Although the best of both worlds is obviously preferred. Also trying to decide if it better fits the style to have a pixelated style of rotations or rotate smoothly.

If I prefer smooth rotation when possible (at higher resolutions), this is a good explanation of how to achieve the best of both worlds, itā€™s basically what Iā€™m doing now (which I did before this video came out haha) I just only have 1 layer and it is pixelated. My only question is how he applies the light shader to his rotated player sprite: https://www.youtube.com/watch?v=jguyR4yJb1M

Hmmā€¦ I think he just draws the light to a separate layer and then blends its upscaled version with e.g. the high-res player layer. That wonā€™t do it for me since I plan on doing some diffused lighting and other stuff.

1 Like