Problems animating COLLADA Model

I have some problems animating a loaded COLLADA Model. I’ve written my own parser and now I also want to write my own draw routine as well. The problem ist, that as soon as I enable the animation on my model, the hands, legs and the head is stretched away from the origin of the model. (The loader is implemented based on the tutorial here: COLLADA Tutorial)

The first thing I do in my draw function of the model is setup the joints matrices (not it’s world matrices!) with the given targets from the read blocks, If I for example read a channel like:

<channel source="#some_sampler" target="some_joint/transform(3)(2)"/>

I will modify the matrix component (3)(2) from the joint’s jointMatrix with the sid=“transform” in this first step:


if( mCurrentAnimations_.size() > 0 ) {
    unsigned currentFrame = GEAR::Root::getSingleton().getFrameEvent().frame;
    bool updateTime = false;
    if( currentFrame != mLastFrameUpdate_ ) {
        if( timeSinceLastFrame < 1.0f ) 
            updateTime = true;
        mLastFrameUpdate_ = currentFrame;
    }

    /****************************************************
     * If we have an active animation,                  *
     * we animate it in each of it's defined channels   *
     ***************************************************/
    std::list<DAEAnimation*>::iterator it = mCurrentAnimations_.begin();
    while( it != mCurrentAnimations_.end() ) {
        for( int c = 0; c < (*it)->animation->channels.size(); ++c ) {
            // update the time of the channelanimation if requested
            if( updateTime ) {
                (*it)->channelStates[c].elapsedTime += timeSinceLastFrame;
            }

            GEAR::COLLADA::Channel* channel = (*it)->animation->channels[c];
            // read the two indices depending on the time we're 
            int firstKeyframeTimeIndex = 0;
            int secondKeyframeTimeIndex = 0;
            for( int i = 0; i < channel->sampler->inputSource->mFloatArray_->mCount_; ++i ) {
                float time = channel->sampler->inputSource->mFloatArray_->mFloats_[i];
                if( firstKeyframeTimeIndex == secondKeyframeTimeIndex && time > (*it)->channelStates[c].elapsedTime && i > 0) {
                    firstKeyframeTimeIndex = i-1;
                    secondKeyframeTimeIndex = i;
                    break;
                }
                if( firstKeyframeTimeIndex == secondKeyframeTimeIndex && i == channel->sampler->inputSource->mFloatArray_->mCount_-1 ) {
                    (*it)->channelStates[c].elapsedTime = 0.0f;
                    firstKeyframeTimeIndex = i;
                    secondKeyframeTimeIndex = 0;
                    break;
                }
            }
            // look what kind of TargetAccessor we have
            if( channel->targetAccessor != NULL && channel->targetAccessor->type == GEAR::MATRIX_ACCESSOR ) {
                // ok we have to read 1 value for first and second index
                float firstValue = channel->sampler->outputSource->mFloatArray_->mFloats_[firstKeyframeTimeIndex];
                float secondValue = channel->sampler->outputSource->mFloatArray_->mFloats_[secondKeyframeTimeIndex];

                float firstTime = channel->sampler->inputSource->mFloatArray_->mFloats_[firstKeyframeTimeIndex];
                float secondTime = channel->sampler->inputSource->mFloatArray_->mFloats_[secondKeyframeTimeIndex];
                float interpolateValue = 1.0f / (secondTime - firstTime) * (secondTime - (*it)->channelStates[c].elapsedTime);
                // now we calculate an linear interpolated value
                float value = (secondValue*interpolateValue) + (firstValue*(1.0-interpolateValue));

                // now we have to write this value to the Joint's Matrix
                int entry = ((COLLADA::MatrixTargetAccessor*)channel->targetAccessor)->firstAccessor*4+((COLLADA::MatrixTargetAccessor*)channel->targetAccessor)->secondAccessor;
                channel->targetJoint->matrix->jointSpaceMatrix.entries[entry] = channel->targetJoint->matrix->matrix.entries[entry] + value;
            }
        }
        ++it;
    }
}

After the jointMatrices are modified by all channels, I recalculate the joint’s worldMatrices by calling the following function on the root Joint:


 void 
COLLADA::Joint::recalcWorldSpaceTransMat() {
    GEAR::Mat4 parentMat;
    if( parent != NULL )
        parentMat = parent->worldSpaceTransformationMatrix;
    // @todo Here we have to test against NULL!
    if( matrix != NULL ) 
        this->worldSpaceTransformationMatrix = parentMat * matrix->jointSpaceMatrix;
    else {
        this->worldSpaceTransformationMatrix = parentMat;
    }
    //std::cout << "Joint " << sid << " recalculated
";
    for( int i = 0; i < mChildJoints_.size(); ++i )
        mChildJoints_[i]->recalcWorldSpaceTransMat();
}

Now everything should be ready to draw my model width the following last part of my draw function:


for( int i = 0; i < mSubMeshes_.size(); ++i ) {
    for( int k = 0; k < mSubMeshes_[i]->mSubMeshes_.size(); ++k ) {
        // first we animate it
        GEAR::DAESubMesh* submesh = mSubMeshes_[i]->mSubMeshes_[k];
        submesh->buffer->lock( true );
        {
            for( unsigned v = 0; v < submesh->buffer->getNumVertices(); ++v ) {
                // get the array of joints, which influence the current vertex
                DAEVertexInfo* vertexInfo = submesh->vertexInfo[v];
                GEAR::Vec3 vertex; // do not init the vertex with any value!
                float totalWeight = 0.0f;
                for( int j = 0; j < vertexInfo->joints.size(); ++j ) {
                    Mat4& invBindPoseMatrix = vertexInfo->joints[j]->joint->invBindPoseMatrix;
                    Mat4& transMat = vertexInfo->joints[j]->joint->worldSpaceTransformationMatrix;
                    totalWeight += vertexInfo->joints[j]->weight;
                    vertex += (transMat*invBindPoseMatrix*(submesh->skin->bindShapeMatrix*vertexInfo->vertex))*vertexInfo->joints[j]->weight;
                }
                if( totalWeight != 1.0f ) {
                    float normalizedWeight = 1.0f / totalWeight;
                    vertex *= normalizedWeight;
                }
                submesh->buffer->bufferVertexPos( v, vertex );
            }
        }
        submesh->buffer->unlock();

        mSubMeshes_[i]->mSubMeshes_[k]->buffer->draw( GEAR::TRIANGLES, 0, mSubMeshes_[i]->mSubMeshes_[k]->buffer->getNumVertices() );
    }
}

Now The problem is, that the output looks like the following:

I’m sure to have the data loading routine implemented right, because the general animation of the walking man is visible, but the mesh is deformed:

As I said, when I uncomment the line:

channel->targetJoint->matrix->jointSpaceMatrix.entries[entry] = channel->targetJoint->matrix->matrix.entries[entry] + value;

The animation is disabled and the model is displayed in it’s standard pose:

Anyone, who has an idea about what’s going wrong?

I think you will find this popular COLLADA animation tutorial helpful.

