Envision, Create, Share

Welcome to HBGames, a leading amateur game development forum and Discord server. All are welcome, and amongst our ranks you will find experts in their field from all aspects of video game design and development.

Custom Filters

Important!
This Tutorial is for my Filters Plugin, it can be applied to PIXI.js in general but some concepts will be specific to my Filters Plugin.
You can find my Filters Plugin here; arpgmaker.com/viewtopic.php?p=930983

IDE Recommendation
As a JS development IDE I highly recommend the multi-platform IDE "Visual Studio Code" (Available for Windows, OS X and Linux). code.visualstudio.com

Below are framework Filters. They are bare-bones with what they do.
First filter is a single-pass "multiplier" which will multiply the RGB components of the input by a fixed number.
Second filter is an example multi-pass filter, which will perform the multiply operation of the first filter twice in separate passes.

Included are an example of a custom Filter with animation and a custom texture (Refract.js) and an example of a custom multi-pass Filter (Retro.js).

Basic Custom Filter
BasicCustomFilter.js
basic_custom { "multiplier" : 1.0 }
Code:
(function() {

    

    // BasicCustomFilter construction function

    BasicCustomFilter = function() {

        PIXI.AbstractFilter.call( this ); // Call parent constructor

        

        this.passes = [this]; // A Filter can be multi-pass (See MultiPassFilter below)

 

        // Uniforms are sent to the GPU shader (See Uniform Values table below)

        this.uniforms = {

            multiplier: { type: '1f', value : 1.0 } // multiplier uniform is a float with a default value of 1.0

        };

 

        // Filters only use the fragment shader (usually)

        // This is the source for the GPU program, performed per-pixel

        // It is a string array that is joined at a later stage before uploading to the GPU

        this.fragmentSrc = [

            'precision mediump float;',       // Precision is the accuracy of the result - lowp is faster but less accurate, highp is slower but more accurate

            'varying vec2 vTextureCoord;',    // DEFAULT - input from MV for the input texture UV

            'varying vec4 vColor;',           // DEFAULT - input from MV from the vertex shader (can be ignored)

            'uniform sampler2D uSampler;',    // DEFAULT - input from MV for the input texture itself

            // A Filters Plugin default input is uTime, which requires a getter/setter to be written (See multiplier getter/setter above for an example)

            'uniform float multiplier;',  // This is our custom uniform value (see above)

 

            // GLSL main entry point

            'void main(void) {',

 

            '   gl_FragColor = texture2D( uSampler, vTextureCoord );', // Read the colour of our source image

            '   gl_FragColor.rgb *= multiplier;', // Do some operation on it (multiply by the multiplier uniform)

            

            '}'

        ];

    }

    

    // Prototype setup

    BasicCustomFilter.prototype = Object.create( BasicCustomFilter.prototype );

    BasicCustomFilter.prototype.constructor = BasicCustomFilter;

    

    // BasicCustomFilter.multiplier getter and setter (notice the use of .value)

    // This is to avoid accessing the uniform directly and lets us do cool things later

    Object.defineProperty( BasicCustomFilter.prototype, 'multiplier', {

        // Getter

        get: function() {

            return this.uniforms.multiplier.value;

        },

        // Setter

        set: function( value ) {

            this.uniforms.multiplier.value = value;

        }

    });

    

    if ( Filter ) {

        // Filters Plugin standard naming is to remove the "Filter" and switch from camel case to lower-case with underscore

        // BasicCustomFilter -> basic_custom

        Filter.add( "basic_custom", BasicCustomFilter );

    }

 

})();

Multi Pass Custom Filter
MultiPassCustomFilter.js
multi_pass_custom { "multiplier" : [1.0, 1.0] }
Code:
(function() {

    

    // MultiPassCustomFilter construction function

    function MultiPassCustomFilter() {

        this.firstFilter = new BasicCustomFilter(); // Create our first Filter instance

        this.secondFilter = new BasicCustomFilter(); // Create our second Filter instance

 

        this.passes = [this.firstFilter, this.secondFilter]; // The passes are set here

    }

    

    // Prototype setup

    MultiPassCustomFilter.prototype = Object.create( MultiPassCustomFilter.prototype );

    MultiPassCustomFilter.prototype.constructor = MultiPassCustomFilter;

    

    // Forward the properties from both our passes to this interface

    Object.defineProperty( MultiPassCustomFilter.prototype, 'multiplier', {

        // Getter

        get: function() {

            // Returns an array containing the first and second multiplier uniforms

            return [ this.firstFilter.multiplier, this.secondFilter.multiplier ];

        },

        // Setter

        set: function( value ) {

            this.firstFilter.multiplier = value[0];

            this.secondFilter.multiplier = value[1];

        }

    });

    

    if ( Filter ) {

        // Filters Plugin standard naming is to remove the "Filter" and switch from camel case to lower-case with underscore

        // MultiPassCustomFilter -> multi_pass_custom

        Filter.add( "multi_pass_custom", MultiPassCustomFilter );

    }

 

})();

