Skip to content
GitHub repository
Smooth graphics

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 });
            }
        });
    },
});
PixiJS Graphics (left) and SilkGraphics (right) draw the same scene. When you zoom in, Graphics without MSAA shows stairs and facets. SilkGraphics stays smooth.

The options at a glance#

TechniqueHow to enable in PixiJS v8Edge qualityCostApplies to
MSAAapp.init({ antialias: true })4 samples, so 5 coverage levels per edge pixelmemory bandwidth, resolve passmain canvas (render textures and filters opt in)
Higher resolutionresolution: devicePixelRatio, autoDensity: truesmaller stairs, still aliasedfill rate grows with DPR squaredeverything
Curve smoothnessapp.init({ bezierSmoothness: 0.9 })fewer facets on curves, edges still aliasedmore trianglesGraphics curves
Supersampling (SSAA)render into a 2x render texture, then draw it scaled downhigh4x pixels, extra texturewhat you render into it
FXAAa post-process filtersoftened edges, blurred detail and textone full-screen passthe filtered object
Analytic SDF (pixi-silk)new SilkGraphics()exact coverage, continuousone instanced draw calleverywhere, 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 at resolution: 1 unless 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: true plus resolution: devicePixelRatio.
  • For UI, charts, icons, thin lines and zoomable or rotating vector content, use analytic anti-aliasing with SilkGraphics. Keep antialias: false to 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#

API reference: SilkGraphics, createSilkApp