Skip to main content
skill CX Codex

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.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add pixijs/pixijs-skills --skill pixijs-custom-rendering -a claude-code
Open on GitHub

What 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.from without 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-code

2Manually: put the files in place

~/.claude/skills/pixijs-custom-rendering/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/pixijs-custom-rendering/

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

Similar skills

Everything for Game dev and 3D: Unity, Unreal, Godot, Blender →

From the same set26

pixijs-scene-dom-container Teaches the agent how to overlay HTML elements on a PixiJS v8 canvas with DOMContainer and DOMPipe. Useful for native inputs, iframes or video that must follow a display object. CX Codex +1 5,700installs pixijs-blend-modes A skill that tells your agent how to composite PixiJS v8 display objects with blendMode: standard modes, advanced ones, and batch-friendly ordering. CX Codex +1 5,700installs pixijs-color PixiJS v8 skill for the Color class. It guides color conversions, tint calculations, and buffer reuse to prevent GC spikes in animation loops. CX Codex +1 5,700installs pixijs-create The skill walks your agent through scaffolding a PixiJS v8 project or adding the library to an existing one: templates, package managers, TypeScript setup. CX Codex +1 5,700installs pixijs-scene-gif Teaches coding assistants how to load and play animated GIFs in PixiJS v8 with GifSprite. Useful when embedding GIF assets directly into scene trees. CX Codex +1 5,600installs pixijs-environments Guidance for running PixiJS v8 in Web Workers, OffscreenCanvas, Node.js, and CSP-restricted setups. Useful when browser DOM APIs are unavailable or blocked. CX Codex +1 5,600installs pixijs-accessibility Configures screen readers and keyboard navigation in PixiJS v8 apps by rendering a shadow DOM overlay. CX Codex +1 5,500installs pixijs-migration-v8 This skill guides a PixiJS v7 to v8 migration and diagnoses common post-upgrade breakage. Use it when old APIs or imports need replacing. CX Codex +1 5,300installs

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor