Categories
PixiJS

PixiJS Layers Kit

Ivan Popelyshev recently migrated the PixiJS Layers Kit to be compatible with PixiJS 6. I helped him also document the @pixi/layers API here. This package introduces an interesting concept – making the order in which items in your scene tree render separate from the tree’s hierarchy. It allows you to change the display order of display objects by assigning layers.

Let’s start with a scenario in which this might be helpful – notes that can be dragged using handles that are on top. Each note and its handle is kept in the same container – so they move together; however, they need to be in separate “layers” – one below for the items and one above for the handles. In a conventional scene tree, it would not be possible to have this layering without splitting the notes and their handles into separate containers and setting their positions individually.

But @pixi/layers makes this possible! You can group items in your scene tree and render those groups as layers. These items will only render when their parent layer becomes active during rendering.

// The stage to be rendered. You need to use a PIXI.display.Stage
// so that it correctly resolves each DisplayObject to its layer.
const stage = new PIXI.display.Stage();

// Set the parentGroup on items in your scene tree.
const GROUPS = {
  BG: new PIXI.display.Group(),
  FG: new PIXI.display.Group(),
};

// These groups are rendered by layers.
stage.addChild(new PIXI.display.Layer(GROUPS.BG));
stage.addChild(new PIXI.display.Layer(GROUPS.FG));

// How to make an item so that the handle is above all 
// other content
function makeItem() {
  const item = new PIXI.Container();
  const handle = item.addChild(new PIXI.Graphics());// Do drawing
  const body = item.addChild(new PIXI.Graphics());// Do drawing

  // Set the group of the handle to foreground
  handle.parentGroup = GROUPS.FG:

  // Set the group of the body to background
  body.parentGroup = GROUPS.BG;

  return item;
}

This changes the display order of items in the scene tree.

How it works

When you import @pixi/layers, it applies a mixin on PixiJS’ DisplayObject and Renderer. It adds these new behaviors:

  • If the scene root is a Stage, the renderer would now call updateStage – which traverses the scene tree and resolves each item to its group and layer.
  • A DisplayObject will only render when its active parent layer is being rendered (which is resolved in the update-stage step).
  • It also patches the Interaction API’s TreeSearch to correctly hit-test a layer-enhanced scene tree.

In the updating phase, every Group with sorting enabled will sort its display objects by their zOrder. This z-order is different from the built-in z-index that PixiJS provides. Both z-index and z-order are used for sorting objects into the order you want them to be rendered. z-order is the implementation provided by @pixi/tilemap. You can use both of them in conjunction – objects will be sorted by z-index first then by z-order.

When a layer renders, it will set the active layer on the renderer – which then indicates that objects in that layer can now render.

An example of how layering changes the display order

Note that Layer extends Container, so you can add children directly to it like Item 3 in the diagram. You don’t have to set parentGroup or parentLayer on these children as it is implicit.

Z-grouping to reduce sorting overhead

In containers with many children, if only a few z-indices are being used they can be replaced with a fixed number of layers. For example, when users edit a group of items they expect them to come on top. Instead of setting a higher z-index on these items, this can be implemented by promoting items to an “editing” layer. This is much easier than shifting items into another container, which interferes with interaction.

This technique replaces sorting with a linear-time traversal of your scene tree. It should especially be used when your scene tree is large.

Using zOrder

A Group will sort its items by their z-order if the sort option is passed:

import { Group } from '@pixi/layers';

// Group that sorts its items by z-order before rendering
const zSortedGroup = new Group(0, true);

If the z-order of a scene is relatively static, it’s more efficient to disable automatic z-order sorting and invoke it manually:

// Don't sort each frame
group.enableSort = false;

// Instead, call this when z-orders change
group.doSort();

Another neat feature is that Group emits a sort event for each item before sorting them. This can be used to dynamically calculate the z-order for each item, which is particularly useful when you want to order items based on their y-coordinate:

// You have to enable sort manually if you don't pass
// "true" or a callback to the constructor.
group.enableSort = true;

// Sort items so that objects below (along y-axis) are on rendered over others
group.on('sort', (item) => {
  item.zOrder = item.y;
});

Check out this example – here the bunnies are moving back and forth but the bottommost bunnies are rendered over others:

Layers as textures

Layers can be rendered into intermediate render-textures out of the box. The useRenderTexture option enables this:

layer.useRenderTexture = true;
layer.getRenderTexture();// Use this texture to do stuff!

When a layer renders into a texture, you won’t see it on the canvas directly. Instead, its texture must be drawn manually onto the screen. This can be done using a sprite:

// Create a Sprite that will render the layer's texture onto the screen.
const layerRenderingSprite = app.stage.addChild(new PIXI.Sprite(layer.getRenderTexture());

The layer’s texture is resized to the renderer’s screen – so too many layers with textures should not be used. This technique can be used to apply filters on a layer indirectly through the sprite rendering its texture:

// Apply a Gaussian blur on the sprite that indirectly renders the layer
layerRenderingSprite.filters = [new PIXI.filters.BlurFilter()];

By rendering into an intermediate texture, layers can be optimized to re-render when their content changes. You can split your scene tree so that layers are rendered separately – and then use the layer textures in the main scene.

// Main application with its own stage
const app = new PIXI.Application();

// The relatively static scene that is rendered separately. This is not
// directly shown on the canvas - later, a sprite is added
// to render its texture onto the canvas.
const staticStage = new PIXI.display.Stage();
const staticLayer = new PIXI.Layer();

staticLayer.addChild(new ExpensiveMesh());
staticLayer.useRenderTexture = true;

// Add a sprite that renders the static scene's snapshot to the main
// scene.
app.stage.addChild(new PIXI.Sprite(staticLayer.getRenderTexture());

// Rerenders the static scene in the next frame before
// the main scene is rendered onto the canvas. This should be invoked
// whenever the scene needs to be updated.
function rerenderExpensiveMeshNextFrame() {
  app.ticker.addOnce(() => {
    renderer.render(staticStage);
  });
}

Double buffering

A beautiful use of layer textures is for showing trails of moving objects in a scene. The trick is to render the last frame’s texture into the current frame with a lower alpha. By applying an alpha, previous frames quickly decay which ensures only a few frames are seen trailing.

Since WebGL does not allow rendering a texture into itself, double buffering is needed. The layer needs to flip-flops between two textures, one for rendering into and one for rendering from. This can be enabled by useDoubleBuffer:

// Ensure the layer renders into a texture instead of the canvas
layer.useRenderTexture = true;

// Enable double buffering for this layer
layer.useDoubleBuffer = true;

Note that useRenderTexture must be enabled for double buffering – not enabling it will result in the layer rendering directly to the canvas.

Now, since the layer flip-flops between rendering into the two textures, the texture used to render the last frame back into the layer needs to flop-flip. The layer kit does this internally by hot swapping the framebuffer and WebGL texture of getRenderTexture() each frame.

// Create a sprite to render the last frame of the layer
const lastFrameSprite = new PIXI.Sprite(layer.getRenderTexture());

// Apply an alpha so the last frame decays a bit
lastFrameSprite.alpha = 0.6;

// Render the last frame back into the layer
layer.addChild(new PIXI.Sprite(layer.getRenderTexture()));

// Render the layer into the main stage
stage.addChild(new PIXI.Sprite(layer.getRenderTexture()));

In the above snippet, sprites are created using the layer’s texture. When it runs, the sprites are actually flip-flopping between two different textures each frame. See it in action by Ivan’s example: