Scaling sprite down while preserving blockiness

Hi all,

first of all, I apologize in advance if (or better when!) I’ll be using the wrong terminology: I’m not a game developer, and have only recently started playing with SDL2 to write a game engine for fun, so there’s good chances that I’ll describe my issues and attempted solutions poorly. In case there’s better ways to address things, please do let me know, as this is all part of a learning process for me!

As I was saying, I’m using SDL2 to write a game engine, specifically an open source point and click adventure engine called KIAVC. I only started a few months ago, and so far it’s been a great experience, since SDL2 definitely made a lot of things easy for me to handle. I did bump into a couple of obstacles when dealing with scaling, though, so I thought I’d ask the community about it.

Specifically, the demo I’m working on uses a lowres canvas (320x180) that I then scale up programmatically (it can be changed dynamically) as far as the actual window is concerned. By default, the demo enforces a 4x scale factor, which results in an actual 1280x720 resolution. Initially I took care of this scaling manually, using SDL_Rect accordingly, but then I found out about SDL_SetLogicalSize which did make all that much easier for me (I only had to scale mouse clicks accordingly, since unlike motion events those seem to refer to the actual resolution and not the logical one).

This works fine, but I’m encountering issues when it comes to scale the characters (actors, in my engine) I show on screen, which again can happen dynamically (e.g., character working towards the horizon). With a 4x scale factor and a small logical size, everything looks intentionally “blocky”, since pixels are basically 4x4 rather than 1x1, as most pixel-art games do: scaling a character down, though, this assumption seems to be broken, since they become more defined, as they’ll be rendered with pixels that are smaller than the 4x4 pixels everything else is rendered at. I guess this makes sense since SDL_SetLogicalSize under the curtain seems to only use viewports and SDL_rect to do its job (so basically what I was doing manually before), but that’s still a bummer, as it breaks the pixel-art-y nature of the game.

An example is presented in the image below, where I scaled the main actor at 0.20 of its actual size to demonstrate the problem: as you can see, it’s much more defined than everything else, since the scaling is taking into account the actual resolution (1280x720) rather than the logical one (320x180), despite me passing the logical scaled-down resolution in SDL_Rect object when calling SDL_RenderCopy.

Assuming this is the intended behaviour, is there any way to circumvent that? Is this something that SDL_SetRenderScale can help with? (I assume not, since from the documentation it looks like SDL_SetLogicalSize already uses it internally) Am I forced to make copies of the textures at different scale factors myself and then render those? This is something I’d rather avoid, since scaling can happen progressively in the engine, and this would force me to continually create/destroy textures as scaling occurs, possibly with additional reference counting since the same “costume” (animations) can be assigned to different actors in my engine, and these actors may be scaled differently or not, as part of their role in the game. I’m not sure how this is usually dealt with.

I have a few other doubts related to SDL_SetLogicalSize, but I’ll create different topics for them in the next few days: at the moment this is the main issue I’m trying to address.

Thanks in advance for any help or feedback you’ll be able to provide, and apologies again if I did a terrible job of explaining what I’ve been doing and what I’m struggling with :grin:

I see two (similar) options.

  1. you set up a global target texture of size (320x180), you do all your operations by rendering to this texture. Including scaling your characters when you want them smaller. And then, only once per frame, you render it to the “true” desktop screen (eg 1280x720) using whatever scale you need.

  2. (more complicated, but this would allow, for instance, to have a gradient background with high resolution on which you draw your pixel art) for each character you define 2 textures; Tex1 is the original. Tex2 is a target texture. Each time you want to render Tex1 at some (logical) scale x, for instance make it smaller because the character moves away, you first render_scale it onto Tex2 with scale x, and then you render Tex2 to your scene using whatever scaling your desktop imposes.
    In this way your pixel should always have the same size.

Thanks for the quick answer @Sanette!

