Optimizing Bounds Calculations in PixiJS Scenes Shukant K. Pal February 2020 This paper describes techniques that can be used to optimize the bounds calculation of pixi.js display objects. 1 Introduction 1.1 Terminology • Target object: Any pixi.js display object on which an operation occurs or property accessed is called the ”target”. • All nodes below a target node are collec- tively a part of the subtree (which includes the target as well). • Frame of reference: The frame of reference for any display object is a 2D coordinate system uniquely associated with it. • Content Bounds: The bounds of a display- object when no transformation is applied. This is defined in the target’s own frame of reference. The space taken by children is ignored when calculating content bounds. • Local Bounds: - The union of the content bounds of a display-object and the content bounds of nodes in its subtree after trans- forming them into the target’s frame of ref- erence. Due to the transformation required in child nodes, the local bounds depend on the local transform of each node in the sub- tree. An alternate, recursive, and simpler definition of local bounds is the union of the content bounds of a display object and the local bounds of its children (not the whole subtree) when transformed into its own frame of reference. • World Bounds: - The transformation of local bounds into the scene graph’s root frame of reference. This ”root” frame of reference is also known as the ”world” and the aforementioned bounds are called ”world bounds”. • Node depth: The depth of a node in the scene graph denoted by α • Transformation matrix T ( α 0 , α P ): Con- verts a point from the reference frame of a node at depth α 0 to that of its ances- tor at depth α P = α 0 − ∆ α The world transformation matrix is always of the form T ( α 0 , 0) = T world ( α 0 ). 1.2 Why? To generate the local bounds of an object, the the transforms of all nodes in its subtree have to be recalculated. Similarly, to calculate the world bounds of an object, the transforms of all ances- tors must be updated before the target’s subtree. If the depth of an object in the scene graph is α and the number of objects in its subtree is N , then the number of transforms calculated in a getLocalBounds() call is N and in a getBounds() call is N + α It can easily be calculated that in a tree with depth α and n times more nodes each successive depth, calling getBounds(false) on each node will result in α × n α transform updates. Of course, pixi.js deals with this, by updat- ing the whole scene graph once before a render pass (with O ( N ) transform updates). Internally, the renderer uses getBounds(true) to pre- vent any additional transform updates. This vul- nerability results in the following restrictions: 1. getBounds(true) must always be used in the render pass to prevent any unneces- 1 sary transform updates. This has not been documented properly. 2. getLocalBounds() will dirty the trans- forms of all the nodes in the subtree. This means that an updateTransform must always be called after it. If not, then child nodes will render using the wrong trans- form. 3. Applications with large scene graphs will suffer from full transform updates each frame if they require local-bounds of nodes anywhere in their code. 1.3 Goals The following optimizations are the end-goal of this paper: 1. Local-bounds calculations of a node will not result in ”dirty transforms” in its sub- tree. 2. Local-bounds of any object are only recal- culated when a) its transform has changed or b) a node in its subtree has changed transforms or c) content bounds in its sub- tree has changed. Similarly, world-bounds of an object should be recalculated on the same conditions or if one of the ances- tor’s transform has changed. This must be achieved by preventing transform updates from making the bounds dirty if the trans- form hasn’t changed. 2 Co-existence of non-local transformation matrices The current implementation of PIXI.Transform holds only two transformations matrices - a lo- cal transform T local and its corresponding world transform T world ( α 0 ) (for a node at depth α 0 ). However, there actually exist α 0 non-local trans- forms for a node that may be required when cal- culating the local bounds of one of its ancestors. The current behaviour of getLocalBounds is to cut the scene graph temporarily at the tar- get node and update the world-transforms of its subtree as if the target itself was the root. This overwrites each world-transform and makes it dirty. A call later to getBounds on the target or any one of its ancestors will trigger a full re- calculation of the world transforms in the whole subtree. This behaviour can be corrected by allowing the co-existence of non-local transforms that are tagged with a depth-delta: 1. The difference in depth of the node itself and the ancestor whose reference frame is the destination of the transformation ma- trix (= α 0 − α P ). or 2. ” ∞ ” indicating that the non-local trans- form is, in fact, the actual world trans- form. This is required because it is very expensive to track the absolute depth of a node outside of a getLocalBounds or getBounds call. 2.0.1 Virtuality of non-local, non-world transforms Non-local, non-world transforms may be de- scribed as ”virtual” because they do not have any purpose outside of a getLocalBounds call (i.e. they are an implementation detail and do not ”exist” for the end-user). Furthermore, a virtual transform will always have a finite depth- delta ∆ α = α 0 − α P associated with it. 2.0.2 Equivalence of virtual and world transform Both virtual and world transforms are non-local. The difference is just that the world transform is always cached in PIXI.Transform , while virtual transforms are temporarily created when calculating local-bounds of an ancestor. 2 2.1 Implementation • A PIXI.Transform object will have a map associating the depth-delta to its corresponding virtual transform (say virtualTransforms ). • The exhibited worldTransform will be either the actual world transform or one of the virtual transforms in virtualTransforms By default, the actual world transform shall be exhibited. Only under certain circumstances shall a virtual transform be exhibited, and the end-user will never see a virtual transform being exhibited. • A transform-update associated with a getLocalBounds call will indicate the depth-delta (if any) of the transform up- date. If it exists, then the virtual trans- form associated with that depth-delta will be calculated (by the same recursive algo- rithm used for the world transform.) T virtual ( α ) = T virtual ( α − 1) × T local (1) • At the beginning of a getLocalBounds call, the transform update will use depth- deltas that associated with the refer- ence frame of the target. This will make transforms in the subtree exhibit worldTransform s of the newly created virtual transforms. At the end of a getLocalBounds call, all virtual trans- forms associated with the target node shall be deleted and the actual world transforms be restored. 2.2 Reflection This section dealt with the first goal of this pa- per. By temporarily creating virtual transforms, we prevent the actual world transforms from be- ing corrupted in getLocalBounds 3 Optimized virtual trans- form and lazy bounds cal- culations 3.1 Derivation of a virtual trans- form based off of the corre- sponding world transform A world transform T world ( α 0 ) can be decom- posed into an ordered product of local transforms of ancestors at each level: T world ( α 0 ) = T local (1) × T local (2) ... × T local ( α 0 ) (2) Similarly, the transformation matrix used to calculate the bounds of a a node at depth α 0 in the frame of reference of a node at depth α P (for example, when calculating local bounds of an ancestor) will be: T ( α 0 , α P ) = T local ( α P +1) × T local ( α P +2) ... × T local ( α 0 ) (3) On comparing with equation (1), it can be seen that: T ( α 0 , α P ) = inv( T world ( α P )) × T world ( α 0 ) (4) This optimization relies on the fact that the predicate T world ( α P ) is equal in the target and child node transforms. This assumption is valid if we are simultaneously updating worlds trans- forms while calculating virtual transforms. This also assumes that the world transform of the target node is invertible. The condition for this is that the determinant of the world trans- form T world ( α P ) is non-zero, which can safely be assumed for transforms in a pixi.js scene graph. This is an alternate method of calculating a virtual transform without having to recursively calculate the corresponding virtual transform of its direct parent. The benefit derived is that a virtual transform is not needed by a node’s chil- dren and, hence, the virtual transform can be 3 deleted right after a calculateBounds rather than an (additional) recursive deletion at the end of the target’s getLocalBounds 3.2 Preventing dirty bounds with- out dirty transforms The current implementation of updateTransform will dirty boundsID even if the transform has not changed. This can be prevented by only making the bounds dirty of the worldID of the transform has changed. To do this, updateTransform will re- turn the a value indicating the difference in boundsID it has caused. This differ- ence will then be added by the parent’s updateTransform method. The target node on which updateTransform was called will manually then propagate this to its ancestors. 3.3 Bounds dependencies Suppose transforms throughout the scene graph have not changed, then still the bounds of a tar- get object depend on the changes in the content- bounds of nodes in its subtree. Unlike transforms whose dependence is top-down, this dependency is bottom-up (i.e. changes in children affect the parent, not vice versa). Since updateTransform is a recursive function, it can serve to detect changes in both types of dependencies. Hence, to de- tect whether any content bounds have changed for a node in the subtree, each node will have a subtreeBoundsID which will be the sum of its own boundsID and its children’s subtreeBoundsID . If the content-bounds of a node in the subtree have changed, then its boundsID and hence subtreeBoundsID must have changed too. Similar is the situation if a transform has changed. Changes will prop- agate up the subtree because of the summation at each level. 3.4 Preventing redundant trans- form calculations by pre- evaluating changes in local transforms The local-bounds of an object depend only on the local transforms in its subtree (ex- cluding itself). A preliminary pass similar to updateTransform that checks for any changes in transforms without actually calculating them will prevent redundant transform updates. 3.5 Reflection This section dealt with preventing transform- updates from causing a bounds recalculation when transforms haven’t changed and intrinsic, content bounds are still the same. This solves goal no. 2 for world bounds. This still leaves a weakness that if a trans- form of an ancestor has changed, then the trans- forms of the whole subtree will be updated and, henceforth, the local bounds will be recalculated again. This is prevented by the preliminary check discussed in 3.4 4 Implementation summary 4.1 getLocalBounds After implementing all the changes recom- mended in this paper, a getLocalBounds call would operate as follows: • A checkBounds method will be used to detect any local bounds-affecting changes in the subtree (from section 3.4). – Each node’s local transform will be updated and the difference in transform_ currentLocalID will be added to boundsID – A content-bounds change should have already changed boundsID – Changes in boundsID will be prop- agated up the subtree via summation in subtreeBoundsID 4 • If the resulting subtreeBoundsID is dif- ferent in the target, this would indicate that the stored local-bounds are dirty. • A updateTransform call will occur. Each node in the subtree will calcu- late a virtual transform that results in the target’s reference frame. This virtual transform shall be exhibited in node.transform.worldTransform The calculations of these virtual trans- forms can be optimized in accordance with section 3.1 by passing down the world transform of the target node at each level (each node will calculate the world transform and then apply equation 4 to calculate the corresponding virtual transform. This requires that the target node update its own world transform via recursivePostUpdateTransform() ). • This updateTransform will dirty boundsID again if the world trans- form has changed. These changes will be propagated up to the target’s subtreeBoundsID again. • A calculateBounds call will occur. • The subtree will be recursed again to restore the actual world transforms and delete the temporary virtual transforms. This can be optimized by making a wrap- per around calculateBounds that will automatically delete any existing virtual transform (because of the section 3.1 op- timization). 4.2 getBounds This operation is similar to getLocalBounds , except here: 1. A recursivePostUpdateTransform is obligatory to check for world-transform changes in ancestors. 2. A transform-update does no harm (be- cause world transforms are cached for each node, and will only be recalculated if parent is dirty, see Transform.updateTransform ). It would look something like as follows: • A recursivePostUpdateTransform occurs followed with an updateTransform • If the subtreeBoundsID has not changed, then bounds do not have to be recalculated. • Otherwise, run calculateBounds as normal. 4.3 Virtual transform tagging Instead of tagging virtual transforms with a depth-delta, it may be more appropriate to tag them with the target node. This is useful if, in the future, virtual transforms may be cached like the world-transform. 5 Future work Local-bounds of the scene graph can be (op- tionally) cached before each render pass like the world bounds in the current implementation. This can be done by updating transforms bottom up and calculating local bounds at each level. 5