pixijs-custom-rendering — Claude Code skill for PixiJS v8 shaders
Guides your agent through custom shaders, uniforms, filters and batchers in PixiJS v8, in GLSL and WGSL, for WebGL and WebGPU.
npx skills add pixijs/pixijs-skills --skill pixijs-custom-rendering -a claude-codeWhat pixijs-custom-rendering does
The skill kicks in when you write low-level draw code in PixiJS v8: shaders, uniforms, filters, batchers, draw instructions. It covers binding GLSL and WGSL programs to scene objects with Shader.from({ gl, gpu, resources }), how GlProgram and GpuProgram behave, and how one shader can target both renderers.
Inside you get typed UniformGroup usage with types like f32, vec2 and mat4x4<f32>, UBO mode, textures passed as separate resources, TextureView for depth sampling, 3D and array textures, WebGPU storage textures, WGSL override constants, custom bind group layouts via gpuLayout, partial buffer updates and TexturePool. There is also a custom Filter through Filter.from, GLSL ES 3.0 conventions with finalColor, uBackTexture sampling, and custom Batcher plus InstructionPipe.destroyInstructionSet wired through extensions.
One gotcha worth knowing before you design around UBOs: the sync path uses new Function, so under a strict CSP without unsafe-eval UBO-backed shaders — and therefore WebGPU — throw on first use. Importing pixi.js/unsafe-eval once at startup swaps in the fallback. Also note that supplying only gl limits the shader to WebGL, and only gpu limits it to WebGPU; the compatibleRenderers bitmask is set for you.
What it helps with
- write a shader that runs on both WebGL and WebGPU
- declare typed uniforms with
UniformGroup - build a custom filter via
Filter.fromwithout a vertex shader - load a 3D or array texture and sample depth
- add a custom batcher and clean up its instruction set
Example prompts for the agent
- Write a PixiJS v8 custom shader with a uTime uniform of type f32 and attach it to a Mesh.
- Create a filter with Filter.from that inverts color using a uStrength uniform.
- Show me how to set up UBO mode in UniformGroup and what to watch for under strict CSP.
How to install pixijs-custom-rendering
1Skills CLI (Vercel)
npx skills add pixijs/pixijs-skills --skill pixijs-custom-rendering -a claude-code2Manually: put the files in place
~/.claude/skills/pixijs-custom-rendering/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/pixijs-custom-rendering/
1Skills CLI (Vercel)
npx skills add pixijs/pixijs-skills --skill pixijs-custom-rendering -a codex2Manually: put the files in place
.agents/skills/pixijs-custom-rendering/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add pixijs/pixijs-skills --skill pixijs-custom-rendering -a cursor2Manually: put the files in place
.cursor/skills/pixijs-custom-rendering/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install pixijs/pixijs-skills pixijs-custom-rendering2Manually: put the files in place
.github/skills/pixijs-custom-rendering/SKILL.md1Skills CLI (Vercel)
npx skills add pixijs/pixijs-skills --skill pixijs-custom-rendering -a gemini-cli2Platform CLI
gemini skills install https://github.com/pixijs/pixijs-skills --path skills/pixijs-custom-renderingManually: put the files in place
.devin/skills/pixijs-custom-rendering/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add pixijs/pixijs-skills --skill pixijs-custom-rendering -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about pixijs-custom-rendering
How do I install pixijs-custom-rendering in Claude?
Run npx skills add pixijs/pixijs-skills --skill pixijs-custom-rendering -a claude-code. The skill lands at ~/.claude/skills/pixijs-custom-rendering/SKILL.md.
What does pixijs-custom-rendering do?
It tells your agent how to write custom shaders, uniforms, filters and batchers in PixiJS v8, from Shader.from and UniformGroup to WebGPU storage textures and custom bind group layouts.
Why do UBO shaders fail under a strict CSP?
UBO sync relies on new Function. In an environment without unsafe-eval, import pixi.js/unsafe-eval once at startup to switch to the fallback sync path.
Source text
SKILL.md · MIT
---
name: pixijs-custom-rendering
description: "Use this skill when writing custom shaders, uniforms, filters, batchers, or low-level draw code in PixiJS v8. Covers Shader.from({gl, gpu, resources}), GlProgram/GpuProgram, UniformGroup with typed uniforms (f32, vec2, mat4x4), UBO mode, textures as resources, TextureView for depth sampling, 3D and array textures (depth, arrayLayerCount, sampler3D, texture_3d), WebGPU storage textures (storage: true), WGSL override constants, custom bind group layouts (gpuLayout), partial buffer updates (Buffer.update), Geometry.vertexCount/indexCount, TexturePool scratch textures, State winding and culling (clockwiseFrontFace, cullMode), WebGPU render bundles, custom Filter via Filter.from, GLSL ES 3.0 conventions (finalColor), uBackTexture sampling, pixi.js/unsafe-eval for strict CSP, custom Batcher and InstructionPipe.destroyInstructionSet via extensions. Triggers on: Shader, GLSL, WGSL, uniform, custom shader, blendRequired, ShaderOverrides, bind group layout, depth texture, 3D texture, sampler3D, texture_3d, storage texture, getSize, indexCount, TexturePool, getOptimalTexture, destroyInstructionSet, beginBundle, executeBundle, RenderBundle."
license: MIT
---
Custom shaders bind GLSL and WGSL programs to scene objects via `Shader.from({ gl, gpu, resources })`. Uniforms live in typed `UniformGroup`s, textures are passed as separate resources, and the same shader can target both WebGL and WebGPU.
## Quick Start
```ts
const uniforms = new UniformGroup({
uTime: { value: 0, type: "f32" },
});
const shader = Shader.from({
gl: { vertex: vertexSrc, fragment: fragmentSrc },
resources: { uniforms },
});
const geometry = new MeshGeometry({
positions: new Float32Array([0, 0, 100, 0, 100, 100, 0, 100]),
uvs: new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]),
indices: new Uint32Array([0, 1, 2, 0, 2, 3]),
});
const mesh = new Mesh({ geometry, shader });
app.stage.addChild(mesh);
app.ticker.add(() => {
shader.resources.uniforms.uniforms.uTime = performance.now() / 1000;
});
```
**Related skills:** `pixijs-filters` (built-in filters), `pixijs-scene-mesh` (custom geometry), `pixijs-performance` (batch optimization), `pixijs-migration-v8` (shader API migration from v7).
## Core Patterns
### Dual-renderer shader (WebGL + WebGPU)
```ts
import { Shader, GlProgram, GpuProgram, UniformGroup } from "pixi.js";
const glVertex = `...`; // GLSL vertex (write `#version 300 es` yourself if you want WebGL2/GLSL ES 3.0)
const glFragment = `...`; // GLSL fragment
const wgslSource = `...`; // WGSL combined
const shader = Shader.from({
gl: { vertex: glVertex, fragment: glFragment },
gpu: {
// entryPoint names are arbitrary; they must match the @vertex / @fragment
// function names in your WGSL source. PixiJS ships examples using
// 'mainVert' / 'mainFrag' but `main` is equally valid.
vertex: { entryPoint: "mainVert", source: wgslSource },
fragment: { entryPoint: "mainFrag", source: wgslSource },
},
resources: {
myUniforms: new UniformGroup({
uColor: { value: new Float32Array([1, 0, 0, 1]), type: "vec4<f32>" },
uMatrix: { value: new Float32Array(16), type: "mat4x4<f32>" },
}),
},
});
```
If only `gl` is provided, the shader works with WebGL only. If only `gpu` is provided, it works with WebGPU only. The `compatibleRenderers` bitmask is set automatically.
`GlProgram` does **not** auto-inject `#version 300 es`. If you write `#version 300 es` yourself, PixiJS preserves it and treats the shader as GLSL ES 3.0; otherwise it injects WebGL1 compat macros (`#define in varying`, `#define texture texture2D`) and runs the shader as WebGL1-style GLSL. `GlProgram` always injects a default precision (`highp` vertex, `mediump` fragment) and the program name. It also declares `highp` for sampler types GLSL ES 3.0 gives no default (`usampler2D`, `isampler2D`, `sampler2DArray`, `sampler3D`, and others), so you don't need a precision line for them; one you write yourself is kept. `sampler2D` and `samplerCube` keep their `lowp` default. For GLSL ES 3.0, use `in`/`out` instead of `attribute`/`varying`, `texture()` instead of `texture2D()`, and an `out vec4` instead of `gl_FragColor`.
### Textures as resources
Textures are resources, not uniforms. Pass the texture's `source` and `style` separately:
```ts
import { Shader, UniformGroup, Texture, Assets } from "pixi.js";
const texture = await Assets.load("myImage.png");
const shader = Shader.from({
gl: { vertex: vertSrc, fragment: fragSrc },
resources: {
uTexture: texture.source,
uSampler: texture.source.style,
myUniforms: new UniformGroup({
uAlpha: { value: 1.0, type: "f32" },
}),
},
});
// Swap texture at runtime
shader.resources.uTexture = otherTexture.source;
```
Resources are a flat key-value map. The key must match the uniform/binding name in the shader source.
Resources can also be plain objects (auto-wrapped into `UniformGroup`):
```ts
const shader = Shader.from({
gl: { vertex: vertSrc, fragment: fragSrc },
resources: {
myUniforms: {
uTime: { value: 0, type: "f32" },
},
},
});
```
### UBO mode (Uniform Buffer Objects)
UBO mode packs uniforms into a single GPU buffer. Required for WebGPU; optional (WebGL2+) for WebGL.
```ts
import { UniformGroup } from "pixi.js";
const ubo = new UniformGroup(
{
uProjection: { value: new Float32Array(16), type: "mat4x4<f32>" },
uAlpha: { value: 1.0, type: "f32" },
},
{ ubo: true, isStatic: true },
);
// Must call update() manually when isStatic is true
ubo.uniforms.uAlpha = 0.5;
ubo.update();
```
UBO rules:
- Only `f32` and `i32` based types are supported (no `u32`). Matrices are float-only.
- Samplers/textures cannot go in a UBO.
- The UniformGroup name in resources must exactly match the UBO block name in the shader.
- Structure and order must exactly match the shader layout.
- UBO sync uses `new Function` under the hood. In strict-CSP environments (no `unsafe-eval`), import `pixi.js/unsafe-eval` once at startup to swap in the fallback sync path; without it, UBO-backed shaders (and therefore WebGPU) will throw on first use.
### Custom filter
`Filter.from({ gl, resources })` is the shorthand. Pass only a fragment shader; PixiJS supplies a default vertex shader that handles output frame positioning.
```ts
import { Filter } from "pixi.js";
const filter = Filter.from({
gl: {
fragment: `
in vec2 vTextureCoord;
out vec4 finalColor;
uniform sampler2D uTexture;
uniform float uStrength;
void main(void) {
vec4 color = texture(uTexture, vTextureCoord);
finalColor = mix(color, vec4(1.0 - color.rgb, color.a), uStrength);
}
`,
},
resources: {
filterUniforms: {
uStrength: { value: 0.5, type: "f32" },
},
},
});
filter.resources.filterUniforms.uniforms.uStrength = 1.0;
```
For a custom vertex shader, use `new Filter({ glProgram: new GlProgram({ vertex, fragment }), resources })`.
#### Filter shader conventions (GLSL ES 3.0)
- `in vec2 vTextureCoord;` instead of `varying vec2 vTextureCoord;`
- `out vec4 finalColor;` instead of `gl_FragColor`
- `texture(uTexture, uv)` instead of `texture2D(uTexture, uv)`
- The default vertex shader exposes `uInputSize`, `uOutputFrame`, `uOutputTexture` and helpers `filterVertexPosition()` / `filterTextureCoord()`
#### Sampling the render target behind the filter
Set `blendRequired: true` and sample `uBackTexture` in the fragment shader. PixiJS copies the destination pixels into that uniform before running the filter:
```ts
const blendFilter = Filter.from({
gl: { fragment: blendFragSrc },
resources: { uniforms: { uAmount: { value: 0.5, type: "f32" } } },
blendRequired: true,
});
```
Only enable `blendRequired` when you need it; it forces an extra GPU copy every frame.
### Updating uniforms at runtime
```ts
// Access the UniformGroup via resources
shader.resources.myUniforms.uniforms.uTime = performance.now() / 1000;
// For isStatic UBOs, call update() after changing values
shader.resources.myUniforms.update();
```
### Advanced GPU features
See [references/advanced-gpu.md](references/advanced-gpu.md) for full samples of:
- **Partial buffer and texture uploads**: `buffer.update(sizeInBytes, offsetInBytes)` uploads only the changed byte range; `bufferImageSource.update(startTexel, endTexel)` uploads only a texel range of a data texture.
- **Vertex and index counts, winding, culling**: `geometry.vertexCount` (replaces the deprecated `getSize()`), `geometry.indexCount` to draw a prefix of a shared index buffer, `state.clockwiseFrontFace` and `cullMode`.
- **WGSL override constants** (WebGPU): `Shader.from({ gpu, resources, overrides: { STEPS: 8 } })`; each distinct set compiles its own pipeline.
- **Custom bind group layouts** (WebGPU): generate the default with `generateGpuLayoutGroups(extractStructAndGroups(source))`, edit it, pass it as `gpuLayout`.
- **Depth sampling with `TextureView`** (WebGPU): bind `new TextureView(depth, { aspect: "depth-only" })` as a resource while the target's depth attachment is `depthReadOnly`.
- **Integer textures**: `BufferImageSource` with `rgba32uint` etc., `usampler2D`/`texelFetch` and `texture_2d<u32>`/`textureLoad`, `scaleMode: "nearest"`.
- **3D and array textures**: `depth` or `arrayLayerCount` on a `TextureSource`/`BufferImageSource`, sampled with `sampler3D`/`texture_3d<f32>` or `sampler2DArray`/`texture_2d_array<f32>`; they upload whole, and `renderer.render({ container, target: source, layer })` draws into one slice or layer.
- **Storage textures** (WebGPU): `storage: true` on a `TextureSource`, then `renderer.texture.getGpuSource(source).createView()` to write it from your own compute pass.
- **Render bundles** (WebGPU): record draws once with `encoder.beginBundle()` / `endBundle()` and replay with `executeBundle()` while `isBundleValid()` holds.
- **Pooled scratch textures**: `TexturePool.getOptimalTexture({ width, height, resolution, antialias })` and `returnTexture()`.
### Uniform type reference
See [references/uniform-types.md](references/uniform-types.md) for the complete table of supported types, their WGSL/GLSL equivalents, and value formats.
### Custom Batcher (extension-based)
The `Batcher` abstract class enables custom batching for specialized rendering. Subclass it and register via extensions:
```ts
import { Batcher, extensions, ExtensionType } from "pixi.js";
import type {
BatcherOptions,
BatchableMeshElement,
BatchableQuadElement,
Geometry,
Shader,
} from "pixi.js";
class MyBatcher extends Batcher {
public static extension = {
type: [ExtensionType.Batcher],
name: "my-batcher",
};
public name = "my-batcher";
protected vertexSize = 6; // floats per vertex
public geometry: Geometry;
public shader: Shader;
constructor(options: BatcherOptions) {
super(options);
// Initialize geometry and shader
}
public packAttributes(
element: BatchableMeshElement,
float32View: Float32Array,
uint32View: Uint32Array,
index: number,
textureId: number,
): void {
// Pack mesh vertex attributes into the batch buffer
}
public packQuadAttributes(
element: BatchableQuadElement,
float32View: Float32Array,
uint32View: Uint32Array,
index: number,
textureId: number,
): void {
// Pack quad vertex attributes into the batch buffer
}
}
extensions.add(MyBatcher);
```
Elements reference the batcher by `batcherName`. The `BatchableElement` interface requires: `batcherName`, `texture`, `blendMode`, `indexSize`, `attributeSize`, `topology`, and `packAsQuad`.
A custom `InstructionPipe` that caches state per `InstructionSet` (the way `BatcherPipe` keeps its batchers) should implement `destroyInstructionSet(instructionSet)`. The renderer calls it when the owning render group is destroyed, so the cached GPU objects are released with it instead of leaking.
```ts
public destroyInstructionSet(instructionSet: InstructionSet): void {
this._cache[instructionSet.uid]?.destroy();
delete this._cache[instructionSet.uid];
}
```
## Common Mistakes
### [CRITICAL] Old Shader.from(vertex, fragment, uniforms) constructor
Wrong:
```ts
const shader = Shader.from(vertex, fragment, { uTime: 1 });
```
Correct:
```ts
const shader = Shader.from({
gl: { vertex, fragment },
resources: {
uniforms: new UniformGroup({
uTime: { value: 1, type: "f32" },
}),
},
});
```
v8 requires an options object with `gl`/`gpu` programs and `resources`. The positional API was removed.
### [CRITICAL] UniformGroup without type annotation
Wrong:
```ts
new UniformGroup({ uTime: 1 });
```
Correct:
```ts
new UniformGroup({ uTime: { value: 1, type: "f32" } });
```
Every uniform requires an explicit `{ value, type }` pair. Omitting the type causes a runtime error: "Uniform type undefined is not supported."
### [HIGH] UBO with unsupported types or wrong structure
UBO mode supports `f32` and `i32` based types (scalars and vectors). `u32` is not in the supported `UniformGroup` type list and will throw. Matrices are float-only (`mat*<f32>`). Samplers cannot be placed in UBOs.
The struct name and field order must exactly match the shader's UBO declaration. Mismatches produce garbled rendering with no error.
### [HIGH] Putting textures in UniformGroup
Wrong:
```ts
new UniformGroup({
uTexture: { value: texture, type: "f32" },
});
```
Correct:
```ts
const shader = Shader.from({
gl: { vertex, fragment },
resources: {
uTexture: texture.source,
uSampler: texture.source.style,
myUniforms: new UniformGroup({
uAlpha: { value: 1.0, type: "f32" },
}),
},
});
```
Textures are resources, not uniforms. Pass `texture.source` (TextureSource) and `texture.source.style` (TextureStyle) as top-level resource entries.
### [HIGH] Replaying a render bundle against a different render target
Wrong:
```ts
const bundle = encoder.endBundle();
// later, on every frame, regardless of what is bound
encoder.executeBundle(bundle);
```
Correct:
```ts
if (!bundle || !encoder.isBundleValid(bundle)) {
bundle = record();
}
encoder.executeBundle(bundle);
```
A bundle bakes the attachments and winding of the pass it was recorded in, and the device it was recorded on. Replaying it under a different target either fails WebGPU validation for the whole frame or silently renders inside out; replaying it after a device loss is rejected outright.
### [MEDIUM] Reading Geometry.getSize()
`geometry.getSize()` is deprecated since 8.20.0 and logs a warning. Use `geometry.vertexCount`, which is cached and only recomputed when buffers or attributes change.
## API Reference
- [Shader](https://pixijs.download/release/docs/rendering.Shader.html.md)
- [GlProgram](https://pixijs.download/release/docs/rendering.GlProgram.html.md)
- [GpuProgram](https://pixijs.download/release/docs/rendering.GpuProgram.html.md)
- [UniformGroup](https://pixijs.download/release/docs/rendering.UniformGroup.html.md)
- [Filter](https://pixijs.download/release/docs/filters.Filter.html.md)
- [Batcher](https://pixijs.download/release/docs/rendering.Batcher.html.md)
- [BatcherPipe](https://pixijs.download/release/docs/rendering.BatcherPipe.html.md)
- [ShaderOverrides](https://pixijs.download/release/docs/rendering.ShaderOverrides.html.md)
- [TextureView](https://pixijs.download/release/docs/rendering.TextureView.html.md)
- [RenderTarget](https://pixijs.download/release/docs/rendering.RenderTarget.html.md)
- [GpuEncoderSystem](https://pixijs.download/release/docs/rendering.GpuEncoderSystem.html.md)
- [RenderBundle](https://pixijs.download/release/docs/rendering.RenderBundle.html.md)
- [Geometry](https://pixijs.download/release/docs/rendering.Geometry.html.md)
- [State](https://pixijs.download/release/docs/rendering.State.html.md)
© pixijs · MIT · pixijs/pixijs-skills