My favorites | English | Sign in

Skinning

Introduction

In O3D, skinning refers to the process of associating mesh data with weighted Transforms for each vertex in the mesh. The Transforms, also referred to as the "bones" or "joints," define a basic network of how the pieces of a shape's "skeleton" move in relation to each other. Once this basic Transform structure has been defined, it can be reused on multiple sets of primitives, or skins that "cover" this skeleton.

Usually, you'll use a digital content creation tool such as Maya, 3ds Max, or Blender, which has skinning features that produce the vertex and transform data described here. Then you'll provide this additional data as input to O3D so that it can use the skinning data to compute the final set of rendered vertices.

Other Resources

If you're interested in understanding more details about skinning, the following papers provide useful background on this topic:

How Skinning Works

As a very simple example, consider this 2D shape, which is composed of 8 vertices:

Figure 1. A simple mesh, composed of 8 vertices, and a set of 3 Transforms (or bones).

Three Transforms affect how this jointed mesh can move. The skin describes (1) which Transforms affect which vertices in the mesh and (2) the amount of relative influence (or weight) each Transform has on the affected vertex. In this example, Vertex1 (V1) and Vertex2 (V2) are influenced by Transform1 (T1). This relationship can be written as:

V1skinned = V1 * T1 * 1.0
V2skinned = V2 * T1 * 1.0

where 1.0 is the weight (or influence) the Transform has on this vertex. Vertex3 (V3) is influenced by T1 and T2, so its skin entry would be as follows:

V3skinned = (V3 * T1 * W1) + (V3 * T2 * W2)

The following O3D example shows how this mesh is modified when the Transforms change:

To view the code for this example, click here.

Figure 2. A simple skinned animation, with 8 vertices and 3 Transforms.

Components of Skinning

The SkinEval object operates on three sets of input data:

  • ParamArray, which references the Transforms from the Transform graph, in indexed form
  • The SourceBuffer, which contains the original set of geometric data (positions and normals for the primitive)
  • The Skin, which contains the detailed breakdown of how each vertex is influenced by one or more Transforms

Figure 3. Evaluating skinning data.

Back to top

Simple Code Example: A Skinned Purple Cylinder

This section describes the Skinning example in more detail. Figure 3 shows the different components that are set up to create the "inputs" to the SkinEval: the ParamArray of Transforms, the source buffer, and the skin. The SkinEval object computes the transformed vertices, which are then sent to the stream bank and vertex buffer for rendering.

Conceptually, the basic steps for skinning a simple shape are as follows:

  1. Create the Shape.
  2. Create the ParamArray, which is used to index the Transforms. Transforms from the Transform graph are bound to this array.
  3. Set up the Transform graph (as usual).
  4. Create the sourceBuffer to hold the original set of vertex data..
  5. Create the Skin, which associates the Transform matrices with the vertices in the mesh and specifies the influence of each Transform.
  6. Create and set up the SkinEval object.
  7. Bind the vertex streams to the SkinEval, which fills in their values.

The following paragraphs describe each step in more detail, using code from the Skinning example. The code snippets are arranged conceptually; in some cases, code shown here as one step appears in separate sections of the actual code example.

Step 1: Create the Shape

This example uses the o3djs utility library function primitives.createCylinderVertices() to create the cylinder. The cylinder has a radius of 40 units and a height of 200 units, and the matrix passed in causes the entire cylinder to be created above the origin (the default is to center the cylinder around the origin, with half the cylinder below the XZ plane).

// Create a cylinder.
var vertexInfo = o3djs.primitives.createCylinderVertices(
    40, 200, 12, 20,
    [[1, 0, 0, 0],
     [0, 1, 0, 0],
     [0, 0, 1, 0],
     [0, 100, 0, 1]]);
var shape = vertexInfo.createShape(g_pack, material);

Step 2: Create the ParamArray of Transforms

This code creates a ParamArray object named matrices and sizes it to hold the 11 Transform matrices.

// Create a ParamArray to hold matrices for skinning.
var matrices = g_pack.createObject('ParamArray');
.
.
.
// Create 11 matrices on our ParamArray.
matrices.resize(11, 'o3d.ParamMatrix4');

Step 3: Create the Transform Graph

The cylinder example creates 11 Transforms and adds them to the Transform graph. It also binds the Transforms to the ParamArray and specifies their InverseBindPose.

// Create 11 transforms for the bones and parent them into a chain.
for (var ii = 0; ii <= 10; ++ii) {
  var transform = g_pack.createObject('Transform');
  g_transforms[ii] = transform;
  if (ii > 0) {
    transform.translate(0, 20, 0);
  }
  transform.parent = ii == 0 ? g_client.root : g_transforms[ii - 1];
  // Bind the world matrix of the transform to the corresponding matrix in the
  // ParamArray.
  matrices.getParam(ii).bind(transform.getParam('worldMatrix'));
  // Store the inverse world matrix of each transform as the bind pose for the
  // skin.
  skin.setInverseBindPoseMatrix(ii, g_math.inverse(transform.worldMatrix));
}
 
// Add the cylinder to the root transform.
g_transforms[0].addShape(shape);

A Note about the Inverse Bind Pose Matrix

Note that, for each transform, the skin.setInverseBindPoseMatrix() function is called:

skin.setInverseBindPoseMatrix(ii, g_math.inverse(transform.worldMatrix));

This function records the value of the transformation matrix (the inverse of it, actually) of the bone at the time the skin is bound. You will see below how this value is used during skinning computations to isolate the effects of the bone movement.

