Ivan Popleyshev has been working hard upgrading the libraries he has authored to PixiJS 6. The next one up is PixiJS Picture Kit – a shader-based implementation for blending modes that WebGL doesn’t actively support. Apart from blending, the “backdrop” texture it exposes can be used for other kinds of filtering.
Blend Modes
This section goes over blend modes and how they work in WebGL.
The blend mode defines how colors output by the fragment shader are combined with the framebuffer’s colors. In more simple terms, a blend mode is used to mix the colors of two objects where they overlap. Below, blend modes supported in PixiJS are shown by rendering a semi-transparent blue square over a red circle:

The normal blend mode makes the source color, blue, appear in the foreground over the destination color, red, in the background.
Porter Duff operations
The blend modes in the 2nd and 3rd columns have suffixes OVER, IN, OUT, and ATOP describing Porter Duff operations. These represent image compositing operations.
- OVER – colors are mixed for the whole object being rendered
- IN – only overlapping areas are rendered
- OUT – colors are outputted only in non-overlapping areas
- ATOP – colors are outputted only over existing content
In PixiJS, the blend modes only apply to pixels in the object being rendered, so the compositing operations look a bit different. For example, SRC_IN and SRC_ATOP look the same when. An actual IN operation would erase non-overlapping areas in the red circle. But since PixiJS only applies the blend mode in the blue square’s area, this is not possible with blending.
The blend modes with prefix DST switch which color is in the foreground. Even though the blue squares are rendered after the red circles, they are behind with DST blend modes. The DST_OVER blend mode will make a scene appear as if z-indices were reversed.
Arithmetic
The blend modes in the 1st column change the arithmetic used to mix the source and destination color.
- ADD – Sums the source and destination color with equal weighting instead of alpha-weighting
- SUBTRACT – Subtracts the source color from the destination. Negative values are clipped to zero.
- MULTIPLY – The colors are multiplied, which always results in darker colors.
Blend equation
The blend equation is a linear function on the source color and destination color that calculates the output color. This equation can be set separately for the RGB and alpha components of colors.

blendFunc
blendFunc is used to set the weights for the source and destination colors. Instead of passing predefined values for these weights, a WebGL constant representing these weights needs to be passed. For example, gl.DST_ALPHA
will set the weight to the destination color’s alpha.
For the normal blend mode, you’d use:
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)
blendFuncSeparate can be used to separately set these weighting for the RGB and alpha channel of colors.
blendEquation
blendEquation sets which equation is used to mix the colors after they’ve been multiplied by weights. You can add, subtract, reverse subtract, and even min / max. Most blend modes use the add equation.