Option 1. sounds intriguing, as it’s basically what I would have expected as the outcome from SDL_SetLogicalSize. Besides, it might be the solution to a different issue I wanted to handle subsequently as well, that is how to render debugging stuff at actual resolution, and game assets at logical size (which is something that SDL_SetLogicalSize makes harder, since it impacts all rendering operations). I’m a bit concerned about performance, since it basically means copying stuff at least twice (and that may be a problem for games that use a higher logical size), but considering I’m already using background layers for parallax that basically span the whole screen, and I may use more for different effects, I guess that one additional copy of the drawn canvas to screen won’t matter much.

How would this be implemented in practice, though? Would I need to create two different renderers, one for the texture (to use for SDL_SetRenderTarget), and one for the screen (using SDL_SetLogicalSize for that one alone)? From what I know, you can only have one renderer, which probably means that rendering to the texture might need to happen differently.

Option 2. is interesting, but from what I understand is basically the equivalent of scaling the actors on demand myself before rendering them, so what I’m trying to avoid. Besides, I’m actually not interested in higher resolution backgrounds, since all assets in the engine are scaled accordingly the way the engine works right now (meaning backgrounds are in line with the logical resolution too).

Quick update: I tried option 1., which was actually quite easy, since it was just a matter of calling SDL_SetRenderTarget on my texture to render everything, and then at the end call SDL_SetRenderTarget with NULL in order to SDL_RenderCopy the texture before calling SDL_RenderPresent. That did indeed do the trick!

The side effect is that, since in my demo I was already partially scaling down the main character in the first location (~0.76 instead of 1.0), now that one is way too blocky :joy: I probably failed to notice that before, but is a different problem I’ll handle separately (I’m using free assets so that actor is simply too large for the screen).

This approach won’t help with the “render debugging info at actual resolution vs. game at logical resolution”, but again, that’s a matter for a separate investigation and potential discussion.

Thanks again for the help and guidance!

The main question is — are all textures and sprites prepared specifically for 320x180 resolution? There are retro-style games that render an image in such a way that all its pixels are the same size and aligned (in the form of a ordinary grid), and there are also games that break this rule. How to implement rendering depends solely on the answer to the above question.

1. The image frame creates an even grid of pixels.

The final image of the frame looks like, for example, on the NES/SNES console. All pixel coordinates are divisible by the given unit, all pixels are the same size, no sub-pixel support. Scaling, stretching and rotation are still possible (see: SNES), antialiasing is still possible.

Solution: a back buffer in the form of a texture with a fixed resolution (yours is 320x180), on which the whole frame is rendered (world, UI, etc.), and which is finally rendered in the window (stretched over its entire client area, keeping or not the right proportions).

To be able to render everything on the texture of such a back buffer (and not on the window texture), it must be created with the SDL_CreateTexture function with the SDL_TEXTUREACCESS_TARGET flag. When rendering, you set the target with SDL_SetRenderTarget, render whatever you need, then restore the target by calling SDL_SetRenderTarget and specifying null as the texture, and finally rendering the buffer in the window with e.g. SDL_RenderCopy. To do this, you need one renderer per game window (if you want to use one window only, then you need one renderer only).

If you care about a classic pixel art setting, use this method. It is very easy to program, sprite assets require minimal amount of time to create, and the final image looks like a retro game. It has its limitations, but they are the same as with the old consoles.

Tip: remember that after rendering a frame image, you can copy it to another, larger back buffer texture and post-process it (distort the frame to the shape of a CRT screen, add scanlines, etc.), so in short, create a render pipeline. True retro-style games do not throw sharp, square pixels on the screen, but try to imitate the CRT screen as much as possible. Sharp pixels are a retro-abomination.

2. Pixels may vary in size and may not be aligned.

This is not classic pixelart, because pixels are rendered anywhere (in other words, the upper left corner of a given pixel can be the same as other pixels, but it can also be inside another pixel). A cheap trick, but it gives more possibilities, because there are no limitations related to the low resolution of the back buffer. Rotating, scaling, stretching and antialiasing is possible in any way. The problem, however, is that the final image only pretends to be pixelart, but breaks its basic rules.