For example, the mesh shown in Figure 1 is created with this code:

var vertexInfo = o3djs.primitives.createPlaneVertices(
    10, 40, 1, 3,
    g_math.matrix4.mul(
        g_math.matrix4.mul(
          g_math.matrix4.rotationX(Math.PI / 2),
          g_math.matrix4.rotationY(Math.PI / 2)),
        g_math.matrix4.translation([0, 20, 0])));

Here, T3 is translated 40 units above the origin. V7 and V8 are also 40 units above the origin. The following calculation

V7' = V7 * T3

would put V7skinned at 80 units above the origin because the transformation is applied twice. The solution is to apply a transformation that reflects the difference between the value of T3 when the skin was bound (the bind pose) and its current value. That effect is achieved by multiplying with the inverse of world transform matrix that was recorded with the skin.setInverseBindPoseMatrix() call. So, the position of V7 after the skinning operation is actually calculated as follows:

V7skinned = V7 * IBP3 * T3 * TS-1 * W

where

V7
is the original position of the vertex
IBP3
is the inverse bind pose matrix
T3
is the transform's value in world coordinate space
TS-1
is the world matrix of the skinned geometry. By taking its inverse, the vertex is converted from the world coordinate system to the local coordinate system. This conversion is needed to render the shape in the right position (the world matrix will be applied to the vertex positions before rendering).
W
is the weight assigned to T3 for V7

Step 4: Create the Source Buffer

The sourceBuffer contains a copy of the POSITION and NORMAL streams created by the createCylinderVertices() function. Here is the code that copies the vertex data into the sourceBuffer so that it can be sent to the SkinEval object:

// Create a SourceBuffer for the Skin and set the vertices on it.
var sourceBuffer = g_pack.createObject('SourceBuffer');
var positionField = sourceBuffer.createField('FloatField', 3);
var normalField = sourceBuffer.createField('FloatField', 3);
sourceBuffer.allocateElements(numVertices);
positionField.setAt(0, positionStream.elements);
normalField.setAt(0, normalStream.elements);

Step 5: Create the Skin

This code creates the skin object and determines the influence (weight) of each Transform on each vertex based on the proximity of the vertex to each Transform (bone):

// Create a Skin to hold the influences and bind pose.
var skin = g_pack.createObject('Skin');
 .
 .
 .
// Make our source data for skinning and setup the influences for each bone.
// We only need the position and normals.
var positions = [];
var normals = [];
var positionStream = vertexInfo.findStream(g_o3d.Stream.POSITION);
var normalStream = vertexInfo.findStream(g_o3d.Stream.NORMAL);
var numVertices = positionStream.numElements();
for (var ii = 0; ii < numVertices; ++ii) {
  // Choose the bones to influence the vertex based on its height.
  // boneArea will be a fractional value between 2 bones based on the Y
  // position of the vertex. In other words, if the y Position of the vertex
  // is 63 and the bones are 20 units apart, then boneArea will = 6.15, meaning
  // it's between bones 6 and 7.
  var boneArea = positionStream.getElementVector(ii)[1] / BONE_SPACING;

  // Pull out the fractional part of boneArea
  var influence = boneArea % 1;

  // Compute the bone indices
  var bone1 = Math.floor(boneArea);
  var bone2 = bone1 + 1;
  if (bone2 >= NUM_BONES) {
    bone2 = NUM_BONES - 1;
  }

  // Now make each vertex be influenced by these two bones. In the example
  // above where boneArea was 6.15 we let bone1 influence the vertex by
  // (1 - 0.15) or 0.85 and bone2 influence it by 0.15.
  skin.setVertexInfluences(ii, [bone1, 1 - influence, bone2, influence]);
}

Step 6:Create and Set Up the SkinEval

Here is how you create the SkinEval object and set its matrices and skin properties, and tell it how to use the SourceBuffer (that is, set up the three sets of input to be evaluated).

// Create a SkinEval to use the Skin.
var skinEval = g_pack.createObject('SkinEval');

// Tell the SkinEval which matrices and Skin to use.
skinEval.matrices = matrices;
skinEval.skin = skin;
.
.
.
// Skinning happens in world space so bind the transform of the shape
// to the SkinEval so it can put our skin in object space.
skinEval.getParam('base').bind(g_transforms[0].getParam('worldMatrix'));
.
.
.
// Tell the skinEval how to use the SourceBuffer
skinEval.setVertexStream(g_o3d.Stream.POSITION,
                         0,
                         positionField,
                         0);
skinEval.setVertexStream(g_o3d.Stream.NORMAL,
                         0,
                         normalField,
                         0);

Step 7: Bind the Output Vertex Streams to the SkinEval

In the "ordinary case," where there is no skinning data, the vertex data is stored directly in the vertex buffer, as described in Shapes. This buffer is ready to be rendered by the GPU. When skinning data is used, the vertex data is first stored in a source buffer, which is regular memory accessed by the CPU. The vertex data from the source buffer is set as vertex streams on the SkinEval object, where it is transformed using the ParamArray and skin data, then output to the stream bank, as shown in Figure 2.

This code shows the final step, in which the POSITION and NORMAL streambanks are bound to the output of the SkinEval, and the transformed shape is rendered.

// Bind the Primitive's position and normal streams
// to the SkinEval so the SkinEval will fill them in each frame.
var streamBank = shape.elements[0].streamBank;
streamBank.bindStream(skinEval, g_o3d.Stream.POSITION, 0);
streamBank.bindStream(skinEval, g_o3d.Stream.NORMAL, 0);