Custom Fragment Shader Filter in Pixi.js

This guide explains the step-by-step process of creating and implementing a custom fragment shader filter in Pixi.js. You will learn how to write the GLSL fragment code, define and update custom uniforms for dynamic effects, instantiate the filter within the Pixi.js framework, and apply it directly to your display objects to achieve unique visual rendering.

Step 1: Write the GLSL Fragment Shader

A fragment shader is written in GLSL (OpenGL Shading Language) and determines the color of each pixel. While Pixi.js provides a default vertex shader, you must provide the custom fragment shader.

Below is a standard GLSL ES 100 fragment shader that creates a simple animated wave distortion effect:

varying vec2 vTextureCoord; // The coordinates of the texture
uniform sampler2D uSampler; // The input texture (your sprite/image)
uniform float uTime;        // Custom uniform to animate the effect over time

void main(void) {
    vec2 uv = vTextureCoord;
    
    // Apply a sine wave distortion to the X coordinate based on Y and Time
    uv.x += sin(uv.y * 10.0 + uTime) * 0.01;
    
    // Output the final pixel color
    gl_FragColor = texture2D(uSampler, uv);
}

Step 2: Define the Filter in JavaScript

To use the shader in Pixi.js, instantiate a PIXI.Filter object. You will pass null or undefined as the first argument to use the default vertex shader, followed by your custom fragment shader string as the second argument, and an object containing your custom uniforms as the third.

// Store the GLSL code in a template literal string
const fragmentShaderSource = `
    varying vec2 vTextureCoord;
    uniform sampler2D uSampler;
    uniform float uTime;

    void main(void) {
        vec2 uv = vTextureCoord;
        uv.x += sin(uv.y * 10.0 + uTime) * 0.01;
        gl_FragColor = texture2D(uSampler, uv);
    }
`;

// Define the uniforms with their starting values
const uniforms = {
    uTime: 0.0
};

// Create the custom filter
const waveFilter = new PIXI.Filter(null, fragmentShaderSource, uniforms);

Step 3: Apply the Filter to a Display Object

Filters in Pixi.js can be applied to any display object (such as a Sprite, Container, or Graphics object) by assigning them to the .filters array of that object.

// Load a texture and create a sprite
const sprite = PIXI.Sprite.from('path/to/image.jpg');

// Apply the custom filter to the sprite
sprite.filters = [waveFilter];

// Add the sprite to your PixiJS stage
app.stage.addChild(sprite);

Step 4: Animate the Uniforms

To make the filter dynamic and interactive, you must continuously update the uniforms inside the animation loop. Pixi.js provides an internal ticker that makes this process straightforward.

// Add a function to the PixiJS ticker to update the uTime uniform every frame
app.ticker.add((delta) => {
    // Increment the time uniform based on the delta time
    waveFilter.uniforms.uTime += 0.05 * delta;
});