How to use Xflow

lachsen edited this page Oct 28, 2014 · 15 revisions

Table of Contents

Data Compositing

First we will describe how data is declared and connected to the scene graph.

Declaring generic DataContainers

Several elements inside the scene graph, including <mesh>, <shader> and <lightshader> use generic data content, e.g. like this:

<mesh type="triangles">
  <int name="index">0 1 2 2 3 0 4 ... </int>
  <float3 name="position">-1.0 -1.0 -1.0 ... </float3>
  <float3 name="normal">0.0 0.0 -1.0 0.0 0.0 ... </float3>
  <float2 name="texcoord">1.0 0.0 ... </float2>
</mesh>

Generic data is declared with a list of typed ValueElements, e.g. <float>, <float3>, <int> and so on. Each ValueElement is assigned with a name to define a named DataField.

In XML3D, it is also possible to declare generic DataContainers that are not directly linked to any semantic. For this, we use the <data> element:

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

...

<group style="transform: translateX(-5px)" >
   <mesh type="triangle" src="#cubeMeshData" />
</group>
<group style="transform: translateX(5px)" >
   <mesh type="triangle" src="#cubeMeshData" />
</group>

Here, we declared the generic data inside a <data> element and reused it for two meshes by referring the <data> node via its document id inside the src attribute of <mesh>.

Combination of data

It is possible to combine multiple DataContainers and ValueElements by declaring them as children of another <data> element.

<data id="meshBaseData">
  <int name="index">0 1 2 2 3 0 4 ... </int>
  <float3 name="position">-1.0 -1.0 -1.0 ... </float3>
  <float3 name="normal">0.0 0.0 -1.0 0.0 0.0 ... </float3>
</data>

<data id="meshData1" >
   <data src="#meshBaseData" />
   <float3 name="color" >1 0 0 ...</float3>
</data>

<data id="meshData2" >
   <data src="#meshBaseData" />
   <float3 name="color" >0 0 1 ...</float3>
</data>

Here we combined #meshBaseData with two different color ValueElements.

It is also possible to combine multiple DataContainers:

<data id="meshBaseData">
  <int name="index">0 1 2 2 3 0 4 ... </int>
  <float3 name="position">-1.0 -1.0 -1.0 ... </float3>
  <float3 name="normal">0.0 0.0 -1.0 0.0 0.0 ... </float3>
</data>

<data id="meshOptionalData">
   <float3 name="texcoord" >1 0 0 ...</float3>
   <float3 name="color" >1 0 0 ...</float3>
</data>

<data id="meshFullData" >
   <data src="#meshBaseData" />
   <data src="#meshOptionalData" />
</data>

Note: A <data> node using the src attribute never contains any children. This is because those nodes ignore all children and only reuse the data of the referred node.

When combining DataContainers and ValueElements, it is possible that we get multiple DataFields with the same name. However, a DataContainer only contains one DataField per name. Multiple DataFields with the same name are reduced to one via the following replacement rules:

  1. A DataField of a ValueElement always replaces a DataField of a sibling DataContainer
  2. If two child nodes are both DataCotainers or ValueElements, the DataField of the later child replaces the DataField of the former.
A few examples:
<data>
  <float name="A" >2</float><!-- <<<< will be replaced -->
  <float name="A" >3</float> 
</data>

<data>
  <data>
    <float name="A" >0</float>
    <float name="B" >1</float><!-- <<<< will be replaced --> 
  </data>
  <data>
    <float name="B" >2</float>
    <float name="C" >3</float>
  </data>
</data>

<data>
  <data>
    <float name="A" >0</float><!-- <<<< will be replaced --> 
    <float name="B" >1</float>
  </data>
  <int name="A">42<int>  
</data>

<data>
  <int name="A">23<int>  
  <data>
    <float name="A" >0</float><!-- <<<< will be replaced --> 
    <float name="B" >1</float>
  </data>
</data>

Renaming/filtering of values

The DataFields inside a DataContainer can be renamed and filtered with the filter attribute. The filter attribute provides 3 modi:

  • remove: provide a list of names of DataFields that should be removed from the DataContainer
  • keep: provide a list of names of DataFields that should stay in the DataContainer. Remove all other DataFields
  • rename: rename DataFields as specified. The syntax for the name mapping is similar to the JavaScript object notation:
    { [destination name 1] : [source name 1], [destination name 2] : [source name 2], ...}
Note: it's also possible to provide a name mapping for keep and remove, though it won't make any difference in case of remove.

Examples:

<data filter="keep(A,D)">
    <float name="A" >0</float> 
    <float name="B" >1</float><!-- <<< will be removed -->
    <float name="C" >1</float><!-- <<< will be removed -->
    <float name="D" >1</float>
</data>

<data filter="remove(D)">
    <float name="A" >0</float> 
    <float name="B" >1</float>
    <float name="C" >1</float>
    <float name="D" >1</float><!-- <<< will be removed -->
</data>

<data filter="rename( {A2 : A, B2 : B} )">
    <float name="A" >0</float><!-- <<< will be renamed into A2 --> 
    <float name="B" >1</float><!-- <<< will be renamed into B2 --> 
    <float name="C" >1</float>
    <float name="D" >1</float>
</data>


<data filter="keep( {A2 : A, B2 : B} )">
    <float name="A" >0</float><!-- <<< will be renamed into A2 --> 
    <float name="B" >1</float><!-- <<< will be renamed into B2 --> 
    <float name="C" >1</float><!-- <<< will be removed -->
    <float name="D" >1</float><!-- <<< will be removed -->
</data>

<data filter="rename( {A1 : A, A2 : A, A3: A} )">
    <float name="A" >0</float><!-- <<< will be provided under the names A1, A2 and A3 --> 
</data>

Declaring data sequences

In certain cases we need arbitrary long sequences of data, e.g. key frame data for mesh animations. In that case it is possible to declare multiple values under the same name, using the key attribute of ValueElements:

<data id="keyFrameData">    
    <float3 name="position" key="0" >-5 0 5 ... </float3>
    <float3 name="normal" key="0" >0 -1 0 ... </float3>

    <float3 name="position" key="1" >-2.886751 2.113249 2.886751 ... </float3>
    <float3 name="normal" key="1">-0.554395 -0.620718 0.554395 ... </float3>

    <float3 name="position" key="2">-1.341089 4.649148 1.341089 ... </float3>
    <float3 name="normal" key="2">-0.696886 0.169412 0.696886 ... </float3>

    <float3 name="position" key="3" >-6.158403 1.408833 6.158403 ... </float3>
    <float3 name="normal" key="3">-0.141341 -0.979819 0.141341 ... </float3>

    ...
</data>

The replacement rules of DataFields always apply to sequence entries with the same name and key. If two DataFields have the same name and type, but different key values, they are always combined into a sequence. If no key is provided, the value 0 is assumed.

Data Processing

Xflow allows to process data by attaching operators on DataContainers. Several DataContainers can nested and attached with operators to create a complex processing graph.

Applying operators on DataContainers

The syntax to apply xflow operators is similar to a JavaScript function call. The operator call is declared inside the compute attribute of the DataContainer:

<data compute="position = xflow.add(position, posAdd)">
    <float3 name="position" > 2 2 2 </float3>
    <float3 name="posAdd" > 1 -1 1 </float3>
</data>	

Here, the operator is xflow.add, the input arguments are position and posAdd, the result is assigned to the name position. The operator xflow.add will simply add the two vectors together. The resulting DataContainer has the following values:

  • posAdd - the forwarded input, still with the value 1 -1 1
  • position - a newly computed value that replaced the original input position. Value: 3 1 3
Xflow provides an additional syntax to assign arguments by name instead of order:
<data compute="xfm = xflow.createTransform({translation: t, rotation: r)">
    <float3 name="t" > 1 0 -5 </float3>
    <float4 name="r" >1 0 0 0.5332</float4>
</data>	

If an operator has more than one ouput, these can be also asigned either by order or via name mapping:

<!-- assign arguments by order -->
<data compute="(index, position, normal) = xflow.createCube(size)">
   <float name="size"> 1.5 </float>
</data>
<!-- assign arguments by name. Here, the output names are 'i', 'p' and 'n' -->
<data compute="{i: index, p: position, n: normal} = xflow.createCube(size)">
   <float name="size"> 1.5 </float>
</data>

When no output mapping is provided, the operator will output its values with their default names

<!-- Will output values with names 'index', 'position' and 'normal' -->
<data compute="xflow.createCube(size)">
   <float name="size"> 1.5 </float>
</data>

DataContainers with operators can be nested to create a processing graph.

Example: A mesh with two blend shapes

<data id="meshData" >
    <float3 name="position" >2 2 2 ...</float3>
    <float3 name="normal" >1 0 0 ...</float3>
    
    <float3 name="blendPos1" >3 3 3 ...</float3>
    <float3 name="blendNormal1" >0 1 0 ...</float3>
    
    <float3 name="blendPos2" >2 3 4 ...</float3>
    <float3 name="blendNormal2" >0 0 1 ...</float3>
</data>

<data compute="position = xflow.morph(position, posAdd2, weight2)" >
    <data compute="position = xflow.morph(position, posAdd1, weight1)">
        <data compute="normal = xflow.morph(normal, normalAdd2, weight2)">
            <data compute="normal = xflow.morph(normal, normalAdd1, weight1)">
            
                <float name="weight1" >0.5</float>
                <float name="weight2" >0.2</float>
                
                <data src="#meshData" filter="keep(position, normal)" />
            
                <data compute="posAdd1 = xflow.sub(blendPos1, position)" >
                    <data src="#meshData" filter="keep(position, blendPos1)" />
                </data>
                <data compute="posAdd2 = xflow.sub(blendPos2, position)" >
                    <data src="#meshData" filter="keep(position, blendPos2)" />
                </data>
                <data compute="normalAdd1 = xflow.sub(blendNormal1, normal)" >
                    <data src="#meshData" filter="keep(normal, blendNormal1)" />
                </data>
                <data compute="normalAdd2 = xflow.sub(blendNormal2, normal)" >
                    <data src="#meshData" filter="keep(normal, blendNormal2)" />
                </data>
                
            </data>
        </data>             
    </data>						
</data>

Adding custom operators

TODO: Write this section.

Dataflows

We often want to declare a processing graph and reuse it for different data entries, just like we write functions to reuse code for different inputs. Xflow provides this feature with the <dataflow> element.

Dataflows define one or multiple sets of values elements and computations. We can annotate values elements as parameters of the dataflow that can be replaced on invocation.

Here a dataflow declaration of the previous blend shape processing graph:

<dataflow id="doubleBlendShape" out="position, normal" >
	<float3 param="true" name="position"></float3>
	<float3 param="true" name="normal"></float3>
	<float3 param="true" name="blendPos1"></float3>
	<float3 param="true" name="blendPos2"></float3>
	<float3 param="true" name="blendNormal1"></float3>
	<float3 param="true" name="blendNormal2"></float3>
	<float  param="true" name="weight1" ></float>
	<float  param="true" name="weight2" ></float>
	<compute>
		posAdd1 = xflow.sub(blendPos1, position);
		posAdd2 = xflow.sub(blendPos2, position);
		normalAdd1 = xflow.sub(blendNormal1, normal);
		normalAdd2 = xflow.sub(blendNormal2, normal);
		normal = xflow.morph(normal, normalAdd1, weight1);
		normal = xflow.morph(normal, normalAdd2, weight2);
		position = xflow.morph(position, posAdd1, weight1);
		position = xflow.morph(position, posAdd2, weight2);		
	</compute>
</dataflow>
The <dataflow> element defines the dataflow. Each value element with the param attribute is used as input parameter for the dataflow, which are identified via the name. The <compute> element inside the dataflow defines several processing steps, by listing several operator calls (as written inside the compute attribute), separated by semicolons. These operators can access any DataField declared previously inside the dataflow.

Here an example of invoking the dataflow:

<data compute="dataflow['#doubleBlendShape']" >
    <float name="weight1" >0</float>
    <float name="weight2" >1.0</float>
    
    <float3 name="position">...</float3>
    <float3 name="normal">...</float3>
    <float3 name="blendPos1">...</float3>
    <float3 name="blendPos2">...</float3>
    <float3 name="blendNormal1" >...</float3>
    <float3 name="blendNormal2" >...</float3>
