PixiJS Batch Rendering Library
pixi-batch-renderer
is a PixiJS plugin that allows you to add batch rendering to your custom display objects. I have documented each class in the PIXI.brend
namespace.
Concepts
Batch rendering objects involves aggregating them into groups/batches and rendering them together with one WebGL draw call. PixiJS supports batch rendering its internal display objects - PIXI.Sprite
, PIXI.Graphics
, and PIXI.Mesh
. However, it is difficult to extend that to custom-built display objects; it wasn't designed as an exposable API.
This library builds upon the same concept and is designed for maximum flexibility. It still uses PixiJS's batch system - a stub that enables objects to be rendered asynchronously, without loosing the order of rendering. To understand how it works, understanding these things are helpful:
-
Attribute Redirects: An attribute redirect is a data-object that tells
pixi-batch-renderer
how it will transform your object into a set of shader attributes. -
Index Property: If you use indices, this will be property on your display object that holds those indices. It could also be a constant array, rather than a property on each object.
-
State Function: This function/property tells the batch renderer what WebGL state is required to render a display-object. It is optional if you're display objects use the default state (
PIXI.State.for2d()
). -
Shader Function: This function generates the shader to render whole batches. It takes one argument - the batch renderer that will use the shader. You can use the
BatchShaderFactory#derive()
API for create one.
New features
-
Shader Templates: The
BatchShaderFactory
allows you to write shader "templates" containing%macros%
. At runtime, you can replace these macros based with another expression. For example, the (built-in)%texturesPerBatch%
macro is set to the no. of textures units in the GPU. -
Custom uniforms: [Experimental] You can also use uniforms in the batch shader; however, this might reduce the batching efficiency if most batchable display-objects have different values for uniforms (because then they can't be batched together).
-
Modular architecture: With the modular architecture of this library, you change the behaviour of any component. The geometry composition, batch generation, and drawing stages are componentized and can be modified by providing a custom implementation to
BatchRendererPluginFactory.from
.
Caveat with filtered/masked objects
Before rendering itself, a PIXI.Container
with filters or a mask will flush the batch renderer and will not batch itself. This
is because the PixiJS batch renderer cannot batch filtered and masked objects. Although this does not break pixi-batch-renderer,
it does reduce batching-efficiency. If you want to create a batch renderer that will batch filtered and masked objects too, your display-object must override render
(however, you will have to derive your own batch renderer class for that):
render(renderer: PIXI.Renderer): void
{
// If you registered the batch renderer as a plugin "pluginName", then replace <BatchRenderer> with
// renderer.plugins.pluginName
renderer.setObjectRenderer(<BatchRenderer>);
<BatchRenderer>.render(this);
for (let i = 0, j = this.children.length; i < j; i++)
{
this._children.render(renderer);
}
}
Usage
Standard Pipeline
For most use cases, PIXI.brend.BatchRendererPluginFactory
is all you'll need from this library. You need to do these three things:
-
Generate the plugin class using
PIXI.brend.BatchRendererPluginFactory.from
-
Register the plugin with PixiJS's WebGL renderer
-
Make your custom display object defer its rendering to your plugin
An example implementation would look like:
;; // ExampleFigure has two attributes: aVertex and aTextureCoord. They come from the// vertices and uvs properties in this object. The indices are in the indices property.Container thisvertices = x0y0 x1y1 x2y2 ... xnyn;// variable number of vertices thisuvs = u0v0 u1v1 u2 v2 ... unyn;// however, all other attributes must have equal length thistexture = PIXITexture; thisindices = 0 1 2 ... n;// we could also tell our batch renderer to not use indices too :) renderer; rendererplugins"ExampleRenderer"; // Define the geometry of ExampleFigure.const attribSet = source: "vertices" attrib: "aVertex" type: 'float32' size: 2 glType: PIXITYPESFLOAT glSize: 2 source: "uvs" attrib: "aTextureCoord" type: 'float32' size: 2 glType: PIXITYPESFLOAT size: 2 ; // Create a shader function from a shader template!const shaderFunction = // Vertex Shader`attribute vec2 aVertex;attribute vec2 aTextureCoord;attribute float aTextureId; varying float vTextureId;varying vec2 vTextureCoord; uniform mat3 projectionMatrix; void main(){ gl_Position = vec4((projectionMatrix * vec3(aVertex.xy, 1)), 0, 1); vTextureId = aTextureId; vTextureCoord = aTextureCoord;}` // Fragment Shader`uniform uSamplers[%texturesPerBatch%];/* %texturesPerBatch% is a macro and will become a number */\varying float vTextureId;varying vec2 vTextureCoord; void main(void){ vec4 color; /* get color, which is the pixel in texture uSamplers[vTextureId] @ vTextureCoord */ for (int k = 0; k < %texturesPerBatch%; ++k) { if (int(vTextureId) == k) color = texture2D(uSamplers[k], vTextureCoord); } gl_FragColor = color;}`{}; // Create batch renderer classconst ExampleRenderer = BatchRendererPluginFactory; // Remember to do this before instantiating a PIXI.Application or PIXI.Renderer!PIXIRenderer;
Uniforms Pipeline [Experimental]
You can take advantage of shader uniforms in batching too! pixi-batch-renderer supports this out of the box
with the AggregateUniformsBatchFactory
. There are two modes for uniforms:
-
Aggregation mode: Uniforms of each display-object are aggregated into an array. Then an uniform-ID attribute is uploaded (say
aUniformId
) that tells the shader which uniform to pick out of an array. The attribute is passed to the plugin factory via theuniformIDAttrib
option. -
No-aggregation mode: Uniforms are uploaded in one-element arrays. Display-objects with different uniform values are not batched together. This is useful if your uniform values don't differ a lot and will avoid another attribute. To use this mode, simply don't pass an
uniformIDAttrib
.
- Aggregation Mode (Example)
const UniformRedirect AggregateUniformsBatchFactory = ; const shaderFunction = // Vertex Shader`attribute vec2 aVertex;attribute vec2 aTextureCoord;attribute float aTextureId;attribute float aUniformId; varying float vTextureId;varying vec2 vTextureCoord;varying float vUniformId; uniform mat3 projectionMatrix; void main(){ gl_Position = vec4((projectionMatrix * vec3(aVertex.xy, 1)), 0, 1); vTextureId = aTextureId; vTextureCoord = aTextureCoord; vUniformId = aUniformId;}` // Fragment Shader`// You can also use this in the vertex shader.uniform shaderType[%uniformsPerBatch%];varying float vUniformId; uniform uSamplers[%texturesPerBatch%];/* %texturesPerBatch% is a macro and will become a number */\varying float vTextureId;varying vec2 vTextureCoord; void main(void){ vec4 color; float type; /* get color & shaderType */ for (int k = 0; k < int(max(%texturesPerBatch%., %uniformsPerBatch%.)); ++k) { if (int(vTextureId) == k) { color = texture2D(uSamplers[k], vTextureCoord); } if (int(vUniformId) == k) { type = shaderType[vUniformId]; } } if (type == 1) { gl_FragColor = color; } else { gl_FragColor = vec4(color.rgb * vTextureCoord.x, vTextureCoord.x); }}`{}; const uniformSet = source: "type" uniform: "shadingType" ;; const ExampleRenderer = BatchRendererPluginFactory
- No Aggregation Mode (Example)
const UniformRedirect AggregateUniformsBatchFactory = ; const shaderFunction = // Vertex Shader (no changes to standard pipeline example!)`attribute vec2 aVertex;attribute vec2 aTextureCoord;attribute float aTextureId;varying float vTextureId;varying vec2 vTextureCoord; uniform mat3 projectionMatrix; void main(){ gl_Position = vec4((projectionMatrix * vec3(aVertex.xy, 1)), 0, 1); vTextureId = aTextureId; vTextureCoord = aTextureCoord;}` // Fragment Shader`// Look only one-element instead of %uniformsPerBatch%uniform shaderType[1];varying float vUniformId; uniform uSamplers[%texturesPerBatch%];/* %texturesPerBatch% is a macro and will become a number */\varying float vTextureId;varying vec2 vTextureCoord; void main(void){ vec4 color; float type = shaderType; /* get color & shaderType */ for (int k = 0; k < %texturesPerBatch%; ++k) { if (int(vTextureId) == k) { color = texture2D(uSamplers[k], vTextureCoord); } } if (type == 1) { gl_FragColor = color; } else { gl_FragColor = vec4(color.rgb * vTextureCoord.x, vTextureCoord.x); }}`{}; const uniformSet = source: "type" uniform: "shaderType" ;; const ExampleRenderer = BatchRendererPluginFactory; PIXIRenderer; // Try using with a Sprite!const sprite = PIXISprite; spritepluginName = "erend";spritetype = 0;// will fade out horizontally in shader
Advanced/Customized Batch Generation
The BatchRendererPluginFactory.from
method also accepts these (optional) options that can be used to extend the
behaviour of built-in components:
-
BatchFactoryClass
: Child class of [StdBatchFactory]{@link https://pixijs.io/pixi-batch-renderer/PIXI.brend.StdBatchFactory.html} -
BatchGeometryClass
: Child class of [BatchGeometry]{@link https://pixijs.io/pixi-batch-renderer/PIXI.brend.BatchGeometryFactory.html} -
BatchDrawerClass
: Child class of [BatchDrawer]{@link https://pixijs.io/pixi-batch-renderer/PIXI.brend.BatchDrawer.html} -
BatchRendererClass
: If overriding a component does not meet your requirements, you can derive your own batch renderer by providing a child class of [BatchRenderer]{@link https://pixijs.io/pixi-batch-renderer/PIXI.brend.BatchRenderer.html}