blendEquationSeparate can be used to separately set which equation is used to mix the RGB and alpha channels of colors.
StateSystem
The StateSystem manages the blend mode for the PixiJS renderer. It works by mapping BLEND_MODES to the parameters for blendFunc and blendEquation described above. If you want to add more blend modes of your own, you can modify the undocumented blendModes
map in the state system
blendModes
basically maps each blend-mode to a list of parameters to blendFuncSeparate
and blendEquationSeparate
. These lists can have up to 8 parameters but only the first two are required. The ADD equation is used by default.
import { BLEND_MODES } from 'pixi.js';
const stateSystem = renderer.state;
// [gl.ONE, gl.ONE_MINUS_SRC_ALPHA]
console.log(stateSystem.blendModes[BLEND_MODES.ADD]);
// Add OVERWRITE blend mode and set it to a unique value!
BLEND_MODES.OVERWRITE = 100;
// This blend mode will basically overwrite the destination
// color with the source color. The destination has zero
// weight in the output.
renderer.state.blendModes[BLEND_MODES.OVERWRITE] = [
renderer.gl.ONE,
renderer.gl.ZERO,
];
In the above snippet, I create a OVERWRITE
blend mode that will make the background disappear wherever an object is rendered and only keep its pixels in the framebuffer.
Custom blending with shaders
The “fixed function” blend modes shown so far are perhaps limited. The normal blend mode is by far used for alpha compositing. The other Porter Duff variants can be used for masking. To make more complicated and artistic blend modes, a shader that samples the “source” and “destination” colors from textures is used.
PixiJS Picture implements this kind of shader as a filter. The source object is rendered into a filter texture. The destination pixels from the framebuffer or canvas are copied into a backdrop texture. The dimensions of these textures must be the same.
// Fragment shader
// Texture coordinates for pixels to be blended.
varying vec2 vTextureCoord;
// Filter texture with source colors
uniform sampler2D uSampler;
// Backdrop texture with destination colors
uniform sampler2D uBackdrop;
This type of filter is called a BlendFilter. The fragment shader in BlendFilter is a template:
void main(void)
{
vec2 backdropCoord = vec2(vTextureCoord.x, uBackdrop_flipY.x + uBackdrop_flipY.y * vTextureCoord.y);
vec4 b_src = texture2D(uSampler, vTextureCoord);
vec4 b_dest = texture2D(uBackdrop, backdropCoord);
vec4 b_res = b_dest;
%blendCode%
gl_FragColor = b_res;
}
b_src
is the source color sampled from the filter textureb_dest
is the destination color sampled from the backdrop textureb_res
is the output color calculated from the source and destination colors by the blending code. It’s set to the destination color by default.
When a BlendFilter is constructed, the %blendCode%
token is replaced by the blending code, which calculates the output color. This way multiple blend modes can be implemented by just writing the blending code for each one. You can find examples of these shader parts in the source code. To emulate the normal blend mode, the blending code would look something like this:
// Note: b_src and b_dest are premultiplied by alpha like
// all other colors in PixiJS.
b_res = b_src.a + (1 - b_src.a) * b_dest;
To use this code as a blend filter, you can construct a BlendFilter and apply it to a display object.
import { BlendFilter } from '@pixi/picture';
// Blending code for normal blend mode
const NORMAL_SHADER_FULL = `
b_res = b_src.a + (1 - b_src.a) * b_dest;
`;
// Create globally shared instance of blend filter. This is a
// good optimization if you're going to use the filter on multiple
// objects.
const normalBlendFilter = new BlendFilter({
blendCode: NORMAL_SHADER_FULL,
});
// Apply the filter on the source object
sourceObject.filters = [normalBlendFilter];
Built-in blend filters
PixiJS Picture implements filters for these blend modes:
- MULTIPLY
- OVERLAY
- SOFTLIGHT
- HARDLIGHT
getBlendFilter
maps each blend mode to instances of their blend filters, which can be applied on a display object to emulate the blend mode.
import { BLEND_MODES } from 'pixi.js';
sourceObject.filters = [getBlendFilter(BLEND_MODES.OVERLAY)];
PixiJS Picture also exports special versions of Sprite and TilingSprite where you can set the blendMode
directly and a blend filter is implicitly applied:
// Note: Use the Sprite exported from @pixi/picture and
// not the default one from pixi.js!
import { Sprite } from '@pixi/picture';
// Set the blendMode on the sprite directly. When the
// sprite renders, it will use the blend filter from
// getBlendFilter() automatically.
Sprite.blendMode = BLEND_MODES.OVERLAY;
Dissolve
The dissolve blend mode randomly chooses pixels from the source or destination texture to output. The likelihood of choosing the source color is equal to its alpha, i.e. a 0.5 alpha means half of the output pixels will come from the top layer and the rest will be from the bottom layer. In this mode, colors aren’t truly “mixed”.