</data>

This DataContainer uses the dataflow via a special syntax inside the compute attribute. We use the dataflow array and provide the document id of the dataflow element as key. Note that it is possible to provide a url to an external XML file including the dataflow es well:

<data compute="dataflow['datatflows.xml#doubleBlendShape']" >
   <!-- value elements same as before -->
</data>

Since the dataflow invoking element is still a generic DataContainer, we can use all technique of Xflow to combine and process the input data of the dataflow. For instance, we can separate the mesh data (position, normal, blendPose1, ... etc.) from the configuration of the pose (weight1, weight2) and even reuse the former for several instances:

<data id="blendData" >
    <int name="index" >...</int>
    <float3 name="position">...</float3>
    <float3 name="normal">...</float3>
    <float3 name="blendPos1">...</float3>
    <float3 name="blendPos2">...</float3>
    <float3 name="blendNormal1" >...</float3>
    <float3 name="blendNormal2" >...</float3>
</data> 

<group style="transform: translate(5px, 0, 0)" >
    <mesh type="triangles" compute="dataflow['#doubleBlendShape']" >
        <float name="weight1" >0</float>
        <float name="weight2" >0.4</float>
        <data src="#blendData" />
    </mesh>
</group>

<group style="transform: translate(-5px, 0, 0)" >
    <mesh type="triangles" compute="dataflow['#doubleBlendShape']" >
        <float name="weight1" >0.8</float>
        <float name="weight2" >0.2</float>
        <data src="#blendData" />
    </mesh>
</group>
In this example we directly use <mesh> elements to refer the dataflow. The blend data now also includes an index value element to correctly display the mesh. The index is ignored by the dataflow but still part of the result, because the output of the dataflow is combined with the original output of the <data>/<mesh> element, just as with operators.

It is also possible to use dataflows inside another dataflow. Let's for instance declared a quatroBlendShape dataflow using our previous doubleBlendShape:

<dataflow id="quatroBlendShape" out="position, normal" >
	<float3 param="true" name="position"></float3>
	<float3 param="true" name="normal"></float3>
	<float3 param="true" name="blendPos1"></float3>
	<float3 param="true" name="blendPos2"></float3>
	<float3 param="true" name="blendPos3"></float3>
	<float3 param="true" name="blendPos4"></float3>
	<float3 param="true" name="blendNormal1"></float3>
	<float3 param="true" name="blendNormal2"></float3>
	<float3 param="true" name="blendNormal3"></float3>
	<float3 param="true" name="blendNormal4"></float3>
	<float  param="true" name="weight1" ></float>
	<float  param="true" name="weight2" ></float>
	<float  param="true" name="weight3" ></float>
	<float  param="true" name="weight4" ></float>
	<compute>
		(position, normal) = dataflow['#doubleBlendShape'](position, normal, 
			blendPos1, blendPos2, blendNormal1, blendNormal2, weight1, weight2);
		(position, normal) = dataflow['#doubleBlendShape'](position, normal, 
			blendPos3, blendPos4, blendNormal3, blendNormal4, weight3, weight4);		
	</compute>
</dataflow>

Note, that we can provide an input and output mapping for the dataflow invokation, just as with operators. If this mapping is not provided, dataflow will always select input parameters by name and keep the original names of the output parameters.

Prototypes

The <proto> element and proto attribute have been removed since the introduction of the <dataflow> element in version 4.5 If you are using prototypes and want to update xml3d.js the conversion is pretty straight forward:

  1. Every <proto> element needs to be renamed to <dataflow> (content doesn't need to be changed)
    1. If the <proto> element itself has a filter or compute attribute, create an additional child <data> element that has these attributes instead. If the <proto> only uses a keep filter (e.g. filter="keep(position, normal)") you can use the out attribute of <dataflow> for that (e.g. out="position, normal")
  2. Whenever you instantiate a prototype with proto="#urlToProtoElement", you need to use compute instead: compute="dataflow['#urlToProtoElement']"