This site was exactly the one, on which I started. I implemented my parser with that tutorial, but the author has made some assumptions, which I do not want to made, since my loader should support most cases of animations as well as all different geometries like triangles, poly lists and so on.
The assumptions he made are:

  1. Although COLLADA documents exported from Max or Maya Should be the same but in some cases they are different. We will only talk about COLLADA document exported from Studio Max, which of course does not mean that this tutorial might not help those who work in Maya. Because I am still positive that COLLADA documents exported from Maya should be the same, if we export them with backed matrices and triangulate options checked, from the COLLADA Exporter Options dialog. But I have never worked with Maya and don't know where my exporter might fail. This is ok
    
  2. The COLLADA document must have at least and at most a mesh, which means, anything in the asset's Max file, should be attached. So we must not have more then one &lt;mesh&gt; in the &lt;library_geometries&gt; in the COLLADA document. If we are able to read one &lt;mesh&gt; then we can read a 10000 too. Also ok
    
  3. Geometry in COLLADA should be triangulated, since that’s the better (If not best) option, we can provide OpenGL, so we let the triangulation work done by Max.
    
  4. Later in the implementations part we will assume our Model which was exported to COLLADA document has only One Texture file. Here I have support for more textures
    
  5. Animations in COLLADA must have at least or at most one Skeleton, with only one Root Bone (Typical). And I think that’s why we are here, to implement skeletal animation. Exactly the case I have implemented, since it really is the use case.
    
  6. Animation exported to COLLADA must be baked in matrices, which essentially in some cases makes 1 channel of animation and in others 16 channels of animation (Now what is channel? It should be explained later). Here the problem lies, as described later.
    
  7. Animations can only be valid if the channel targets the "Transform" of the targeted entity, just to keep things clear and easy. When you will bake matrices, then you will have this automatically, so don't need to worry about that. Here I also want to support &lt;rotation&gt; &lt;translation&gt; and &lt;scale&gt;, which make things much more complex.
    
  8. Animations can't have nested animations. I have implemented that also nested animations are read, but up to now, I do not use them, since my sample file does not have a nested animation.
    
  9. Only Skeletal Animation is supported (No baked animation yet). For me it's not clear, what's the difference.
    
  10. Every bone in the hierarchy must be attached as effecter on the skin. In other words, it must be added to the skin. This also is not really clear to me. Are non effector bones <node> blocks with type=“NODE”? If yes, than in the sample file of the astro boy, there are <node> blocks with type=“NODE” and I cannotsimply ignore them as I think.

Now back to the problem: how to animate the COLLADA Model
In the COLLADA file of the astro boy, we can find 16 channels in animations like this:



      <channel source="#astroBoy_newSkeleton_root-transform_astroBoy_newSkeleton_root_transform_0__0_-sampler" target="astroBoy_newSkeleton_root/transform(0)(0)"/>
      <channel source="#astroBoy_newSkeleton_root-transform_astroBoy_newSkeleton_root_transform_1__0_-sampler" target="astroBoy_newSkeleton_root/transform(1)(0)"/>
      <channel source="#astroBoy_newSkeleton_root-transform_astroBoy_newSkeleton_root_transform_2__0_-sampler" target="astroBoy_newSkeleton_root/transform(2)(0)"/>
      <channel source="#astroBoy_newSkeleton_root-transform_astroBoy_newSkeleton_root_transform_3__0_-sampler" target="astroBoy_newSkeleton_root/transform(3)(0)"/>
      <channel source="#astroBoy_newSkeleton_root-transform_astroBoy_newSkeleton_root_transform_0__1_-sampler" target="astroBoy_newSkeleton_root/transform(0)(1)"/>
      <channel source="#astroBoy_newSkeleton_root-transform_astroBoy_newSkeleton_root_transform_1__1_-sampler" target="astroBoy_newSkeleton_root/transform(1)(1)"/>
      <channel source="#astroBoy_newSkeleton_root-transform_astroBoy_newSkeleton_root_transform_2__1_-sampler" target="astroBoy_newSkeleton_root/transform(2)(1)"/>
      <channel source="#astroBoy_newSkeleton_root-transform_astroBoy_newSkeleton_root_transform_3__1_-sampler" target="astroBoy_newSkeleton_root/transform(3)(1)"/>
      <channel source="#astroBoy_newSkeleton_root-transform_astroBoy_newSkeleton_root_transform_0__2_-sampler" target="astroBoy_newSkeleton_root/transform(0)(2)"/>
      <channel source="#astroBoy_newSkeleton_root-transform_astroBoy_newSkeleton_root_transform_1__2_-sampler" target="astroBoy_newSkeleton_root/transform(1)(2)"/>
      <channel source="#astroBoy_newSkeleton_root-transform_astroBoy_newSkeleton_root_transform_2__2_-sampler" target="astroBoy_newSkeleton_root/transform(2)(2)"/>
      <channel source="#astroBoy_newSkeleton_root-transform_astroBoy_newSkeleton_root_transform_3__2_-sampler" target="astroBoy_newSkeleton_root/transform(3)(2)"/>
      <channel source="#astroBoy_newSkeleton_root-transform_astroBoy_newSkeleton_root_transform_0__3_-sampler" target="astroBoy_newSkeleton_root/transform(0)(3)"/>
      <channel source="#astroBoy_newSkeleton_root-transform_astroBoy_newSkeleton_root_transform_1__3_-sampler" target="astroBoy_newSkeleton_root/transform(1)(3)"/>
      <channel source="#astroBoy_newSkeleton_root-transform_astroBoy_newSkeleton_root_transform_2__3_-sampler" target="astroBoy_newSkeleton_root/transform(2)(3)"/>
      <channel source="#astroBoy_newSkeleton_root-transform_astroBoy_newSkeleton_root_transform_3__3_-sampler" target="astroBoy_newSkeleton_root/transform(3)(3)"/>

