Quick answer: Subpixel shimmer in pixel art is almost always caused by fractional camera positions, non-integer render scales, or smoothing filters. Fix it by snapping the camera to whole pixels, rendering to an integer-scale offscreen target with point sampling, disabling MSAA, and adding a pixel-snap pass in the vertex shader.

Pixel art looks deceptively simple, but rendering it correctly is one of the trickier problems in 2D graphics. A sprite that looks crisp in your art tool can shimmer, swim, or develop seams the moment it appears on screen. The cause is almost always a mismatch between the artist’s pixel grid and the renderer’s pixel grid. When those two grids drift out of alignment by even a fraction of a pixel, you get the telltale “crawling pixels” effect that ruins the aesthetic. This guide walks through the four root causes and the fixes for each.

Symptom: Sprites Shimmer When the Camera Moves

Move the player character across the screen and watch the edges of background sprites carefully. If you see pixels appearing to crawl, swap, or flicker as the camera moves, the camera transform is landing on fractional pixel values. The renderer samples the source texture across texel boundaries, and bilinear or even point sampling produces inconsistent results frame to frame.

The fix is to round the camera position to the nearest whole pixel after all gameplay logic has run, but before the render. Crucially, you should keep the unrounded camera position around for game logic that needs subpixel precision (smooth follow, deadzones, lookahead). Only the rendered transform gets snapped.

function lateUpdate() {
    // Game logic uses precise subpixel position
    camera.position = followTarget.position + lookahead;

    // Rendering uses snapped position
    const pixelSize = 1.0 / pixelsPerUnit;
    renderCamera.position.x = Math.round(camera.position.x / pixelSize) * pixelSize;
    renderCamera.position.y = Math.round(camera.position.y / pixelSize) * pixelSize;
}

Notice the rounding is in pixel units, not world units. If your art is authored at sixteen pixels per unit, your snap interval is one-sixteenth of a unit, not one unit. Get this wrong and your camera will jerk between large positions instead of moving smoothly through pixel-sized increments.

Symptom: Sprites Look Blurry or Have Seams

If your sprites look soft or have a one-pixel gap between tiles, your render target is not at an integer scale relative to the source art. Suppose your art is authored at 320×180 and the player is running at 1920×1080. The clean ratio is 6×, which is fine. But if the player is on a 1366×768 laptop, the ratio is 4.27× — not an integer — and the GPU has to fudge the upscale.

The standard fix is to render the entire scene to an offscreen target sized to the native art resolution, then upscale that target to the window with point sampling at the largest integer factor that fits. Letterbox or pillarbox the leftover space. This guarantees that every pixel of source art maps to an exact integer block of screen pixels.

If you want to support fractional window sizes without letterboxing, render at the next integer up (e.g. 5× instead of 4.27×) and downscale the final result with bilinear filtering. This keeps the pixel grid intact internally and only smooths during the final composite, which is far less objectionable than smoothing each sprite individually.

Symptom: Edges Look Smoothed Even Though You Want Sharp Pixels

Multisample antialiasing is on by default in many engines, including Unity’s Universal Render Pipeline and Unreal’s default project templates. MSAA averages samples along polygon edges, which is exactly what you do not want for pixel art. The hard edges of your sprites get softened into a gradient.

Disable MSAA in your project settings. In Unity, set MSAA to Disabled in the URP asset. In Godot, set msaa_2d to Disabled in the project rendering settings. Also confirm that your sprite import settings use Point (no filter) sampling, not Bilinear or Trilinear. Bilinear sampling on a sprite atlas will leak adjacent texels into your sprite edges and create the same softening effect.

Symptom: Sprites Wobble Even With Snapped Camera

If you have already snapped the camera but individual sprites still wobble — particularly sprites that are children of moving objects, or sprites with rotation — the snap is happening at the wrong level. The camera transform might be on the pixel grid, but each sprite’s world transform is not.

The most reliable fix is a pixel-snap pass in the vertex shader. Snap each vertex position to the pixel grid after the model-view-projection transform but before perspective divide:

// HLSL / GLSL vertex shader fragment
float4 snapToPixel(float4 clipPos, float2 renderSize) {
    float2 ndc = clipPos.xy / clipPos.w;
    float2 screen = (ndc * 0.5 + 0.5) * renderSize;
    screen = floor(screen + 0.5);
    ndc = (screen / renderSize) * 2.0 - 1.0;
    return float4(ndc * clipPos.w, clipPos.zw);
}

This snap shader is a safety net: it forces every sprite vertex onto a whole pixel regardless of what the CPU-side transform was. It works well for orthogonal sprites and tile maps. It does not work for rotated sprites, where you want subpixel rotation to look smooth — in that case, accept the wobble or use a pre-rotated sprite atlas.

“We chased a shimmer bug for two weeks before realizing our parallax background layers were each calculating their own camera offset and rounding independently. The main camera snapped to whole pixels, but the parallax layers snapped to fractions of pixels because their offsets were divided by their parallax factor. Once we snapped each layer’s final transform instead of the camera offset, the shimmer disappeared.”

Related Issues

For a Unity-specific deep dive, see how to fix Unity Pixel Perfect Camera jitter on movement. For broader 2D rendering debugging tips, read best practices for error logging in game code.

Before you change a single line of shader code, take a screenshot at native resolution and zoom in 800%. You will often see the artifact is in the source art, not the renderer.