Skip to content

Animating Models with Xflow

Christian Schlinkmann edited this page Oct 26, 2015 · 12 revisions

Introduction

In the previous tutorial Using Assets and Models we learned how to add instances of a model to our scene and use Xflow to change aspects of it. This time we'll build on these concepts to animate the Ciccio robot that we've been working with.

The first half of this tutorial will serve as an introduction to Xflow and some of the concepts that we'll use in the second half to implement the animations.

  • Introduction to Xflow
  • Using Operators to change data
  • Combining a chain of Operators into a Dataflow
  • Animating Ciccio

Introduction to Xflow

Xflow was designed as a hierarchical data structure and drives all of the data processing in XML3D. Lets take a look at a simple <data> element that contains the mesh data for a 2D plane:

<data id="simplePlane" >
   <int name="index">0 1 2 1 2 3</int>
   <float3 name="position">-1.0 -1.0 -10.0 1.0 -1.0 -10.0 -1.0 1.0 -10.0 1.0 1.0 -10.0</float3>
   <float3 name="normal">0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 1.0</float3>
   <float2 name="texcoord">0.0 1.0 1.0 1.0 0.0 0.0 1.0 0.0</float2>
</data>

In this instance the <data> element does nothing more than collect the data stored in the value elements (int, float3 and so on) and provide it to any <mesh> that references it. We could also reference this <data> element from another <data> element, and we can change this simple example to be as complex in structure as we'd like:

<data id="simplePlaneIndicies">
   <int name="index">0 1 2 1 2 3</int>
</data>
<data id="simplePlaneFlippedNormals">
   <float3 name="normal">0.0 0.0 -1.0 0.0 0.0 -1.0 0.0 0.0 -1.0 0.0 0.0 -1.0</float3>
</data>
<data id="simplePlane" >
   <data src="#simplePlaneIndicies"></data>
   <data src="#simplePlaneFlippedNormals"></data>
   <data id="partialData">
      <float3 name="position">-1.0 -1.0 -10.0 1.0 -1.0 -10.0 -1.0 1.0 -10.0 1.0 1.0 -10.0</float3>
      <float3 name="normal">0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 1.0</float3>
   </data>
   <float2 name="texcoord">0.0 1.0 1.0 1.0 0.0 0.0 1.0 0.0</float2>
</data>

There's a lot going on here, but again each <data> element acts as a collector for the data beneath it. When we reference #simplePlane from another element we'll receive the same data that we did in the more simple representation... well, almost.

Notice that we've added another set of normal data with inverted values. Because Xflow evaluates in a bottom-up approach this second set of normals will override our original normals, which we've stored one level deeper in the hierarchy. So the end result when referencing #simplePlane will be our original plane mesh with flipped normals.

This is one of the core concepts in Xflow. Each level of the hierarchy can change, add or remove data without destroying the original.

The other important concept to keep in mind is that Xflow is a reactive system. Changing the data in one of the value elements above (for example, swapping the texture coordinates with new ones) will trigger a re-evaluation of the <data> element that contains it, which will in turn trigger a re-evaluation of any <data> element that relies on it and so on.

Using Operators to change data

As mentioned above, Xflow doesn't just collect data, it can also change it or even generate it. We can do this by using Xflow Operators. Lets look at a simple example that adds an offset to a set of positions:

<data id="offsetPositions" compute="position = xflow.add(position, offset)">
   <float3 name="position">-1.0 -1.0 -10.0 1.0 -1.0 -10.0 -1.0 1.0 -10.0 1.0 1.0 -10.0</float3>
   <float3 name="offset">0.0 0.0 10.0 0.0 0.0 10.0 0.0 0.0 10.0 0.0 0.0 10.0</float3>
</data>

The compute attribute is used to apply an operator to the <data> element, in this case xflow.add, which is one of the default operators included in xml3d.js. When this <data> block is evaluated the operator will receive the position and offset data as input, add them together, and then output the result as a new set of positions. Any element further up the data hierarchy will "see" only this new set of positions.

If we want to apply a chain of operators we can simply nest <data> elements inside one another, applying a different operator at each level of the hierarchy. And again, each level may add more data to the mix:

<data id="offsetX" compute="position = xflow.add(position, xoffset)">
   <float3 name="xoffset">1.0 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0</float3>
   <data id="offsetZ" compute="position = xflow.add(position, zoffset)">
      <float3 name="position">-1.0 -1.0 -10.0 1.0 -1.0 -10.0 -1.0 1.0 -10.0 1.0 1.0 -10.0</float3>
      <float3 name="zoffset">0.0 0.0 10.0 0.0 0.0 10.0 0.0 0.0 10.0 0.0 0.0 10.0</float3>
   </data>
</data>

Here the output of offsetX will be a single set of positions with the following values:

0.0 -1.0 0.0 2.0 -1.0 0.0 0.0 1.0 0.0 2.0 1.0 0.0

In other words, our simple plane shifted +10 units on the Z axis and +1 unit on the X axis.

xml3d.js includes many operators as standard (a full list is available here). You can of course create your own custom operators to use, but that's beyond the scope of this tutorial.

Combining a chain of Operators into a Dataflow

