Custom Filters with Pixi.js using GLSL Shaders

NOTE: This applies to Pixi v3

Pixi.js is Mat Groves’ lightning fast 2D rendering engine for the web utilizing WebGL or 2D Canvas. When used with WebGL it supports filters, which are basically a simple way to leverage specific GLSL shaders. We can even write our own filters for Pixi.js using GLSL and javascript.

To create your own GLSL filter download the source on github and follow the instructions to build, running ‘npm install’ and then ‘gulp’ from the command line. All the filters live in the ‘src/filters/’ directory. To create your own filter you’ll need to add a new directory for your filter name which will include at minimum two files. First, the javascript file that contains the browserify module for your filter. Second, the glsl fragment shader with the suffix ‘.frag’.

Below is an example of a custom filter module I used for Signed Distance Fields, a way to visually vectorize specially created raster images. I’ll walk through the different parts of the code.

var core = require('../../core');
// @see https://github.com/substack/brfs/issues/25
var fs = require('fs');
/**
 * This applies a filter to vectorize Sined Distance Field Images.
 *
 * @class
 * @extends AbstractFilter
 * @memberof PIXI.filters
 */
function SDFFilter()
{
    core.AbstractFilter.call(this,
        // vertex shader
        null,
        // fragment shader
        fs.readFileSync(__dirname + '/sdf.frag', 'utf8'),
        // custom uniforms
        {
            threshStart: {type: '1f', value: 0.5},
            threshEnd: {type: '1f', value: 0.5},
            color: {type: '4f', value: [0.0, 0.0, 0.0, 1.0]},
        }
    );
}
SDFFilter.prototype = Object.create(core.AbstractFilter.prototype);
SDFFilter.prototype.constructor = SDFFilter;
module.exports = SDFFilter;
Object.defineProperties(SDFFilter.prototype, {
    color: {
        get: function ()
        {
            return this.uniforms.color.value;
        },
        set: function (value)
        {
            this.uniforms.color.value = value;
            this.syncUniform(this.uniforms);
        }
    },
        threshStart: {
        get: function ()
        {
            return this.uniforms.threshStart.value;
        },
        set: function (value)
        {
            this.uniforms.threshStart.value = value;
            this.syncUniform(this.uniforms);
        }
    },
        threshEnd: {
        get: function ()
        {
            return this.uniforms.threshEnd.value;
        },
        set: function (value)
        {
            this.uniforms.threshEnd.value = value;
            this.syncUniform(this.uniforms);
        }
    }
});

So the first thing we do is include necessary modules, including core and fs. Then we create our filter class. This is a subclass of AbstractFilter so we are calling the superclass passing in a reference to this shader, optionally a vertex shader (in this case we have none, so it is null), our fragment shader (this is the ‘.frag’ file that will contain our glsl fragment shader),

        fs.readFileSync(__dirname + '/sdf.frag', 'utf8'),

and our custom uniforms (properties that will change).

{
    threshStart: {type: '1f', value: 0.5},
    threshEnd: {type: '1f', value: 0.5},
    color: {type: '4f', value: [0.0, 0.0, 0.0, 1.0]},
}

It’s important to note that the different types passed into the uniforms require certain values associated with them, for example a vec4 will be an array of 4 points. It’s important you use the correct value with the right type so you may want to find an example filter that uses the same type you’re using for an example.

The next bit of code just inherits properties from the superclass, defines our constructor and exports the module. After that we define the properties of our filter. These map to the uniforms we were passing in and each is an object with a get and set method that respective get and set the uniform value.

We then need to create our fragment shader. This involves understanding the basics of glsl, but generally what you need to know is that the uniform values are then reflected in the shader as you defined them.

precision mediump float;
varying vec2 vTextureCoord;
uniform float threshStart;
uniform float threshEnd;
uniform vec4 color;
uniform sampler2D uSampler;
void main(void)
{
    vec4 sample = texture2D(uSampler, vTextureCoord);
    gl_FragColor = color * smoothstep(threshStart, threshEnd, sample.r);
}

After that you’ll need to ensure your shader module is exported in the src/filters/index.js file. And that’s basically it! Now you can create GLSL shaders and apply them to sprites within pixi.js. Also, don’t forget to run gulp to compile your pixi build with your new filter.