Continuing the discussion from How to decide what updates?:
I finally got around to implementing a mechanism by which nodes register updates in v5.2.0 of my prototype API. The API is simple:
node.addRenderTask(function(timestamp) {
// `this` is the node the task was added to.
this.rotation = [0, rotation++, 0]
})
Try it out by checking out commit 5c87435 of trusktr/site then running the following (requires Node.js with NPM 3 and Meteor, but I’ll start posting live demos soon so you won’t need to install anything!):
git clone git@github.com:trusktr/site.git
cd site
git checkout 5c87435f
npm install -g 'npm@^3.0.0' # make sure to use NPM 3
npm install
meteor
Then visit http://localhost:3000. The demo looks the same as the one at Marrying the Scene Graph with the DOM, but it is using node.addRenderTask()
now instead of manual requestAnimationFrame
loops. This reduces the number of animation frames fired from multiple to a single frame per browser paint, manage by a single Motor instance.
What’s interesting:
1) Adding a render task to the node
causes the Motor to start an animation loop if one isn’t already started. When no render tasks exist (removed with node.removeRenderTask()
), the Motor stops the animation loop so if there are no animations then the app is completely idle (see screenshot in the release notes).
2) This API is opt-out, so when a render task is added with node.addRenderTask(fn)
, the task will be fired every animation frame until it is removed with node.removeRenderTask(fn)
.
3) Every frame, the Motor updates only the Node instance that addRenderTask
was called on, by calling node.render()
on that Node. The call to node.render()
does not traverse to the children of the node that .render()
is called on. There is a downside to this: we need to add one render task per node that we wish to animate. For example, one might write the following code expecting for two nodes to rotate, but will be surprised when only one of the nodes rotates:
let rotation = 0
node1.addRenderTask(function(timestamp) {
this.rotation = [0, rotation++, 0]
node2.rotation = [rotation, 0, 0]
})
What happens here is that node1
(referenced as this
inside the render task) will appear to rotate, but node2
will not, even if node2
is a child or grandchild of node1
. This is because Motor is only calling .render()
on node1
since the render task belongs to that node, and as mentioned, the Node#render
method does not traverse to children of the Node that it is called on. In order to make the example work, we’d have to do the following:
let rotation = 0
node1.addRenderTask(function(timestamp) {
this.rotation = [0, rotation++, 0]
})
node2.addRenderTask(function(timestamp) {
this.rotation = [rotation, 0, 0]
})
The upside of updating only the node that a render task is added to is that only that node gets updated, which means that the render tree doesn’t have to be traversed, therefore we may have better performance.
The reason we can avoid traversing the tree ourselves is because we know that when we use nested DOM elements (and therefore CSS transform caching) that the HTML engine itself will do the traversal of the DOM tree in order to perform transform multiplications (we don’t have to do it ourselves, so therefore we can avoid the extra traversal). The WebGL renderer will work similarly: nodes won’t be traversed directly by this part of the engine, only by the WebGL renderer in order to calculate world transforms (just like what the HTML engine does).
So yeah, that’s the first version update management in the engine. But, I already don’t like the burden of having to add a render task to each and every node that needs to be animated. For the next update, I’ll be adding a mechanism by which any node can be updated from a single render task, and I’d also like to move the addRenderTask
method to the Motor. It will look like this:
// ...create node1 and node2 and add them to the scene, then:
let rotation = 0
Motor.addRenderTask(timestamp => {
rotation += 1
node1.rotation = [0,rotation,0]
node2.rotation = [0,rotation,rotation]
})
When the update fires, the Motor will detect that node1
and node2
were updated, and will call .render()
on those nodes. I have a feeling this API will be easier and more convenient to use. For backwards compatibility, I might keep the .addRenderTask
on the Node class, but it will work the same way, just proxying to the Motor method of the same name.