Uniform Values
name      | GLSL type | JS type | example 
----------|-----------|---------|---------
sampler2D | sampler2D | Bitmap | ImageManager.loadPicture( "my_picture" )
1f | float | Number | 1.0
2f | vec2 | Object | { x : 1.0, y : 2.0 }
3f | vec3 | Object | { x : 1.0, y : 2.0, z : 3.0 }
4f | vec4 | Object | { x : 1.0, y : 2.0, z : 3.0, w : 4.0 }
2fv | vec2 | Array | [ 1.0, 2.0 ]
3fv | vec3 | Array | [ 1.0, 2.0, 3.0 ]
4fv | vec4 | Array | [ 1.0, 2.0, 3.0, 4.0 ]
mat2 | mat2 | Array | [ 1.0, 2.0, 3.0, 4.0 ]
mat3 | mat3 | Array | [ 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0 ]
mat4 | mat4 | Array | [ 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0 ]


Custom Texture Uploads
Things get more advanced when you need custom textures (one example would be a refraction shader).
The Filters plugin can accept strings as uniforms. The string will switch from source state to destination state at the mid-point of the Filter interpolation animation.
An example getter/setter for a customer texture would be;
Code:
Object.defineProperty( TextureCustomFilter.prototype, 'customTexture', {

    // Returns the texture url

    get: function() {

        if ( this.uniforms.customTexture.value === null ) {

            return null;

        }

        return this.uniforms.customTexture.value.texture_url;

    },

    // value is a string (texture name within the pictures folder)

    set: function( value ) {

        if ( this.uniforms.customTexture.value === null || this.uniforms.customTexture.value.texture_url != value ) {

            this.uniforms.customTexture.value = ImageManager.loadPicture( value );

            this.uniforms.customTexture.value.texture_url = value;

        }

    }

});

Animation
The Filters Plugin will attempt to set a uTime property if it exists. To make uTime visible a getter/setter needs to be created;
Code:
Object.defineProperty( AnimatedCustomFilter.prototype, 'uTime', {

    get: function() {

        return this.uniforms.uTime.value * 60.0; // 60.0 is used to control the animation speed

    },

    set: function( value ) {

        this.uniforms.uTime.value = value / 60.0; // The multiply/divide value should be consistent with getter/setter

    }

});

Notes
fragmentSrc is where the GLSL lives. This is the most important part as this is the shader code itself. You must know GLSL to be able to write this (it is rather different to Javascript!). You can do an awful lot with this.

Refraction Filter
This is an example custom filter which features animation (set the speed) and the use of a custom texture (set the refractMap to the name of a .png image in the Pictures folder).
Version 2.0 of this Filter will be part of Filter Pack 1.

Refract.js
refract { "refractMap" : null, "strength" : 0.1, "speed" : { "x" : 0, "y" : 0 }, "uTime" : 0 }
Code:
//=============================================================================

// Refract.js

//=============================================================================

 

/*:

 * @plugindesc Refract Filter. For Xilefian's Filter Plugin.

 * @author Felix "Xilefian" Jones

 * 

 * @help

 * 

 * Version 1.0

 * Website [url=http://www.hbgames.org]http://www.hbgames.org[/url]

 * 

 * Filter:

 *   refract    { "refractMap":null, "strength":0.1, "speed":{ "x":0, "y":0 }, "uTime":0 }

 * 

 * Change log:

 *   Version 1.0:

 *     Initial version.

 * 

 */

 