Every channel targets another matrix component of one single joint, so all in all every component of the matrix is animated ofer time.
Now I started to simply parse the channels and during animation I simply linear interpolated every component itself. But as far as I know, interpolation of the hole matrix cannot be made by simply linear interpolating each component. It has to be done in a global manner such like this:
In one frame, you have the matrix of the start frame and the matrix of the end frame given. This matric contains rotation as well as scale and translation. Now in order to interpolate this matrices with a delta in the range 0.0,1.0 given, we have to extract the rotation matrix. In my code I do this here by simply reseting the translation part:


		GEAR::Mat4 rotMatStart = matrix->jointSpaceMatrixStart;
		rotMatStart.setTranslationPart( GEAR::VEC3_ZERO );
		GEAR::Mat4 rotMatFinish = matrix->jointSpaceMatrixFinish;
		rotMatFinish.setTranslationPart( GEAR::VEC3_ZERO );

Since this is done, I calculate 2 quaternions from these matrices:


		// create Quaternions, which represent these 2 matrices
		float w = GEAR::Tools::sqr(1.0 + rotMatStart.entries[0] + rotMatStart.entries[5] + rotMatStart.entries[10]) / 2.0;
		float w4 = (4.0 * w);
		float x = (rotMatStart.entries[6] - rotMatStart.entries[9]) / w4 ;
		float y = (rotMatStart.entries[8] - rotMatStart.entries[2]) / w4 ;
		float z = (rotMatStart.entries[1] - rotMatStart.entries[4]) / w4 ;
		GEAR::Quaternion rotQuadStart(x, y, z, w);
		rotQuadStart.normalize();
		w = GEAR::Tools::sqr(1.0 + rotMatFinish.entries[0] + rotMatFinish.entries[5] + rotMatFinish.entries[10]) / 2.0;
		w4 = (4.0 * w);
		x = (rotMatFinish.entries[6] - rotMatFinish.entries[9]) / w4 ;
		y = (rotMatFinish.entries[8] - rotMatFinish.entries[2]) / w4 ;
		z = (rotMatFinish.entries[1] - rotMatFinish.entries[4]) / w4 ;
		GEAR::Quaternion rotQuadFinish(x, y, z, w);
		rotQuadFinish.normalize();

Now I use SLERP to interpolate the matrices and to get the final matrix:


		// create the interpolated rotation matrix
		GEAR::Quaternion slerpedRotQuat = slerp(rotQuadStart, rotQuadFinish, matrix->delta );
		slerpedRotQuat.normalize();
		GEAR::Mat4 rotMat;
		slerpedRotQuat.createMatrix( rotMat );

As the last interpolation step, I separately linear interpolate the translation part:



		
		// interpolate the translation part
		GEAR::Vec3 transVecStart(0.0,0.0,0.0);
		matrix->jointSpaceMatrixStart.getTranslatedVector3D( transVecStart );
		GEAR::Vec3 transVecFinish(0.0,0.0,0.0);
		matrix->jointSpaceMatrixFinish.getTranslatedVector3D( transVecFinish );
	
		GEAR::Mat4 transMat;
		transMat.setTranslation( transVecFinish*matrix->delta + (transVecStart*(1.0f-matrix->delta)) );

Now I write compose it back to a final joint space matrix for the current joint:


		// now write the resulting Matrix back to the Joint
		matrix->jointSpaceMatrix = rotMat * transMat;

