Scaling Pixel Art
This is a beginner's guide, it’s oversimplified to be used as a quick reference.
What’s Pixel Art?
There are many definitions, but my favorite is: “It’s low-resolution art where the placement of every pixel is intentional.” Working with pixel art has many advantages but also comes with several challenges and limitations. Pixels are drawn on a grid, similar to certain mosaics or cross-stitching, like this:
Scaling
Your computer screen (or phone, or tablet, or whatever device you use) also has physical pixels on it:
The pixels on our drawing grid need to be translated to real monitor pixels. The most obvious way would be for 1 pixel on the drawing to become 1 pixel on the screen. Back in the 90s, that worked great: a typical drawing had 32x32 pixels, and the screen resolution was around 640x480 at the high end. Now, my current monitor uses a 3840x2160 resolution, so a 32x32 image would look way too tiny. Time to scale it up!
Scaling Up
The good news is that you can scale up pixel art forever without losing quality. The bad news is that you can only do this in 100% increments of the original image. Scaling it to 2x or 3x is fine, but scaling it to 2.5x will result in a broken resolution, like this:
Just make sure whatever software you’re using doesn’t use a smoothing filter! When scaling images like photos, you want a smooth filter to avoid seeing individual pixels, but that’s the last thing you want for pixel art!
Each software calls their filters by different names, so look for Nearest Neighbor, Point, or None. Avoid anything like Bilinear, Linear, or Bicubic.
Scaling Down
Let’s get this out of the way: you shouldn’t scale pixel art down—it will ruin it. Are you going to do it anyway? If so, I recommend using a smooth filter like Bilinear or Trilinear. It won’t look like pixel art anymore, but at least it won’t completely break apart. To do a proper job at scaling down, you need to manually remove columns and rows of your image while fixing the problems that this would cause.
There’s one exception, though! Sometimes, someone sends you a pixel art image that’s already scaled, say 3x, but you really want it at 2x. Can you do this without losing quality? Yes!
First, if you don’t know the original scale, zoom in and find the size of a big pixel; that’s the size of your image! Literally count the pixels there: if it’s 3 pixels tall, you have a 300% scaled image. Second, I like scaling it back to the original size just to be safe. Technically, you don’t really need to do this most of the time, but I like to check that nothing breaks when scaling down. Just divide the image resolution by the scale, then you can scale it up normally.
Common Problems
I want to scale it to a non-integer number
Well, you can. It won’t look as good, but you can. If you’re aiming at a really high resolution, it won’t make much of a difference, to be honest. This is something you can handle on a case-by-case basis, so here are some solutions with their upsides and downsides:
Up and Down
Scale it to the closest integer using Nearest Neighbor, then scale it up or down using Bilinear. This is usually my go-to approach. It blurs the pixels a little, but it keeps the art mostly intact.
I don’t care
Just scale it to your target using Nearest Neighbor. Sometimes, your resolution is so high that the broken pixels aren’t noticeable.
Use a fancy shader
If you know how to use shaders and aren’t afraid of them, this is by far the best approach. Based on this amazing article, I made my own shader for use on Murder Engine. Feel free to use or adapt it however you want:
#include "murder.fxh"
float2 viewportSize;
float2 textureSize;
float texelsScale;
float4 MainPixelShader(VSOutput input) : SV_Target0
{
float2 texelSize = float2(texelsScale, texelsScale);
// Position within the input texture
float2 texPos = float2(input.uv * textureSize);
// Position inside the input texture pixel
float2 locationWithinTexel = frac(texPos);
float2 interpolationAmount = clamp(locationWithinTexel / texelsScale, 0, .5) + clamp((locationWithinTexel - 1) / texelsScale + .5, 0, .5);
float2 finalTextureCoords = (floor(input.uv * textureSize) + interpolationAmount) / texelSize.xy;
float4 color = input.color;
color.rg = locationWithinTexel;
color.b = 0;
return SAMPLE_TEXTURE(Texture, finalTextureCoords / viewportSize) * input.color;
}
float4 PixelMainPixelShader(VSOutput input) : SV_Target0
{
float2 texelSize = float2(1 / textureSize.x, 1 / textureSize.y);
float2 locationWithinTexel = frac(input.uv * textureSize);
float2 interpolationAmount = clamp(locationWithinTexel / texelsScale, 0, .5) + clamp((locationWithinTexel - 1) / texelsScale + .5, 0, .5);
float2 finalTextureCoords = (floor(input.uv * textureSize) + interpolationAmount) / texelSize.xy;
return SAMPLE_TEXTURE(Texture, finalTextureCoords / textureSize) * input.color;
}
SINGLE_PASS_TECHNIQUE
I want to mix pixel art resolutions
Every time someone mixes pixel art resolutions, an angel dies. Unless you know exactly what you’re doing or are doing it to look bad on purpose, this is almost always a bad idea. The point of pixel art is that it respects a very obvious pixel grid, and by mixing resolutions, you make it clear that your grid is arbitrary. But if you really need to do this, here are some ideas to make it less bad:
- Use different resolutions on separate “canvases” and keep them apart. A good example would be having different resolutions for your UI and game.
- Use integer multiples, like elements at 100% and others at 200%, but never at 150%.
- Do not mix more than two resolutions.
- Instead of mixing pixel art resolutions, consider mixing pixel art with high resolution. I also have an article about that.