5 min read
Pixel In-Game

Now that I’ve got a clear art direction and assets, I need to actually add them to the game.

Let’s just shove all the new pixel art assets into the texturepacker, upscale them 2x, and see how it goes.

it-kinda-worked

Huh, it kinda worked.

There are dozens of minor issues, but the largest issue is that the pixel art isn’t scaled properly.

Unlike our original assets, pixel art is typically scaled using an algorithm called nearest neighbor. You can’t have half a pixel, so nearest neighbor can omit pixels unless it is scaled at a perfect integer interval.

Oh god, the code is NOT prepared for integer scaling…

Pixel Art Scaling

There are two methods for scaling/rendering pixel art:

  • Pixelated: Scale assets by multiplier, render at desktop resolution
  • Pixel Perfect: Render to native pixel resolution, scale whole rendering by multiplier

Pixelated renders mean that, even though the assets are scaled 2x, there are still double the amount of visible pixels. If I had a background 2x and a mage 2x on top of it, the mage could be sitting on 1/2 a pixel of the background.

Pixel Perfect renders ensure that the entire canvas is scaled all at once, so you cannot have extra pixels or objects sitting at 1/2 a pixel of another.

For either method, I need to choose a native pixel resolution. This decision is not easy, and incredibly permanent. The resolution needs to be able to nicely scale to 1920x1080, 1440p, 4k, etc by an integer.

Also, pixel art looks the worst at 1.5x. 1.5x is when nearest neighbor creates the maximum amount of bad pixel artifacts (because of half pixels), so if your native resolution has a 1.5x modifier at any point on a popular resolution, people with it will HATE YOU.

And the only solution is to create a complete asset set of all of your pixel art at a different size, like 32x32 -> 24x24 (or using a different scaling strategy, like linear filtering, and accept blurry pixel art).

Common native pixel art resolutions:

  • 320x180: Celeste
  • 320x240: Cave Story
  • 400x320: Stardew Valley (16x16 tiles)
  • 640x360: “High bit era” games
  • 960x540: Higher resolution pixel art (32x32 tiles)
  • 1280x720: FTL

So, I knew what was out immediately: 1280x720. It has a 1.5x modifier on 1080p, the most common desktop resolution. And the FTL devs have been told this a thousand times.

However, I wanted 24x24 or 32x32 characters, so 400x320 was just too small. Castles couldn’t really fit on this resolution.

I chose 960x540.

It scales 2x to 1080p, 3x to 1440p, and 4x to 4k. Unfortunately, 720p is a ~1.5x, but that is a shrinking portion of the market. It still looks clearly like pixel art, but plenty of space for detail and many units.

And it looked decent on 1080p (note I fixed the selection shader):

fixed

Pixel Viewport

So, obviously, 16:10 exists. I can’t just render the game at 960x540 with an integer multiplier. And there’s a thousand resolutions between pixel perfect choices.

Which means I have a few choices:

  • Stretch the pixel art, render at 960x540 scaled always
  • Add blackbars vertically or horizontally
  • Show more of the world but keep the pixel scale consistent

I chose to create a viewport that could flexible show more of the world but keep the pixel scale in most cases.

Now, my world doesn’t work in pixels, it works in meters. Characters are ~2 meters tall, so they are 16px per meter natively.

My scaling algorithm basically works like…

// find the best integer multiplier by finding the smallest deviation
int ppm = 16;
int multiplier = 1;
while (magnitude(screenWidth, screenHeight, multiplier) > magnitude(screenWidth, screenHeight, multiplier + 1))
    multiplier += 1;
// clamp width & height to maximum / minimum values. There will be blackbars in extreme cases
float worldWidth = Math.max(minWorldWidth, Math.min(maxWorldWidth, (float) (screenWidth) / (ppm * multiplier)));
float worldHeight = Math.max(minWorldHeight, Math.min(maxWorldHeight, (float) (screenHeight) / (ppm * multiplier)));

Which does pretty well, like for example on 1600x900

scale-900p

For my worst-case of ~720p, that means it’ll render at 16 ppm, which will keep the pixel art crisp but means players will be fairly zoomed-in compared to others.

scale-720p

And even at these resolutions, I can keep it pixel-perfect:

pixel perfect