Understanding Pixi.js Display List and Scenegraph

This article explains the inner workings of the Pixi.js scenegraph and display list, detailing how parent-child relationships govern rendering order, coordinate transformations, and visual inheritance. By understanding how Pixi.js traverses this hierarchical tree structure, developers can optimize rendering performance and manage complex 2D visual scenes more effectively.

The Scenegraph as a Tree Structure

At its core, Pixi.js organizes all visual elements in a hierarchical tree structure known as the scenegraph. The root of this tree is typically the Application.stage, which is a specialized Container object.

Every node in this tree is an instance of DisplayObject. While DisplayObject is the base class for anything that can be rendered (such as Sprite, Graphics, and Text), the Container class is a subclass of DisplayObject that can act as a parent to other display objects.

Stage (Container)
 ├── Background (Sprite)
 ├── GameUI (Container)
 │    ├── HealthBar (Graphics)
 │    └── ScoreText (Text)
 └── Player (Sprite)

By nesting containers inside other containers, you construct a logical hierarchy that mirrors the organization of your game or application interface.

Parent-Child Relationships and Transform Propagation

When you add a child to a parent container using parent.addChild(child), the child becomes dependent on the parent’s spatial properties. This relationship dictates how positions, rotations, scales, and visibilities are calculated.

Local vs. Global Coordinates

Each display object maintains its own local coordinate system. When you set sprite.position.set(10, 20), you are positioning the sprite relative to its parent’s origin, not the screen’s origin.

To draw objects on the screen, Pixi.js must convert these local coordinates into global coordinates (screen space). It does this using matrix mathematics: 1. Every DisplayObject has a local transform matrix (localTransform) calculated from its position, scale, skew, and rotation. 2. Every object also has a world transform matrix (worldTransform). 3. During the update phase, Pixi.js recursively calculates the worldTransform of each object by multiplying its parent’s worldTransform with its own localTransform.

If you move, rotate, or scale a parent container, all of its descendants automatically move, rotate, and scale with it because their world transforms are derived from the parent.

Attribute Inheritance

Beyond spatial transforms, other properties cascade down the scenegraph: * Alpha (Opacity): The actual rendering opacity of an object is its local alpha multiplied by its parent’s calculated world alpha. * Visibility: If a parent’s visible property is set to false, the entire branch of the tree is ignored during the update and rendering phases, regardless of the individual children’s visible states. * Renderable: Similar to visibility, if renderable is false, the object (and its children) will not be drawn, though its transforms are still calculated.

The Render Loop and Depth-First Traversal

The actual display list is not a separate flat list; rather, it is derived directly from the scenegraph during the rendering phase. Pixi.js processes the tree using a depth-first traversal algorithm.

Step 1: The Transform Update (updateTransform)

Before rendering, Pixi.js runs an internal update pass. Starting from the stage, it recursively calls updateTransform() on every active node. This ensures that all matrix calculations, bounds updates, and world alphas are fully computed and up-to-date before any pixels are drawn.

Step 2: Traversal and Painter’s Algorithm

Once transforms are updated, the renderer traverses the tree to draw the objects. Pixi.js uses the Painter’s Algorithm, meaning objects are drawn from back to front.

When traversing a Container, the rendering engine: 1. Renders the container itself (if it has its own visual representation, like a custom subclass). 2. Iterates through the container’s children array from index 0 to children.length - 1. 3. Recursively visits and renders each child.

Because of this order, children with lower indices in the array are drawn first (in the background), while children with higher indices are drawn on top of them (in the foreground).

Managing Z-Order

By default, the z-order of elements is determined strictly by their insertion order in the children array. If you want to change the rendering order, you can manipulate this array using methods like addChildAt(), removeChild(), or swapChildren().

Alternatively, Pixi.js supports automatic sorting via the sortableChildren and zIndex properties: * If you set container.sortableChildren = true, Pixi.js will automatically sort the children array based on each child’s zIndex value before rendering. * This sorting occurs during the update phase, ensuring that objects with higher zIndex values are drawn on top of those with lower values within the same parent container.