Anti-aliasing in PixiJS v8
Compare six ways to get smooth graphics in PixiJS v8: MSAA, higher resolution, curve smoothness, supersampling, FXAA and analytic SDF anti-aliasing.
Updated pixi-silk 0.1.0
Anti-aliasing removes the jagged “staircase” edges of shapes on a pixel grid. It gives each edge
pixel a partial colour, proportional to how much of it the shape covers. PixiJS v8 has five
practical options: MSAA (antialias: true), a higher resolution, finer curve tessellation,
supersampling and post-process FXAA. pixi-silk adds a sixth, analytic anti-aliasing. It
computes coverage from each shape’s exact signed distance field, so it is exact at any zoom,
rotation and device pixel ratio.
Source src/demos/versus.demo.ts
import { Container, Graphics, Text } from 'pixi.js';
import { SilkGraphics } from 'pixi-silk';
import { defineDemo } from './runtime';
// Left: Pixi v8 Graphics (tessellated, no MSAA). Right: SilkGraphics (distance fields).
export default defineDemo({
size: [480, 240],
controls: {
zoom: { type: 'range', label: 'zoom', min: 1, max: 6, step: 0.01, value: 1 },
spin: { type: 'toggle', label: 'rotate', value: true },
},
setup({ stage, params, tick }) {
const sides = [new Container(), new Container()];
const pixi = new Graphics();
const silk = new SilkGraphics();
sides[0].addChild(pixi);
sides[1].addChild(silk);
sides.forEach((side, i) => {
const mask = new Graphics().rect(i * 240, 0, 240, 240).fill(0xffffff);
const label = new Text({
text: i ? 'SilkGraphics' : 'Graphics',
style: { fill: 0x8e8e93, fontSize: 12, fontFamily: 'system-ui' },
});
label.position.set(i * 240 + 12, 10);
side.mask = mask;
stage.addChild(side, mask, label);
});
let angle = 0;
tick((_t, dt) => {
if (params.spin) angle += dt * 0.15;
for (const [i, side] of sides.entries()) {
side.pivot.set(120, 130);
side.position.set(i * 240 + 120, 130);
side.scale.set(params.zoom);
side.rotation = angle;
}
for (const g of [pixi, silk]) {
g.clear();
g.circle(120, 130, 62).stroke({ width: 3, color: 0x64d2ff });
g.roundRect(70, 90, 100, 80, 24).stroke({ width: 1, color: 0xffffff });
g.circle(120, 130, 18).fill(0xff375f);
for (let k = 0; k < 6; k++)
g.moveTo(40, 60 + k * 3)
.lineTo(200, 64 + k * 5)
.stroke({ width: 0.6, color: 0xffd60a });
}
});
},
});The options at a glance#
| Technique | How to enable in PixiJS v8 | Edge quality | Cost | Applies to |
|---|---|---|---|---|
| MSAA | app.init({ antialias: true }) | 4 samples, so 5 coverage levels per edge pixel | memory bandwidth, resolve pass | main canvas (render textures and filters opt in) |
| Higher resolution | resolution: devicePixelRatio, autoDensity: true | smaller stairs, still aliased | fill rate grows with DPR squared | everything |
| Curve smoothness | app.init({ bezierSmoothness: 0.9 }) | fewer facets on curves, edges still aliased | more triangles | Graphics curves |
| Supersampling (SSAA) | render into a 2x render texture, then draw it scaled down | high | 4x pixels, extra texture | what you render into it |
| FXAA | a post-process filter | softened edges, blurred detail and text | one full-screen pass | the filtered object |
| Analytic SDF (pixi-silk) | new SilkGraphics() | exact coverage, continuous | one instanced draw call | everywhere, including filters and render textures |
How to enable MSAA in PixiJS v8#
import { Application, Graphics } from 'pixi.js';
const app = new Application();
await app.init({
antialias: true, // MSAA on the main canvas (WebGL context attribute)
resolution: window.devicePixelRatio, // render at device resolution
autoDensity: true, // keep the CSS size
});
MSAA belongs to each render target, so it does not follow your objects everywhere:
- Render textures are not multisampled unless you create them with
antialias: true. - Filters render into their own targets with
antialias: 'off'by default ('on'or'inherit'changes it). They also render atresolution: 1unless you raise it. - Thin lines below one pixel still break up. Four samples cannot represent a line that covers a tenth of a pixel.
Why MSAA is not enough for 2D UI#
MSAA samples coverage at four fixed points per pixel. That is fine for large shapes at rest. The things UI and charts are made of show its limits:
- Slowly moving or rotating edges jump between the five coverage levels, which reads as shimmer.
- Hairlines and small text-like strokes fall between samples and look dotted.
- Zooming reveals the tessellation of curves, because Graphics fixed its triangles when you drew.
- Fractional DPRs (1.25, 1.5, 2.625) add resampling blur, unless the canvas backing store matches the device pixels exactly.
Analytic anti-aliasing with signed distance fields#
A signed distance field (SDF) gives the distance from any point to the shape’s edge. pixi-silk evaluates each primitive’s exact SDF per pixel. It turns the distance into coverage over exactly one device pixel:
float px = sqrt(abs(dFdx(p).x * dFdy(p).y - dFdx(p).y * dFdy(p).x)); // one device pixel, in shape units
float coverage = clamp(0.5 - d / px, 0.0, 1.0); // d: signed distance to the edge
The result is right at any scale, rotation, resolution and render target, because the pixel size comes from screen-space derivatives. There is nothing to tessellate and no samples to miss. See How it works for details.
import { createSilkApp, SilkGraphics } from 'pixi-silk';
const { app } = await createSilkApp({ parent: document.body }); // antialias: false, DPR-exact canvas
const g = new SilkGraphics();
g.circle(200, 150, 80).stroke({ width: 2, color: 0x64d2ff }); // smooth at any zoom
g.roundRect(100, 60, 200, 180, 40, 0.6).stroke({ width: 0.5, color: 0xffffff }); // a clean hairline
app.stage.addChild(g);
Which one should you use?#
- For sprites and textures only, skip MSAA. The GPU filters textures.
- For occasional large shapes and no zoom, use
antialias: trueplusresolution: devicePixelRatio. - For UI, charts, icons, thin lines and zoomable or rotating vector content, use analytic
anti-aliasing with
SilkGraphics. Keepantialias: falseto save multisample bandwidth. - For content behind filters or in render textures, SilkGraphics keeps its quality. With Graphics, opt those targets into MSAA and raise the filter resolution.
References#
- PixiJS v8 Graphics API and the issue on smoother curves in v8.
- Inigo Quilez, 2D distance functions.
- web.dev, Pixel-perfect rendering with devicePixelContentBox.
- Glossary: MSAA, SSAA, FXAA, signed distance field.
API reference: SilkGraphics, createSilkApp