(function() {

 

    /**

     * @author Felix "Xilefian" Jones

     * The RefractFilter

     *

     * @class RefractFilter

     * @extends AbstractFilter

     * @constructor

     */

    RefractFilter = function() {

        PIXI.AbstractFilter.call( this );        

        this.passes = [this];

 

        this.uniforms = {

            refractMap: {type: 'sampler2D', value:null },

            strength:   {type: '1f', value:0.1},

            speed:      {type: '2f', value:{x:0,y:0}},

            uTime:      {type: '1f', value:0}

        };

 

        this.fragmentSrc = [

            'precision mediump float;',

            'varying vec2 vTextureCoord;',

            'varying vec4 vColor;',

            'uniform sampler2D uSampler;',

            'uniform float uTime;',

            'uniform sampler2D refractMap;',

            'uniform float strength;',

            'uniform vec2 speed;',

            

            'void main(void) {',

            '   vec2 ruv = fract( vTextureCoord.xy + ( speed * uTime ) );',

            '   vec3 offset = texture2D( refractMap, ruv ).rgb;',

            '   offset -= ( offset / 2.0 );',

            '   offset *= 2.0;',

            '   offset.z *= strength;',

            '   gl_FragColor = texture2D( uSampler, vTextureCoord.xy + offset.xy * offset.z );',

            '}'

        ];

    };

 

    RefractFilter.prototype = Object.create( RefractFilter.prototype );

    RefractFilter.prototype.constructor = RefractFilter;

 

    /**

     * Sets the refract texture

     *

     * @property refractMap

     * @type String

     * @default null

     */

    Object.defineProperty( RefractFilter.prototype, 'refractMap', {

        get: function() {

            if ( this.uniforms.refractMap.value === null ) {

                return null;

            }

            return this.uniforms.refractMap.value.xi_url;

        },

        set: function( value ) {

            if ( this.uniforms.refractMap.value === null || this.uniforms.refractMap.value.xi_url != value ) {

                this.uniforms.refractMap.value = ImageManager.loadPicture( value );

                this.uniforms.refractMap.value.xi_url = value;

            }

        }

    });

    

    /**

     * Sets the power of the refraction

     *

     * @property strength

     * @type Number

     * @default 0.1

     */

    Object.defineProperty( RefractFilter.prototype, 'strength', {

        get: function() {

            return this.uniforms.strength.value;

        },

        set: function( value ) {

            this.uniforms.strength.value = value;

        }

    });

    

    /**

     * Animation time

     *

     * @property uTime

     * @type Number

     * @default 0

     */

    Object.defineProperty( RefractFilter.prototype, 'uTime', {

        get: function() {

            return this.uniforms.uTime.value * 60.0;

        },

        set: function( value ) {

            this.uniforms.uTime.value = value / 60.0;

        }

    });

    

    /**

     * Animation speed

     *

     * @property speed

     * @type Point

     * @default 0, 0

     */

    Object.defineProperty( RefractFilter.prototype, 'speed', {

        get: function() {

            return this.uniforms.speed.value;

        },

        set: function( value ) {

            this.uniforms.speed.value = value;

        }

    });

    

    /**

     * Register filter class with the Filter Plugin (if available)

     */

    if ( Filter ) {

        Filter.add( "refract", RefractFilter );

    }

        

})();

Retro Filter
This is an example custom multi-pass filter which both pixelates and reduces the colour of the screen.

Retro.js
retro { "step" : 3.779, "size" : 2 }
Code:
//=============================================================================

// Retro.js

//=============================================================================

 

/*:

 * @plugindesc Retro Filter. For Xilefian's Filter Plugin.

 * @author Felix "Xilefian" Jones

 * 

 * @help

 * 

 * Version 1.0

 * Website [url=http://www.hbgames.org]http://www.hbgames.org[/url]

 * 

 * Filter:

 *   retro    { "step":3.779, "size":2 }

 * 

 * Change log:

 *   Version 1.0:

 *     Initial version.

 * 

 */

 

(function() {

 

    /**

     * @author Felix "Xilefian" Jones

     * The RetroFilter

     *

     * @class RetroFilter

     * @extends AbstractFilter

     * @constructor

     */

    RetroFilter = function() {

        this.colorStep = new PIXI.ColorStepFilter(); // To reduce the colours

        this.pixelate = new PIXI.PixelateFilter(); // To reduce the sampling resolution

 

        this.passes = [this.colorStep, this.pixelate]; // We reduce colour first, then resolution (avoids an anti-aliasing blur)

 

        this.step = 3.779;

        this.size = 2.0;

    };

 

    RetroFilter.prototype = Object.create( RetroFilter.prototype );

    RetroFilter.prototype.constructor = RetroFilter;

 

    /**

     * Sets the colorStep reduction

     *

     * @property step

     * @type Number

     * @default 3.779

     */

    Object.defineProperty( RetroFilter.prototype, 'step', {

        get: function() {

            return this.colorStep.step;

        },

        set: function( value ) {

            this.colorStep.step = value;

        }

    });

    

    /**

     * Sets the pixelate size

     *

     * @property size

     * @type Number

     * @default 2.0

     */

    Object.defineProperty( RetroFilter.prototype, 'size', {

        get: function() {

            return this.pixelate.size.x;

        },

        set: function( value ) {

            this.pixelate.size.x = value;

            this.pixelate.size.y = value;

        }

    });

    

    /**

     * Register filter class with the Filter Plugin (if available)

     */

    if ( Filter ) {

        Filter.add( "retro", RetroFilter );

    }

        

})();
 

Thank you for viewing

HBGames is a leading amateur video game development forum and Discord server open to all ability levels. Feel free to have a nosey around!

Discord

Join our growing and active Discord server to discuss all aspects of game making in a relaxed environment. Join Us

Content

  • Our Games
  • Games in Development
  • Emoji by Twemoji.
    Top