pixi-batch-renderer

Batch rendering library for PixiJS applications

Usage no npm install needed!

<script type="module">
  import pixiBatchRenderer from 'https://cdn.skypack.dev/pixi-batch-renderer';
</script>

README

pixi-batch-renderer

Node.js CI install size dependencies

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.

Usage

In browser:

PIXI.brend.BatchRenderer

Using ES:

import { BatchRenderer } from 'pixi-batch-renderer';

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:

  1. Generate the plugin class using PIXI.brend.BatchRendererPluginFactory.from

  2. Register the plugin with PixiJS's WebGL renderer

  3. Make your custom display object defer its rendering to your plugin

An example implementation would look like:

import * as PIXI from 'pixi.js';
import { AttributeRedirect, BatchRendererPluginFactory, BatchShaderFactory } from 'pixi-batch-renderer';

// 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.
class ExampleFigure extends PIXI.Container
{
  _render(renderer)
  {
    this.vertices = [x0,y0, x1,y1, x2,y2, ..., xn,yn];// variable number of vertices
    this.uvs = [u0,v0, u1,v1, u2, v2, ..., un,yn];// however, all other attributes must have equal length
    this.texture = PIXI.Texture.from("url:example");

    this.indices = [0, 1, 2, ..., n];// we could also tell our batch renderer to not use indices too :)

    renderer.setObjectRenderer(renderer.plugins["ExampleRenderer"]);
    renderer.plugins["ExampleRenderer"].render(this);
  }
}

// Define the geometry of ExampleFigure.
const attribSet = [
  new AttributeRedirect({
      source: "vertices", 
      attrib: "aVertex", 
      type: 'float32', 
      size: 2, 
      glType: PIXI.TYPES.FLOAT, 
      glSize: 2
  }),
  new AttributeRedirect({
      source: "uvs", 
      attrib: "aTextureCoord", 
      type: 'float32', 
      size: 2, 
      glType: PIXI.TYPES.FLOAT, 
      size: 2
  }),
];

// Create a shader function from a shader template!
const shaderFunction = new BatchShaderFactory(
// 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;
}
`,
{}).derive();

// Create batch renderer class
const ExampleRenderer = BatchRendererPluginFactory.from({
    attribSet,
    indexProperty: "indices",
    textureProperty: "texture",
    texIDAttrib: "aTextureId", // this will be used to locate the texture in the fragment shader later
    shaderFunction
});

// Remember to do this before instantiating a PIXI.Application or PIXI.Renderer!
PIXI.Renderer.registerPlugin("ExampleRenderer", ExampleRenderer);

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 the uniformIDAttrib 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.

  1. Aggregation Mode (Example)
const { UniformRedirect, AggregateUniformsBatchFactory } = require('pixi-batch-renderer');

const shaderFunction = new BatchShaderFactory(
// 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);
    }
}
`,
{}).derive();

const uniformSet = [
  new UniformRedirect({ source: "type", uniform: "shadingType" });
];

const ExampleRenderer = BatchRendererPluginFactory.from({
  uniformSet,
  uniformIDAttrib: "aUniformId",

  // Previous example's stuff
  attribSet,
  indexProperty: "indices",
  textureProperty: "texture",
  texIDAttrib: "aTextureId",
  shaderFunction,

  BatchFactoryClass: AggregateUniformsBatchFactory
})
  1. No Aggregation Mode (Example)
const { UniformRedirect, AggregateUniformsBatchFactory } = require('pixi-batch-renderer');

const shaderFunction = new BatchShaderFactory(
// 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);
    }
}
`,
{}).derive();

const uniformSet = [
  new UniformRedirect({ source: "type", uniform: "shaderType" });
];

const ExampleRenderer = BatchRendererPluginFactory.from({
  uniformSet,

  attribSet,
  indexProperty: "indices",
  textureProperty: "texture",
  texIDAttrib: "aTextureId",
  shaderFunction,

  BatchFactoryClass: AggregateUniformsBatchFactory
});

PIXI.Renderer.registerPlugin("erend", ExampleRenderer);

// Try using with a Sprite!
const sprite = PIXI.Sprite.from(<url>);

sprite.pluginName = "erend";
sprite.type = 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: