pixijs/pixijs-skillsMIT3 files

Pixijs Custom Rendering

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, 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, getSize, indexCount, TexturePool, getOptimalTexture, destroyInstructionSet, beginBundle, executeBundle, RenderBundle.

Specification
Skill ID
pixijs/pixijs-skills/pixijs-custom-rendering
Publisher
pixijs
Repository
pixijs-skills
Installs
0
Files
3
License
MIT
Synced
Sep 23, 2026
How to use it

Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.

pixijs/pixijs-skills/pixijs-custom-renderingInstalls these files
  • SKILL.md
  • references/advanced-gpu.md
  • references/uniform-types.md

What this skill tells the agent

Custom shaders bind GLSL and WGSL programs to scene objects via Shader.from({ gl, gpu, resources }). Uniforms live in typed UniformGroups, textures are passed as separate resources, and the same shader can target both WebGL and WebGPU.

Quick Start

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)

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. 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:

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):

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.

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.

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:

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

// 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 for full samples of:

  • Partial buffer updates: buffer.update(sizeInBytes, offsetInBytes) uploads only the changed byte range.
  • 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.
  • 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

More from pixijs

Use this skill in a project

Open any RiverX project, search the Skills panel for pixijs/pixijs-skills/pixijs-custom-rendering, and install it in one click.

Use this skill