Vertex Skinning help

Started by
4 comments, last by larspensjo 11 years, 10 months ago
Hello!
I'm trying to implement GPU Vertex Skinning with the help of Assimp Library.
However i've not understand what should i do and how a Skinning animation works.

Code Examples? Complete theory tutorials?
Thank you for your help!

P.S: already googled.
Advertisement
First off you will need to understand how skinning animation works. This is an excellent article that explains it. Try to understand the theory and ignore the DirectX specifics.

Now, assuming you've read the article, we will write the code. First off i will show you the animating and rendering code, then i will show you how to get the data from Assimp.

Step 1: The animating

Here's my animation class. I will explain it shortly.

Animation.h

#ifndef Animation_h
#define Animation_h

#include <D3dx10math.h>
#include <vector>

// Holds the data for a single keyframe
struct KeyFrame
{
float time;
D3DXVECTOR3 T; // Translation
D3DXVECTOR3 S; // Scale
D3DXQUATERNION R; // Rotation
};

//=====================================================================================================

// Represents a single bone or joint, whatever you want to call it. It contains a list of children bones, its offset matrix and
// an animated matrix which will be updated every frame.
struct Joint
{
D3DXMATRIX mOffsetTransf;
D3DXMATRIX mAnimatedTransf;
std::vector<Joint*> mChildren;
};

//=====================================================================================================
// Holds the entire animation data (list of keyframes) for a specific joint.
struct Channel
{
Joint* mJoint;
std::vector<KeyFrame> mKeyFrames;
};

//=====================================================================================================
class Animation
{
public:
Animation();
void Update(float delta);

Channel* CreateChannel()
{
mChannels.push_back(Channel());
return &mChannels.back();
}

void SetDuration(double Duration)
{
mDuration = Duration;
}

private:
void InterpolateJoint(const KeyFrame& k0, const KeyFrame& k1, D3DXMATRIX& out);

double mDuration; // in ms
float mTime;

// List of bone channels. Note that some bones may not be animated and thus will have no channel.
std::vector<Channel> mChannels;
};

#endif // Animation_h



Animation.cpp

#include "Animation.h"
Animation::Animation() :
mDuration(0),
mTime(0)
{
}
//=====================================================================================================
// Update animation
//=====================================================================================================
void Animation::Update(float delta)
{
mTime += delta;
if (mTime >= mDuration)
mTime = 0;

for (unsigned i = 0; i < mChannels.size(); i++)
{
std::vector<KeyFrame>& keyFrames = mChannels.mKeyFrames;

// Figure out where we are in our animation
unsigned k0 = 0;
while (true)
{
// If we have reached the end of our animation, do nothing
if (k0 + 1 >= keyFrames.size())
break;

// The current time is less than that of the next keyframe, interpolate the two keyframes accordingly.
if (mTime < keyFrames[k0+1].time)
{
InterpolateJoint(keyFrames[k0], keyFrames[k0 + 1], mChannels.mJoint->mAnimatedTransf);
break;
}

else
{
k0++;
}
}
}
}

//=====================================================================================================
// Interpolate joint transformation between 2 keyframes
//=====================================================================================================
void Animation::InterpolateJoint(const KeyFrame& k0, const KeyFrame& k1, D3DXMATRIX& out)
{
float t0 = k0.time;
float t1 = k1.time;
float lerpTime = (mTime - t0) / (t1 - t0);
D3DXVECTOR3 lerpedT;
D3DXVECTOR3 lerpedS;
D3DXQUATERNION lerpedR;
D3DXVec3Lerp(&lerpedT, &k0.T, &k1.T, lerpTime);
D3DXVec3Lerp(&lerpedS, &k0.S, &k1.S, lerpTime);
D3DXQuaternionSlerp(&lerpedR, &k0.R, &k1.R, lerpTime);
D3DXMATRIX T, S, R;
D3DXMatrixTranslation(&T, lerpedT.x, lerpedT.y, lerpedT.z);
D3DXMatrixScaling(&S, lerpedS.x, lerpedS.y, lerpedS.z);
D3DXMatrixRotationQuaternion(&R, &lerpedR);
out = R * S * T;
}


Woah.. What's going on there?

The animation class represents an animation track. Every frame, the Update function will be called with the elapsed time, the function will figure out between which 2 keyframes we are in our animation, and call the InterpolateJoint() function to interpolate the two keyframes. The result will be stored in the bone's animated matrix. This process is done for all bone channels.

So now we have animated our individual bones. Now we need to calculate the combined transform for all bones in order to get our final transfomations that will be sent to the shader. The following function does just that. It will be called with the skeleton's Root bone and an identity matrix.



//=====================================================================================================
// Recursively calculate the joints' combined transformations
//=====================================================================================================
void AnimationController::CombineTransforms(Joint* pJoint, const D3DXMATRIX& P)
{
D3DXMATRIX final = pJoint->mAnimatedTransf * P;

mFinalTransforms.push_back( pJoint->mOffsetTransf * final );

for (unsigned i = 0; i < pJoint->mChildren.size(); i++)
{
CombineTransforms(pJoint->mChildren, final);
}


I have that function inside an AnimationController class that manages all the animation tracks for a single mesh. It holds a list of Animation objects and the root node for the skeleton. Every frame, i ask the AnimationController to update all of its animation tracks and combine the final transformations. I would then retrieve mFinalTransforms from the AnimationController and pass them to the skinning shader.


Step 2: The rendering

Here's the skinning shader in HLSL. For simplification i have omitted lighting calculations and texturing. I also assume that the vertex will be affected by a maximum of 4 bones and that there are a maximum of 32 joints in the skeleton.


cbuffer c_buffer
{
float4x4 World;
float4x4 WorldViewProj;
float4x4 FinalTransforms[32];
}

struct VS_OUT
{
float4 position : SV_POSITION;
};

VS_OUT VShader(float4 position : POSITION, float4 weights : BLENDWEIGHT, int4 boneIndices : BLENDINDICES)
{
VS_OUT output;

float4 p = float4(0.0f, 0.0f, 0.0f, 1.0f);
float lastWeight = 0.0f;
int n = 3;

// I believe you can optimize this by unrolling the loop and making sure the weights add up to 1 during loading instead of in the shader
for(int i = n; i > 0; i--)
{
lastWeight += weights;
p += weights * mul(FinalTransforms[boneIndices], position);
}

lastWeight = 1.0f - lastWeight;
p += lastWeight * mul(FinalTransforms[boneIndices[0]], position);
p.w = 1.0f;

output.position = mul(WorldViewProj, p);
return output;
}



Step 3: Importing the data using Assimp

Now for the above code to work, we need to retrieve the following information:
- Vertex data (mainly bone indices and vertex weights)
- Skeleton hierarchy
- Bone data
- Keyframes

This step is very project specific so i won't provide any code.

I'm going to assume that you know how to load a scene using Assimp. If not, see how it's done in the sample. Make sure you pass the following flags to the aiImportFile function:

aiProcessPreset_TargetRealtime_Quality | aiProcess_ConvertToLeftHanded) & ~aiProcess_FindInvalidData

The first flag is for optimization.
The second one is for DirectX, since it uses a left handed coordinate system.
The third one is used out of convenience. aiProcess_FindInvalidData removes redundant animation key frames, meaning you will get a different number of scaling, rotation and translation keys, forcing you to even them out yourself. I choose to remove aiProcess_FindInvalidData (which is by default defined in aiProcessPreset_TargetRealtime_Quality) so that i get the same number of keyframes.

Once you have imported your scene, you will find an array of aiMesh pointers in the aiScene object. Obtaining the vertex positions, normals and texcoords is straightforward. (Note that mTextureCoords could contain more than one set of texture coordinates).

Inside the aiMesh object you will also find three things we need for skinning: vertex weights, bone indices and bone data. I will leave it up to you to retrieve the first two but getting the bone data is a bit tricky, so here's some code to do it. I didn't test it but it should theoretically work.