As you can imagine, chaining many operators together can quickly become confusing. In addition some common tasks like Skinning (aka Skeletal Animation) always apply an identical set of operators to our mesh data. It would be useful if we could define a template for such operations to easily reference them inside our models.

And we can do just that using Dataflows. Lets look at the skinning dataflow that we'll be using in the next steps to animate the Ciccio robot:

<dataflow id="skinning" out="position, normal">
   <float3   name="position" param="true"></float3>
   <float3   name="normal" param="true"></float3>
   <int4     name="boneIdx" param="true"></int4>
   <float4   name="boneWeight" param="true"></float4>
   <int      name="boneParent" param="true"></int>
   <float3   name="translation" param="true"></float3>
   <float4   name="rotation" param="true"></float4>
   <float4x4 name="inverseBindPose" param="true"></float4x4>
   <float    name="key" param="true"></float>
   <compute>
      rot       = xflow.slerpSeq(rotation, key);
      trans     = xflow.lerpSeq(translation, key);
      pose      = xflow.createTransform({translation: trans, rotation: rot});
      pose      = xflow.forwardKinematics(boneParent, pose);
      boneXform = xflow.mul(inverseBindPose, pose);
      position  = xflow.skinPosition(position, boneIdx, boneWeight, boneXform);
      normal    = xflow.skinDirection(normal, boneIdx, boneWeight, boneXform);
   </compute>
</dataflow>

For the first time we've defined value elements without any data in them. Normally this would generate an error in Xflow, but by annotating these elements with the param attribute we're essentially telling Xflow that this data will be supplied to the dataflow by the <data> element that uses it. Xflow then takes this data and applies the compute operations from top to bottom.

The end result will be the two data sets listed in the out attribute, position and normal.

By creating this dataflow template once we can now reference it from any <data> element (or even other <dataflow> elements) with one simple compute attribute:

<data compute="dataflow['#skinning']">
   ...all of the data required by the skinning dataflow...
</data>

Any element referencing this <data> element will "see" only the position and normal outputs of the dataflow.

Animating Ciccio

Ciccio scene from the last tutorial: ciccio_animation.zip

Ciccio as of the last tutorial

Our meshes for the Ciccio robot already contain all the data necessary to create the skinning animation, but until now we've been missing the skinning dataflow which has been included in the .zip above. You'll find it in dataflows.xml.

First we'll need to apply this dataflow to the mesh data of our Ciccio robot. Lets open ciccio.xml and do that now:

<!-- shared data -->
<assetdata name="base" compute="dataflow['dataflows.xml#skinning']">
   <data src="data/ciccio-mesh.xml#shared" />
   <data src="data/ciccio-anims.xml#animation" />
</assetdata>

Refreshing the page now will surprise us with some error messages from Xflow:

Xflow: operator xflow.lerpSeq: Input for key contains no data.
Xflow: operator xflow.slerpSeq: Input for key contains no data.

While the ciccio-mesh.xml and ciccio-anims.xml files contain all the data necessary to create the animations we're still missing one key bit of information: the animation key. You'll notice it's listed as a parameter in the skinning dataflow above, and right now this parameter is missing.

The animation key is our 'interface' to the animation. It lets us play the animation forwards or backwards, loop only certain sections of the animation or skip to any part of the animation. This is the only parameter that we need to override from inside our <model> tag.

Lets first add this missing element to our "base" assetdata:

<assetdata name="base" compute="dataflow['dataflows.xml#skinning']">
   <float name="key">0.0</float>
   <data src="data/ciccio-mesh.xml#shared" />
   <data src="data/ciccio-anims.xml#animation" />
</assetdata>

Refreshing the page now should clear the error messages, and we once again see our Ciccio robot from before.

By changing the value of the key we can change his pose:

<float name="key">3.0</float>

Adding the animation key

Lets automate this to loop through all of his animations in sequence. First, we need to override this key from our <model> tag:

<model src="ciccio.xml#ciccio" transform="#ciccio_transform">
   <assetdata name="base">
      <float name="key" id="animationKey">0.0</float>
   </assetdata>
   <assetmesh name="armor">
      <float3 name="emissiveColor">0.2 0.6 0.8</float3>
   </assetmesh>
   <assetmesh name="glass">
      <float3 name="diffuseColor">1.0 0.2 0.2</float3>
   </assetmesh>
</model>

Refresh the page and you'll see Ciccio is now back to his original pose, verifying that the key value of 0.0 from our <model> is overriding the value of 3.0 inside our asset.

Back to the original pose

Now lets add a simple JavaScript function to our page to loop the key through the valid range of values. This range is of course specific to the model that we're animating, in this case it's defined by the range of "rotation" and "translation" keyframes defined in ciccio-anims.xml: between 0 and 16.875.

<script type="text/javascript">
   var keyValue = 0.0;

   function loopAnimationKey() {
      keyValue = (keyValue + 0.05) % 16.875;
      document.getElementById("animationKey").textContent = keyValue;
   }

   setInterval(loopAnimationKey, 30);
</script>

The animated Ciccio

And there we have it! Here we see the reactive nature of Xflow in action. By changing just one input parameter we trigger a re-evaluation of every piece of data that depends on it. What's more, this re-evaluation will only happen the next time the data is requested (for example when drawing the mesh for the next frame).

Feel free to experiment with the key value, or maybe add a second Ciccio to the scene with his own animation key.