PixiJS renderers draw your scene to a canvas using WebGL/WebGL2, WebGPU, or the Canvas 2D API. They're GPU-accelerated engines composed of modular systems that manage texture uploads, rendering pipelines, and more.
All renderers inherit from a common base, providing consistent methods like .render(), .resize(), and .clear(), along with shared systems for canvas management, texture GC, and events.
| Renderer | Description | Status |
|---|---|---|
WebGLRenderer |
Default renderer using WebGL/WebGL2. Stable and widely supported. | Recommended |
WebGPURenderer |
Uses the WebGPU API. Faster in many cases, still maturing. | Experimental |
CanvasRenderer |
Fallback renderer using the HTML Canvas 2D context. | Experimental |
The WebGPU renderer is feature-complete, but inconsistencies in browser implementations may cause unexpected behavior. Use the WebGL renderer for production applications.
Use autoDetectRenderer() to pick the best renderer for the current environment:
import { autoDetectRenderer } from 'pixi.js';
const renderer = await autoDetectRenderer({
preference: 'webgpu', // or 'webgl' or 'canvas'
});
// Only allow specific renderers (acts as a blocklist for any type not listed)
const renderer = await autoDetectRenderer({
preference: ['webgl', 'canvas'], // webgpu is excluded entirely
});
Or construct one directly when you need a specific renderer type (e.g., for testing or when you know the target environment):
import { WebGLRenderer } from 'pixi.js';
const renderer = new WebGLRenderer();
await renderer.init(options);
Most applications should use autoDetectRenderer() and let PixiJS pick the best backend. Use direct construction only when you have a specific reason.
Both paths run the same init(): environment extensions load first, then any WebGLLoader/WebGPULoader/CanvasLoader extensions registered for that backend are awaited, and only then are the renderer's systems and pipes created. See the extensions guide for registering a loader.
Call render() with a Container to draw it to the screen:
import { Container } from 'pixi.js';
const container = new Container();
renderer.render(container);
You can also pass an options object for more control:
import { Matrix } from 'pixi.js';
renderer.render({
container: myContainer,
clear: true,
transform: new Matrix(),
});
The container property is the scene root to draw. target is a separate property that specifies a render destination (e.g., a RenderTexture).
renderer.resize(window.innerWidth, window.innerHeight);
Create textures from any display object with generateTexture():
import { Sprite } from 'pixi.js';
const sprite = new Sprite();
const texture = renderer.generateTexture(sprite);
When rendering to a texture-backed target, you can specify mipLevel to render into a specific mip level of the target's underlying texture storage. Most applications won't need this; it's useful for custom LOD (level of detail) systems or manual mipmap generation.
import { RenderTexture } from 'pixi.js';
const rt = RenderTexture.create({
width: 256,
height: 256,
mipLevelCount: 4,
autoGenerateMipmaps: false,
});
// Render into mip 1 (128x128)
renderer.render({
container,
target: rt,
mipLevel: 1,
});
If your target is a Texture with a frame (e.g. an atlas sub-texture), that frame is interpreted in mip 0 pixel space and is scaled/clamped when rendering to mipLevel > 0.
layer picks which part of a layered texture to render into: an array layer of a 2D array (arrayLayerCount), a face of a cube map, or a depth slice of a 3D texture (depth). Pass the TextureSource itself as target; RenderTexture.create doesn't take depth.
import { TextureSource } from 'pixi.js';
const volume = new TextureSource({ width: 64, height: 64, depth: 4, format: 'rgba8unorm' });
for (let z = 0; z < 4; z++) {
renderer.render({ container, target: volume, layer: z, clear: true });
}
Custom render code can bind a layer the same way with renderer.renderTarget.push({ target, layer }).
By default a texture render is stored in PixiJS's Y-down orientation, which the 2D pipeline samples upright but 3D UV conventions read upside down. Pass flipY: true to invert the Y orientation of the render. Back-face culling stays correct because the winding order flips together with the projection.
renderer.render({
container: scene3d,
target: renderTexture,
flipY: true,
});
Every texture you render to gets a RenderTarget behind the scenes. Create one yourself when you need multiple color attachments, an explicit depth or stencil texture, or per-attachment load and store behavior.
import { RenderTarget, TextureSource } from 'pixi.js';
const color = new TextureSource({ width: 512, height: 512 });
const depth = new TextureSource({ width: 512, height: 512, format: 'depth24plus-stencil8' });
const target = new RenderTarget({
colorAttachments: [{ texture: color, loadOp: 'clear', clearValue: [0, 0, 0, 1] }],
depthStencilAttachment: { texture: depth, depthLoadOp: 'clear', depthClearValue: 1 },
});
renderer.render({ container, target });
The attachment objects mirror the WebGPU render pass descriptors, with texture in place of view. The clear option you pass to render() overrides the attachments' load ops for that call. The older colorTextures, depth, stencil, and depthStencilTexture options still work and are converted to attachments internally.
Pass colorTextures: 0 with depth: true, or hand a depth-format TextureSource to depthStencilTexture. Rendering directly to a depth-format TextureSource also works; PixiJS wraps it in a depth-only target.
const shadowMap = new RenderTarget({ width: 1024, height: 1024, colorTextures: 0, depth: true });
Supported depth and stencil formats are stencil8, depth16unorm, depth24plus, depth24plus-stencil8, depth32float, and depth32float-stencil8. A depth-only format cannot be used for stencil masks.
Custom rendering code binds surfaces through renderer.renderTarget. Pass an options object; the positional form is deprecated since 8.20.0 and warns once.
import { CLEAR } from 'pixi.js';
// bind: replaces the current binding
renderer.renderTarget.bind({ target: renderTexture, clear: true, clearColor: [0, 0, 0, 0] });
// push/pop: save and restore the previous binding
renderer.renderTarget.push({ target: scratch, clear: CLEAR.COLOR, mipLevel: 1 });
// ... draw ...
renderer.renderTarget.pop(); // returns the restored RenderTarget, throws if the stack is empty
// capture and replay a binding without clearing it
const saved = renderer.renderTarget.getBindState();
renderer.renderTarget.bind({ target: scratch, clear: true });
renderer.renderTarget.bind(saved);
Available options are target, clear, clearColor, frame (in mip 0 pixel space), mipLevel, layer, and flipY.
// copy color pixels from any texture, canvas, or render target into a texture
renderer.renderTarget.copyToTexture(source, destTexture, { x: 0, y: 0 }, { width: 256, height: 256 }, { x: 0, y: 0 });
// copy the depth attachment into a depth-format texture (WebGL2 and WebGPU)
renderer.renderTarget.copyDepthTexture(sourceTarget, destDepthTexture, { x: 0, y: 0 }, { width: 256, height: 256 });
// then render into the destination without clearing the copied depth
renderer.render({ container, target: destTarget, clear: CLEAR.COLOR });
copyDepthTexture warns and does nothing when the source has no depth attachment or the destination texture is not a depth or stencil format. Clear only the color buffer afterwards, or the copied depth is lost.
3D code that needs the resolved winding of the current target can call renderer.renderTarget.isFrontFaceInverted(), or isFrontFaceInverted(target, flipY) to ask about a target before binding it. The target is a RenderTarget; get one for a texture with renderer.renderTarget.getRenderTarget(texture).
A RenderTarget you construct is yours to destroy. Every renderer that drew into it frees the framebuffers and MSAA textures it built for it. Destroying the renderer frees those too, without destroying your target.
import { RenderTarget } from 'pixi.js';
const target = new RenderTarget({ colorTextures: [texture] });
renderer.render({ container, target });
target.destroy(); // the GPU objects built for it go with it
These have no effect on the WebGL renderer. Branch on renderer.name === 'webgpu' before relying on them.
WGSL override declarations can be set per shader without recompiling the source. Values are baked into the pipeline, so each distinct set of overrides creates a separate pipeline. Keep the number of combinations small.
import { Shader } from 'pixi.js';
const shader = Shader.from({
gpu: { vertex: { source, entryPoint: 'vsMain' }, fragment: { source, entryPoint: 'fsMain' } },
resources: { uniforms },
overrides: { BLUR_STEPS: 8 },
});
Browsers without pipeline constant support (Safari) get the values substituted into the source instead. renderer.limits.supportsOverrideConstants reports which path is in use.
Set storage: true on a TextureSource so your own compute pass can write to it. PixiJS adds GPUTextureUsage.STORAGE_BINDING to the texture and still samples it like any other texture.
import { TextureSource } from 'pixi.js';
const volume = new TextureSource({ width: 64, height: 64, depth: 64, format: 'rgba8unorm', storage: true });
// bind this view to a texture_storage_3d<rgba8unorm, write> in your compute pass
const view = renderer.texture.getGpuSource(volume).createView();
Every device accepts rgba8unorm, rgba16float, r32float, rg32float, rgba32float and the matching integer formats as storage textures. bgra8unorm needs the bgra8unorm-storage feature, and formats such as r8unorm or r16float need texture-formats-tier1. PixiJS enables both features when the GPU has them. WebGPU rejects any other format when the texture is created.
A 3D texture with autoGenerateMipmaps needs storage: true and the rgba8unorm or rgba16float format on WebGPU, because a compute shader writes its mips.
A render bundle records a sequence of draw calls once and replays them on later frames, cutting CPU cost for static content drawn through renderer.encoder. A bundle bakes the render target it was recorded against, so check it before replaying and re-record when the check fails.
let bundle;
if (!bundle || !renderer.encoder.isBundleValid(bundle)) {
renderer.encoder.beginBundle('static-props');
renderer.encoder.draw({ geometry, shader, state });
bundle = renderer.encoder.endBundle();
}
renderer.encoder.executeBundle(bundle);
Pass an array to executeBundle to replay several bundles in one call. A bundle is also invalid after a WebGPU device loss, because it was recorded on the device that was lost; isBundleValid reports that too.
On a tile-based GPU (every phone GPU and Apple silicon, reported by renderer.device.extensions.tileBased), antialiased targets never write their multisample colour buffer to memory. Only the resolved image is kept. When a pass reopens a target, for example a filter popping back onto its parent or a render with clear: false, PixiJS copies the resolved image back into the multisample buffer before drawing. This saves bandwidth on every frame, and the multisample buffer may not be allocated at all where the browser supports GPUTextureUsage.TRANSIENT_ATTACHMENT. You don't need to set anything.
Restoring writes the resolved colour into every sample, so antialiased edges that meet exactly across a reopen, such as two shapes drawn by separate clear: false renders, can show a faint seam. On an antialiased canvas, a frame that starts without clearing (clearBeforeRender: false, or a first render with clear: false) starts from an empty canvas rather than the previous frame, as it already does without antialiasing.
Other GPUs (Intel, NVIDIA, AMD) keep multisample buffers in video memory, where storing them and loading them back on a reopen is cheaper than restoring, so PixiJS does that there.
The multisample depth/stencil buffer is kept by default, because masks need it across a reopen. A render texture that is drawn in a single pass and never reopened can discard it too, along with its colour buffer on GPUs that aren't tile-based:
import { RenderTexture } from 'pixi.js';
const rt = RenderTexture.create({ width: 1024, height: 1024, antialias: true, transient: true });
The same flag works for the canvas. Pass transient: true to the renderer when the app never reopens the screen pass while it still needs depth or stencil:
await app.init({ preference: 'webgpu', antialias: true, transient: true });
renderer.device.extensions.transientAttachment reports whether the usage bit is available.
When the browser reports the GPU device as lost (a GPU process crash, for example), the WebGPU renderer requests a new device and rebuilds textures, buffers, pipelines, and bind groups on the next render. You do not need to handle it yourself, with one exception: a render bundle recorded on the old device fails renderer.encoder.isBundleValid(bundle) and must be re-recorded. A device you hand in through the gpu option is neither restored nor destroyed by PixiJS. The WebGL renderer restores itself the same way after webglcontextrestored.
When mixing PixiJS with other WebGL/WebGPU libraries (e.g., Three.js), each library may leave GPU state (bound textures, blend modes, active shaders) that conflicts with the other. Call resetState() before each library renders to avoid visual glitches, missing objects, or incorrect blending:
function render() {
threeRenderer.resetState();
threeRenderer.render(scene, camera);
pixiRenderer.resetState();
pixiRenderer.render({ container: stage });
requestAnimationFrame(render);
}
requestAnimationFrame(render);
Call destroy() to clean up all GPU resources, systems, event listeners, and internal state:
renderer.destroy();
This removes all EventEmitter listeners attached to the renderer and nullifies internal systems and pipes. On WebGPU it also destroys the GPUDevice the renderer created (a device passed in through the gpu option is left alone). A destroyed renderer cannot be used for further rendering.