The blending code for this is really simple:
// Noise function that generates a random number between 0 and 1
float rand = fract(sin(dot(
vTextureCoord.xy ,vec2(12.9898,78.233))) * 43758.5453);
if (rand < b_src.a) {
b_res = b_src;
}
The famous one-liner rand is used to generate a random number between 0 and 1. If this random variable is less than the alpha of the source color, then the resulting color is set equal to the source color. Otherwise, the resulting color is set to the destination color (by default).
A BlendFilter is needed to use it:
import { BlendFilter } from '@pixi/picture';
// Copy blendingCode
const DISSOLVE_FULL = `
// Noise function that generates a random number between 0 and 1
float rand = fract(sin(dot(
vTextureCoord.xy ,vec2(12.9898,78.233))) * 43758.5453);
if (rand < b_src.a) {
b_res = b_src;
}
`;
// Create blend filter
const dissolveBlendFilter = new BlendFilter({
blendCode: DISSOLVE_FULL,
});
// Apply it!
sourceObject.filters = [dissolveBlendFilter];
You can also augment BLEND_MODES
and create a DISSOLVE
blend mode. The blendFullArray
exported from @pixi/picture contains the blending code for each mode – the dissolve code needs to be added as well.
import { BLEND_MODES } from 'pixi.js';
import { Sprite, blendFullArray } from '@pixi/picture';
// Any non-conflicting number high enough works here!
BLEND_MODES.DISSOLVE = 100;
// Register the blending code with @pixi/picture
blendFullArray[BLEND_MODES.DISSOLVE] = DISSOLVE_FULL;
// Set it on a PixiJS Picture sprite
new Sprite().blendMode = BLEND_MODES.DISSOLVE;
Now, you can set the blendMode
to dissolve directly on a PixiJS Picture sprite.
Backdrop filters
Blend filters use the backdrop texture to read the destination color. If you have imported @pixi/picture, you can use the backdrop in other filters as well!
PixiJS Picture augments the filter system so that it copies pixels from framebuffer / canvas into the backdrop texture before a filter stack is applied. This backdrop texture is then available to filters as a uniform. The name of the uniform is configured by the backdropUniformName
property. For BlendFilter, this is set to uBackdrop
.
import { BackdropFilter } from '@pixi/picture';
const fragmentSrc = `
// The filter texture containing the object being rendered
uniform sampler uSampler;
// The backdrop texture
uniform sampler2D uBackdrop;
// TODO: Your shader code
`;
class CustomBackdropFilter extends BackdropFilter {
constructor() {
super(/* vertexSrc, fragmentSrc */);
// Set the backdropUniformName so the backdrop
// texture is available to shader code.
this.backdropUniformName = 'uBackdrop';
}
}
Magnifying glasses
Ivan shows how you can use the backdrop texture with his “magnifying glasses” example:

The grass background is rendered first. The two “lens” sprites are then rendered with a “displacement” filter. The lens texture is a displacement map – each texel encodes how much each pixel must be displaced.

The R channel holds the displacement in the x-direction and the G channels holds it for the y-direction. The (R, G) values are centered at (0.5, 0.5) and then scaled by a certain magnitude.
x = (r - 0.5) * scale;
y = (g - 0.5) * scale;
The centering is done because color values must be between 0 and 1, and displacements can have negative values.
The displacement filter samples the “lens” texture and calculates the displacement vector for the current pixel. It then samples the backdrop texture by adding this displacement to the passed texture coordinates:
// Read the displacement data from the lens texture
vec4 map = texture2D(uSampler, vTextureCoord);
// Map it into the displacement vector
map -= 0.5;
map.xy *= scale * inputSize.zw;
// Add the displacement vector to the texture coordinates,
// and then clamp it in case it goes outside the
// the backdrop texture.
vec2 dis = clamp(vec2(vTextureCoord.x + map.x, vTextureCoord.y + map.y), inputClamp.xy, inputClamp.zw);
// Handle y-flipping
dis.y = dis.y * backdropSampler_flipY.y + backdropSampler_flipY.x;
// Sample backdrop and output color
gl_FragColor = texture2D(backdropSampler, dis);
Mask filter
The MaskFilter
allows you to apply a filter on the backdrop wherever it overlaps with a mask. A common use case is the backdrop blur effect, which can be implemented by simply passing a blur filter to MaskFilter:
// Gaussian blur filter
import { BlurFilter, Graphics } from 'pixi.js';
// Masking filter
import { MaskFilter } from '@pixi/picture';
const mask = new PIXI.Graphics()
.beginFill(0xffffff, 1)
.drawRect(0, 0, 100, 100);
mask.filters = [
new MaskFilter(new BlurFilter()),
];
The above have the effect of blurring the background in the rectangle (0, 0, 100, 100). The white rectangular mask itself won’t be visible. Instead, another translucent white rectangle must be added so it appears visible.

If you’ve been reading up until here, I’m glad this article was informative. As the software industry goes remote, we all need a new office. Check out Teamflow, a virtual office built for the future.