The last step is to go deeper in the Joint tree and also intrpolate if required:


	// also interpolate all childs, if needed
	for( int i = 0; i < mChildJoints_.size(); ++i )
		mChildJoints_[i]->interpolateMatrices();

The code for the SLERP of the Quaternions is:


GEAR::Quaternion 
slerp(GEAR::Quaternion& qa, GEAR::Quaternion& qb, double t) {
	// quaternion to return
	GEAR::Quaternion qm;
	// Calculate angle between them.
	double cosHalfTheta = qa.w * qb.w + qa.x * qb.x + qa.y * qb.y + qa.z * qb.z;
	// if qa=qb or qa=-qb then theta = 0 and we can return qa
	if (abs(cosHalfTheta) >= 1.0){
		qm.w = qa.w;qm.x = qa.x;qm.y = qa.y;qm.z = qa.z;
		return qm;
	}
	// Calculate temporary values.
	double halfTheta = acos(cosHalfTheta);
	double sinHalfTheta = sqrt(1.0 - cosHalfTheta*cosHalfTheta);
	// if theta = 180 degrees then result is not fully defined
	// we could rotate around any axis normal to qa or qb
	if (fabs(sinHalfTheta) < 0.001){ // fabs is floating point absolute
		qm.w = (qa.w * 0.5 + qb.w * 0.5);
		qm.x = (qa.x * 0.5 + qb.x * 0.5);
		qm.y = (qa.y * 0.5 + qb.y * 0.5);
		qm.z = (qa.z * 0.5 + qb.z * 0.5);
		return qm;
	}
	double ratioA = sin((1 - t) * halfTheta) / sinHalfTheta;
	double ratioB = sin(t * halfTheta) / sinHalfTheta; 
	//calculate Quaternion.
	qm.w = (qa.w * ratioA + qb.w * ratioB);
	qm.x = (qa.x * ratioA + qb.x * ratioB);
	qm.y = (qa.y * ratioA + qb.y * ratioB);
	qm.z = (qa.z * ratioA + qb.z * ratioB);
	return qm;
}

This at least solves the problem of the overscaling of my mode, but it does not solve the problem at all, because now all parts of my model are positioned in the origin of it.

How can I go further in this? The tutorial site does not go much in depth about this and in the Spec of COLLADA 1.5 there is also nothing, or at least I haven’t found it jet.

Hey Andy,

I’m currently working with the Astroboy model as well and trying to implement my own parser for collada animation.

Unfortunately the Collada spec, the book and that collada tutorial are all lacking on really explaining how to your own parsing.

I have a quick question for you. In the Astroboy model one of the channel targets is
“astroBoy_newSkeleton_spine01/blendParent1”

I have yet to find anything explaining what blendParent1 is. What are we suppose to do with the cubic bezier curve value we get for this animation node?

It will be a single value, but are we suppose to translate, rotate, scale or just multiply the joint by this value?

Thanks in advance, maybe we can help each other through this undocumented headache.

Hi,
while parsing all the channels, I simply only try to connect the channels to the samplers and then I try to get the bone with the sid “astroBoy_newSkeleton_spine01” and then I try to get it’s transformation (this can be <scale>, <rotate>, <translate> and <matrix> in my implementation, or in general a “transformation”), which has the sid in your case “blendParent1”. Such a Bone with such transformation does not exist in the astroboy model (correct me if I’m wrong, because I cannot take a look into the model file at this time). So I simply ignore this channel, since it does not affect the model or any of it’s joints. I think tghat the blendParent1 is an exporter specific channel, which is only available if the model is exported using 3dsmax (blendParent1 is only in my Max Version of the astroboy I think, but I’m not sure. I also have another one exported using Maja).

Btw. I have solved my problem with the matrix interpolation as you can see here:

I also hope that we can help each other, because I’m currently on the next step to not only support <matrix>, but also <scale>, <rotate>, <translate> joint transformations as well as their channel animations. With matrix, everything works fine, but when I try to do the same thing with the rotations and so on, the model animation is broken. Currently I have no code here, because I’m not at my home, but when you’re interested (and I think you are :slight_smile: ), I can post the code as well.

Best regards

andy