$npx -y skills add pixijs/pixijs-skills --skill pixijs-custom-renderingUse this skill when writing custom shaders, uniforms, filters, or batchers in PixiJS v8. Covers Shader.from({gl, gpu, resources}), GlProgram/GpuProgram, UniformGroup with typed uniforms (f32, vec2, mat4x4), UBO mode, textures as resources, custom Filter via Filter.from, GLSL ES 3
| 1 | 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. |
| 2 | |
| 3 | ## Quick Start |
| 4 | |
| 5 | ```ts |
| 6 | const uniforms = new UniformGroup({ |
| 7 | uTime: { value: 0, type: "f32" }, |
| 8 | }); |
| 9 | |
| 10 | const shader = Shader.from({ |
| 11 | gl: { vertex: vertexSrc, fragment: fragmentSrc }, |
| 12 | resources: { uniforms }, |
| 13 | }); |
| 14 | |
| 15 | const geometry = new MeshGeometry({ |
| 16 | positions: new Float32Array([0, 0, 100, 0, 100, 100, 0, 100]), |
| 17 | uvs: new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]), |
| 18 | indices: new Uint32Array([0, 1, 2, 0, 2, 3]), |
| 19 | }); |
| 20 | |
| 21 | const mesh = new Mesh({ geometry, shader }); |
| 22 | app.stage.addChild(mesh); |
| 23 | |
| 24 | app.ticker.add(() => { |
| 25 | shader.resources.uniforms.uniforms.uTime = performance.now() / 1000; |
| 26 | }); |
| 27 | ``` |
| 28 | |
| 29 | **Related skills:** `pixijs-filters` (built-in filters), `pixijs-scene-mesh` (custom geometry), `pixijs-performance` (batch optimization), `pixijs-migration-v8` (shader API migration from v7). |
| 30 | |
| 31 | ## Core Patterns |
| 32 | |
| 33 | ### Dual-renderer shader (WebGL + WebGPU) |
| 34 | |
| 35 | ```ts |
| 36 | import { Shader, GlProgram, GpuProgram, UniformGroup } from "pixi.js"; |
| 37 | |
| 38 | const glVertex = `...`; // GLSL vertex (write `#version 300 es` yourself if you want WebGL2/GLSL ES 3.0) |
| 39 | const glFragment = `...`; // GLSL fragment |
| 40 | const wgslSource = `...`; // WGSL combined |
| 41 | |
| 42 | const shader = Shader.from({ |
| 43 | gl: { vertex: glVertex, fragment: glFragment }, |
| 44 | gpu: { |
| 45 | // entryPoint names are arbitrary; they must match the @vertex / @fragment |
| 46 | // function names in your WGSL source. PixiJS ships examples using |
| 47 | // 'mainVert' / 'mainFrag' but `main` is equally valid. |
| 48 | vertex: { entryPoint: "mainVert", source: wgslSource }, |
| 49 | fragment: { entryPoint: "mainFrag", source: wgslSource }, |
| 50 | }, |
| 51 | resources: { |
| 52 | myUniforms: new UniformGroup({ |
| 53 | uColor: { value: new Float32Array([1, 0, 0, 1]), type: "vec4<f32>" }, |
| 54 | uMatrix: { value: new Float32Array(16), type: "mat4x4<f32>" }, |
| 55 | }), |
| 56 | }, |
| 57 | }); |
| 58 | ``` |
| 59 | |
| 60 | 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. |
| 61 | |
| 62 | `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`. |
| 63 | |
| 64 | ### Textures as resources |
| 65 | |
| 66 | Textures are resources, not uniforms. Pass the texture's `source` and `style` separately: |
| 67 | |
| 68 | ```ts |
| 69 | import { Shader, UniformGroup, Texture, Assets } from "pixi.js"; |
| 70 | |
| 71 | const texture = await Assets.load("myImage.png"); |
| 72 | |
| 73 | const shader = Shader.from({ |
| 74 | gl: { vertex: vertSrc, fragment: fragSrc }, |
| 75 | resources: { |
| 76 | uTexture: texture.source, |
| 77 | uSampler: texture.source.style, |
| 78 | myUniforms: new UniformGroup({ |
| 79 | uAlpha: { value: 1.0, type: "f32" }, |
| 80 | }), |
| 81 | }, |
| 82 | }); |
| 83 | |
| 84 | // Swap texture at runtime |
| 85 | shader.resources.uTexture = otherTexture.source; |
| 86 | ``` |
| 87 | |
| 88 | Resources are a flat key-value map. The key must match the uniform/binding name in the shader source. |
| 89 | |
| 90 | Resources can also be plain objects (auto-wrapped into `UniformGroup`): |
| 91 | |
| 92 | ```ts |
| 93 | const shader = Shader.from({ |
| 94 | gl: { vertex: vertSrc, fragment: fragSrc }, |
| 95 | resources: { |
| 96 | myUniforms: { |
| 97 | uTime: { value: 0, type: "f32" }, |
| 98 | }, |
| 99 | }, |
| 100 | }); |
| 101 | ``` |
| 102 | |
| 103 | ### UBO mode (Uniform Buffer Objects) |
| 104 | |
| 105 | UBO mode packs uniforms into a single GPU buffer. Required for WebGPU; optional (WebGL2+) for WebGL. |
| 106 | |
| 107 | ```ts |
| 108 | import { UniformGroup } from "pixi.js"; |
| 109 | |
| 110 | const ubo = new UniformGroup( |
| 111 | { |
| 112 | uProjection: { value: new Float32Array(16), type: "mat4x4<f32>" }, |
| 113 | uAlpha: { value: 1.0, type: "f32" }, |
| 114 | }, |
| 115 | { ubo: true, isStatic: true }, |
| 116 | ); |
| 117 | |
| 118 | // Must call update() manually when isStatic is true |
| 119 | ubo.uniforms.uAlpha = 0.5; |
| 120 | ubo.update(); |
| 121 | ``` |
| 122 | |
| 123 | UBO rules: |
| 124 | |
| 125 | - Only `f32` and `i32` based types are supported (no `u32`). Matrices are float-only. |
| 126 | - Samplers/textures cannot go in a UBO. |
| 127 | - The UniformGroup name in resources must exactly match the UBO block name i |