Fixing Blurry and Jagged Sprites in Phaser CE When Scaling Down
Why downscaling sprites looks so bad in WebGL
When you scale a sprite down to a small fraction of its original size, the GPU has to map hundreds of source pixels onto a handful of screen pixels. How it handles that is controlled by the texture’s filter mode. The default in most WebGL renderers — including what Phaser CE uses under the hood — is bilinear filtering (LINEAR), which blends nearby pixel values together. At extreme downscale ratios, that produces a muddy, smeared result because it’s sampling only a tiny neighbourhood of the full texture rather than the whole thing.
Mipmaps solve this properly. A mipmap chain is a set of pre-generated, progressively halved copies of your texture stored alongside the original. When the GPU needs to render a sprite at 17% of its natural size, it pulls from the appropriately-sized mipmap level rather than trying to crush the full-resolution image on the fly. The result is dramatically sharper. But there is a catch — a significant one for Phaser CE users.
The power-of-two wall in WebGL1
Phaser CE runs on WebGL1. And WebGL1 enforces a strict rule: mipmaps only work on textures whose width and height are both powers of two (128, 256, 512, 1024, 2048…). If your dimensions do not qualify, the GPU silently ignores the mipmap setting and falls back to plain LINEAR filtering. No error. No warning. It just does not mipmap.
Card assets at 700×1050 are not power-of-two in either dimension. That is why enabling mipmaps produces no visible change — the setting is being discarded before it ever reaches the GPU.
WebGL2 lifts this restriction and supports mipmaps on any texture size, but Phaser CE targets WebGL1 by default.
Clearing up the load.image third parameter
The Phaser CE Loader.image(key, url, overwrite) method has a third parameter, but it is not a mipmap flag. It is an overwrite boolean — when true, it replaces any existing queued entry with the same key instead of skipping the duplicate. It has zero effect on texture filtering or mipmap generation.
Mipmap control in Phaser CE lives on the PIXI.BaseTexture object, not in the loader call.
Two immediate fixes to try
Option 1: Change the scale mode to NEAREST
If your cards have crisp outlines and flat colour — common with vector-exported card art — switch the texture filter mode to NEAREST. This tells the GPU to snap to the closest pixel rather than blending, which eliminates the smeared look at the cost of harder edges.
// Set on each child sprite after loading
sprite.texture.baseTexture.scaleMode = PIXI.scaleModes.NEAREST;
To apply it globally before any assets load:
PIXI.scaleModes.DEFAULT = PIXI.scaleModes.NEAREST;
NEAREST works well for pixel art and clean vector work. For painterly or photographic artwork it can look blocky at mid-range scales — in that case, mipmaps are the better answer.
Option 2: Enable mipmaps with power-of-two textures
For smooth, high-quality downscaling on detailed artwork, mipmaps are the right tool. To use them in Phaser CE you need POT dimensions. The practical options for 700×1050 cards:
- Resize source art to 512×1024 (slight crop or scale-to-fit) or 1024×2048 (upscale first, then let mipmapping handle the reduction).
- Pad canvases to the next power-of-two boundary and position content within that space — preserves every pixel of the original art but adds transparent padding around it.
Once your textures are POT, enable mipmaps right after they load:
this.load.onLoadComplete.addOnce(function () {
var bt = game.cache.getImage('card-base').baseTexture;
bt.mipmap = true;
bt.dirty();
});
The dirty() call tells PIXI to re-upload the texture to the GPU with the updated setting. Set this before the texture is first rendered for best results.
Pre-scaling: avoiding the WebGL1 limitation entirely
If rebuilding all artwork to POT dimensions is not practical, export multiple resolution variants and swap between them based on the sprite’s current scale at runtime.
function cardKey(scale) {
if (scale > 0.5) return 'card-full';
if (scale > 0.2) return 'card-half';
return 'card-quarter';
}
This is how professional card and board game titles handle the problem — resolution atlases selected by display size. It costs storage and export time, but sidesteps every GPU sampling limitation. It also gives you art-direction control at each scale: the tiny stacked version of a card can have text removed entirely rather than rendered illegibly small.
Fixing jagged edges with roundPixels
Blurriness and jaggedness can have different root causes. Jagged edges often come from sub-pixel positioning — when a sprite is placed at a non-integer screen coordinate like x=14.73, the renderer samples it mid-pixel and produces a faint aliasing fringe around the edges. Phaser CE has a flag to snap all coordinates to integer values:
game.renderer.roundPixels = true;
This will not fix blur from downscaling, but it cleans up edge jaggedness noticeably. Worth enabling regardless of which texture approach you choose.
Tweening across multiple resolutions
If you go the multi-resolution route, you can handle smooth scale tweens by swapping the texture when the scale crosses a threshold. Listen on the tween’s update callback:
tween.onUpdateCallback(function () {
var key = cardKey(sprite.scale.x);
if (sprite.key !== key) {
sprite.loadTexture(key);
sprite.key = key;
}
});
The swap is fast enough mid-tween to be invisible. Alternatively, keep the highest-resolution texture active during any animation and switch to the display-size-appropriate one once the card settles at its final resting position.
Sources
- phaserjs.github.io
- api.pixijs.io
- api.pixijs.io
- github.com
- github.com
- cables.gl
- en.wikipedia.org
- phaser.io