Solution: you can render the image however you like, directly in a window or on a back buffer of any size. However, when rendering, you have to take care of the correct size of the sprites on the screen, taking into account the resolution of the window. You can use the autoscaling offered by SDL (I don’t know it, I don’t use it), or you can calculate everything yourself (calculate the multiplier from the window resolution and use it to calculate target coordinates and sprite size).

Thanks for the detailed insight @furious-programming!

My use case is indeed 1., where all assets are conceived to work for that 320x180 resolution (at least for the specific demo I’m using for testing; other games may want to use different resolutions). The back buffer solution is the one Sanette suggested, so it’s what I implemented as an experimental pull request in my engine already. I’ll need to sort out some issues that it introduces, but it definitely solves the main issue I had initially!

You’re definitely right of course about shaders and stuff like that. While I’m focusing on the core aspects of the engine first, I’ve been experimenting a bit with that too, with a barebones scanlines generator that can be enabled programmatically. That said, this is where a different issue I’ll have to address is, since the scanlines need to work with the actual resolution (1280x720) rather than the game one, as otherwise the semitransparent lines you draw are way too thick and it just doesn’t work (and unfortunately, due to my usage of SDL_SetLogicalSize, that’s how it works in my case right now). I’m not exactly sure on how to solve that part, since at the moment SDL_SetLogicalSize is required to render to the smaller texture: one option may be to undo the change I made and stop using SDL_SetLogicalSize, and thus scale things up and down manually as I did before, but if there’s a way to keep it there and still be able to render things at the actual resolution when needed would save me some time.

Actually, I’m being dumb: with the use of the back tecture, SDL_SetLogicalSize is not needed anymore. I’m rendering to a texture of the required resolution already, and rendering that scaled to the whole screen is much easier than scaling every single asset I render. I’ll play with that a bit too.

Exactly. With this approach, something like scaling is not needed at all — that’s why it is so simple to use.

All you need to calculate is the area in the window where the frame will be rendered. This is necessary so that the aspect ratio of the image on the screen is always correct, regardless of the resolution used and the orientation of the monitor/window. You can calculate the target area for each frame, or (better solution) you can calculate it once and use it for reading (just update it when handling the SDL_WINDOWEVENT_RESIZED event).

I understand and think this is a good approach. Nevertheless, if you are creating an engine, you should immediately think about how you will implement the entire rendering process. You should write the rendering code in such a way that it creates a pipeline, i.e. rendering in a certain number of steps, and that the individual steps (and their number) are configurable.

The more filters, the more render steps. If you use any filter (distorting, glowing, scanlines etc.) then add the ability to set the filter strength (e.g. weaker or stronger scanlines, stronger or weaker distortion, etc.). The user should be able to adjust all this to his own requirements — enable or disable a filter, and if enabled, then set its parameters. These are not difficult things to do, and every player will have some freedom.

1 Like

Yep, that part I was doing already, so that’s covered at least :grin:
I just got rid of SDL_SetLogicalSize and rearranged the order of draws a bit, so that I can first draw all the game stuff with the texture as a target, then copy that to the texture and render all debugging stuff (walkboxes/objects area lines, interactive console, scanlines) at actual resolution.

This basically addresses all the roadblocks I had so far with respect to scaling, so thanks again for all the precious help to both of you!

If you’re interested in imitation of a CRT screen, very good references are easily found on Google (e.g. for the phrase crt pixelart). An example of retro-masterpiece below:

This is how modern retro-styled games should look like. Pixel perfect is poverty, not retro. :wink:

As someone who grew up with an MSX home computer plugged in an old TV, I 100% agree :joy:
Thanks for the pointers! I’ll see if I can use that to create something more fancy than my basic scanlines generator.