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.