3D Matrices Update Optimization

4×4 matrices are the heart of any 3D engine as far as math is concerned. And in any engine, how those matrices are computed and made available through the API are two critical points regarding both performances and ease of development. Minko was quite generous regarding the second point, making it easy and simple to access and watch local to world (and world to local) matrices on any scene node. Yet, the update strategy of those matrices was.. naïve, to say the least.

TL;DR

There is a new 3D transforms API available in the dev branch that provides a 50000% 25000% boost on scene nodes’ matrices update in the best cases, making it possible to display 50x 25x more animated objects. You can read more about the changes on Answers.

The Problem

Here is how it worked until very recently: each scene node was the target of a TransformController. The job of this controller was to listen to the changes of the ISceneNode.transform property in order to update the localToWorld and worldToLocal properties accordingly. Of course, as those properties rely on the parent’s transform, the controller also listen to it and that’s where trouble starts: modifying the transform of a node will trigger a lot of signals executions, implying a lot of overhead.

The worse case scenario is vertex skinning: the skeleton has to be updated top to bottom (because there is no other way to do it really…). And each update on a node will trigger updates on all of its descendants, leading to a factorial complexity and a huge overhead.

The good point with this method is that all the scene nodes transforms are always available and “synchronized” with each other. The very bad point is that it does a lot of unnecessary computations using recursive signals that imply a huge function calls overhead.

The Solution

To fasten the local to world matrices computation, we decided to update them in a batched fashion: all the local to world matrices of a (sub)scene had to be updated in a single method call using a single loop. It forbids updating the (sub)scene as a tree data structure, since it would mean recursion – and a big function call overhead – or using stacks – which is not the strong point of AS3. Therefore, we had to linearize the (sub)scene in order to get a list of transforms. To do this, we simply do a breadth first traversal of the (sub)scene tree when it has changed in order to get a flat list of matrices. We also store a few other things in other lists – such as the number of children or the id of parent of each node – in order to preserve the data we need to traverse that very list as a linearized tree structure.

This job is done by the TransformController.updateTransformsList() method:

C++

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

privatefunctionupdateTransformsList():void

{

varroot:ISceneNode=_target.root;

varnodes:Vector.=new[root];

varnodeId:uint=0;

_nodeToId=newDictionary(true);

_transforms=new[];

_localToWorldTransformsInitialized=new[];

_localToWorldTransforms=new[];

_worldToLocalTransforms=new[];

_numChildren=new[];

_firstChildId=new[];

_idToNode=new[];

_parentId=new[-1];

while(nodes.length)

{

varnode:ISceneNode=nodes.shift();

vargroup:Group=node asGroup;

_nodeToId[node]=nodeId;

_idToNode[nodeId]=node;

_transforms[nodeId]=node.transform;

_localToWorldTransforms[nodeId]=newMatrix4x4().lock();

_localToWorldTransformsInitialized[nodeId]=false;

if(group)

{

varnumChildren:uint=group.numChildren;

varfirstChildId:uint=nodeId+nodes.length+1;

_numChildren[nodeId]=numChildren;

_firstChildId[nodeId]=firstChildId;

for(varchildId:uint=0;childId<numChildren;++childId)

{

_parentId[uint(firstChildId+childId)]=nodeId;

nodes.push(group.getChildAt(childId));

}

}

else

{

_numChildren[nodeId]=0;

_firstChildId[nodeId]=0;

}

++nodeId;

}

_worldToLocalTransforms.length=_localToWorldTransforms.length;

_invalidList=false;

}

In order to avoid unnecessary computations, we decided to update them on a frame-to-frame basis. To make sure this update happens just before rendering and that all matrices are actually up to date, we’ve added the Scene.renderingBegin signal. As you might have guesses, this signals is simply executed right before the scene starts the actual rendering operations when Scene.render() is called (so it’s called after Scene.enterFrame, which is the signal which should be used to update the scene).

This is the job of the TransformController.updateLocalToWorld() method: