The pixelated look of the games of the past was largely the result of the severe constraints that graphics hardware imposed to developers back then. Obtaining the same look on modern game engines, however, can be quite difficult and requires some setup.

In a previous post we’ve seen how the game camera can be setup in Unreal Engine 4 to make pixel art look crisp when viewed on most screens. Today, I’d like to go over a series of tricks that we use inside the engine to enforce a more authentic pixelated look in Guntastic.

1. Everything Should Be Aligned to the Pixel Grid

On a screen there is no such thing as an half-pixel, but in a game world it’s common for sprites to end up in positions that aren’t aligned to the pixel grid. This introduces some noticeable inconsistencies in the way sprites are aligned.

A comparison of two sprites which shows the importance of conforming to a pixel grid

The simplest solution to prevent this from happening is to snap the sprites to the pixel art grid just before rendering. Most implementations I could find online perform the snapping directly in the world, by conforming object locations to the grid just before rendering. The original location is then restored on the objects at the beginning of the next frame.

Unfortunately, this is cumbersome to do in Unreal Engine 4 (the resources I found were for Unity), and might have unwanted side-effects (like triggering overlaps, for example). As such, in Guntastic we ended up using a simpler approach: snapping is applied directly in the vertex shader by offsetting the sprite geometry vertices.

Simple shader that snaps geometry vertices to the pixel art grid
A simple material function that can be used to snap vertices to the pixel grid.
The result of the shader illustrated above
Pixel snapping at work.

This works on the assumption that the vertices of the rendering geometry of the sprites are aligned to the pixels of the art itself, so special care should be taken to generate valid rendering geometry.

Examples of render geometries, showing that only sprites with render geometry vertices aligned to the pixel grid will work with the snapping shader
Sprite render geometry vertices should be aligned to the pixel grid for the snapping to work correctly.

Prevent Jittering When the Camera Moves

It’s also important for the camera to be aligned to the pixel grid. Otherwise, when the camera moves, we’ll probably end up looking at the sprites from locations that are not on the grid, which will make the sprites jitter out from their expected positions.

In our implementation, we apply the snapping at the end of the camera update logic, after everything else (including special effects like screen shakes, etc.) have been calculated. This is how the actual code looks like:

void AFolliesPlayerCameraManager::DoUpdateCamera(float DeltaTime)
{
    // Update the camera
    Super::DoUpdateCamera(DeltaTime);

    // Snap the final camera location to the pixel grid
    {
        const float PixelsPerUnits = 0.24f;
        const float UnitsPerPixel = 1.0f / PixelsPerUnits;

        FMinimalViewInfo CameraCachePOV = GetCameraCachePOV();
        CameraCachePOV.Location.X = FMath::GridSnap(CameraCachePOV.Location.X, UnitsPerPixel);
        CameraCachePOV.Location.Z = FMath::GridSnap(CameraCachePOV.Location.Z, UnitsPerPixel);
        FillCameraCache(CameraCachePOV);
    }
}

2. Sprites Shouldn’t Rotate

Pixels in a grid can’t rotate, and so shouldn’t your sprites. Furthermore, rotating textures that use nearest-neighbor filtering introduces evident aliasing between pixels.

A rotated sprite, showing some ugly aliasing between pixels
Rotated sprites look bad.

The simplest solution here it to avoid to physically rotate the objects in the world, and use hand-drawn rotated versions of a sprite where rotation is actually needed.

While developing Guntastic, however, we encountered some edge cases that still required to handle in-world rotations. An example of such cases are guided missiles, which need to track a target by pointing towards it: here the amount of rotated sprites to draw was simply too much to handle for a team with a single artist.

To handle these (sporadic) cases, we fell back to an antialiasing technique, created by Cláudio Fernandes, called Manual Texture Filtering. This technique works by:

[…] performing linear interpolation between texels on the edges of each texel in the fragment shader, but sampling the nearest texel everywhere else.

In other terms, it smooths out jaggies between pixels, while keeping the overall result crisp. The only caveat when working with this technique is that linear filtering should be applied to the sprite texture (instead of nearest-neighbor filtering). Here’s how the shader looks like when implemented in an UE4 material function:

The "Manual Texture Filtering" shader implemented in UE4
The "Manual Texture Filtering for Pixelated Games" shader implemented in UE4.

This almost completely eliminated the problem:

Manual texture filtering at work inside UE4
Manual texture filtering at work.

3. Maintain Pixel Density

Finally, it’s important for the sprites in the game to have the same pixel density: this means they should be created using the same reference grid, and never scaled up.

Two sprites of the same size, but with different pixel densities

Luckily, applying this inside the engine is straightforward as it only takes some discipline to never apply scale to the sprites.

Taking It Further

The final step to ensure a true pixel-perfect look would be to render the game at the original art resolution, and upscale the rendered frame to match the actual screen resolution. This would automatically eliminate any possibility of inconsistent pixel sizes, because it’s impossible to draw anything smaller than a pixel.

Unfortunately we hit a couple of major road blocks when trying to implement it, and decided to abandon it (at least for now):

  • Rendering at a very low resolution would make the handful of post processing effects we use in the game (such as bloom) look distorted and aliased once upscaled.
  • Implementing this solution in Unreal Engine 4 requires changes at the engine level. This is something we’re trying to avoid as it can soon become a nightmare to manage for a very small team such as ours, with no full-time programmer/engineer.

If you’re interested in seeing this technique in action, I highly encourage you to take a look at the implementation available on Alex Ocias’ blog for Unity.