// Call a recursive function with the first bone in the mesh (the root node).
// Don't forget to store the root node because if you recall we need it in the AnimationController.
pJoint* pRoot = Formhierarchy(pMesh->mBones[0]);

// And here's the function
Joint* FormHierarchy(aiBone* pBone)
{
Joint* pJoint = new Joint;

Joint->mOffsetTransf = pBone->mOffsetMatrix;

// Get the aiNode belonging to the aiBone. g_Scene is the aiScene instance you got earlier
aiNode* node = g_Scene->mRootNode->FindNode(pBone->mName);

// initialize mAnimatedTransf with the bone's local transformation
pJoint.mAnimatedTransf = node->mTransformation;

// Now for the children
for (int i = 0; i < node->mNumChildren; i++)
{
Joint* pChild = FormHierarchy(pBone + i);
pJoint->mChildren.push_back( pChild );
}

return pBone;
}


Still alive? We're almost done tongue.png

Now we need the keyframe data, which are very easy to obtain. Here's some pseudo-code:


for every aiAnimation in the g_Scene
create an Animation instance

for every aiNodeAnim in the aiAnimation
create a Channel instance

// ~aiProcess_FindInvalidData flag should take care of it but just incase
assert((node->mNumPositionKeys == node->mNumRotationKeys) && (node->mNumRotationKeys == node->mNumScalingKeys));

for i = 0, i < node->mNumPositionKeys, i++
create a Keyframe instance

store everything in the Keyframe. The translation and scaling keys in a vector and the rotation in a quaternion
also store the time but don't forget to convert it to milliseconds by multiplying by 1000.


Now store everything in its proper place and you're done!

EDIT: One very important thing i forgot to mention is that you might have to transpose the bone matrices, depending on how you use them in your shader.

I'm not particularly good at explaining stuff so i'm not sure if that was easy to follow. So if you have any questions don't hesitate to ask smile.png
"Spending your life waiting for the messiah to come save the world is like waiting around for the straight piece to come in Tetris...even if it comes, by that time you've accumulated a mountain of shit so high that you're fucked no matter what you do. "
Also check out http://sourceforge.net/projects/assimp/forums/forum/817654/topic/3880745

First off you will need to understand how skinning animation works. This is an excellent article that explains it. Try to understand the theory and ignore the DirectX specifics.
...
....
....



YOU ARE AWESOME!!!!
I'M GOING TO READ ALL YOUR POST!
p.s: i already managed entire static scene loading with texturing and all vertex data (except bones)
I have one question about the animation. This is a quote from the Assim documentation (

aiNodeAnim Struct Reference):

The name specifies the bone/node which is affected by this animation channel. The keyframes are given in three separate series of values, one each for position, rotation and scaling. The transformation matrix computed from these values replaces the node's original transformation matrix at a specific time. This means all keys are absolute and not relative to the bone default pose.
[/quote]
But... Does this mean that the keys of the node (in other words - matrix) are relative to its parent node?
I am embarrassed by the word "absolute".


I have one question about the animation. This is a quote from the Assim documentation (aiNodeAnim Struct Reference):

The name specifies the bone/node which is affected by this animation channel. The keyframes are given in three separate series of values, one each for position, rotation and scaling. The transformation matrix computed from these values replaces the node's original transformation matrix at a specific time. This means all keys are absolute and not relative to the bone default pose.

But... Does this mean that the keys of the node (in other words - matrix) are relative to its parent node?
I am embarrassed by the word "absolute".
[/quote]
I complained about that one also, as it is clearly misleading. Yes, the keys are relative the parent node. I think the reference to "absolute" means that it is not relative to the mesh transformation matrix you also find in the node tree. This last matrix isn't used when doing animations.
[size=2]Current project: Ephenation.
[size=2]Sharing OpenGL experiences: http://ephenationopengl.blogspot.com/

This topic is closed to new replies.

Advertisement