half-life 1 smd reader in directx (coord system problem)

Started by
5 comments, last by Buckeye 15 years, 8 months ago
Hi, I've started to write a smd reader. I can render the model with its textures and its correct normal vectors. Now I'd like to animate the model with its skeleton. But unfortunatly I can't render the correct skeleton because of the DirectX coordinate system (I can recognize its arms and legs but in wrong position :( DirectX coord system is Left Handed (x to the right, y upward, z goes away from us) but in the model the y and the z is swapped(x to the right, z upward, y goes away from us). I think that makes the euler rotations wrong. I tried to swap the rotations (y<->z) but it didn't work :( I also tried to negate these rotations but the result was the same :( Could you help how can I convert the bone-data into the directx coord system?
Advertisement
I put up with a mirror image mesh (mirrored through the YZ plane) and mirrored animations to use the skeleton and animation frames as-is.

To read each triangle to add to the mesh:
f >> junk >> px >> py >> pz >> nx >> ny >> nz >> tu >> tv;// CORRECTION to vertex posfloat temp = py;py = pz;pz = -temp;// CORRECTION to vertex normaltemp = ny;ny = nz;nz = -temp;// CORRECTION to tex coordstv = 1.0f-tv;// ... followed by boneweights

With some SMDs you have to add an x-rotation of pi/2 to the skeleton root to change from +z = up to +y = up.

Also, be careful about throwing away the node id for the vertex (as I do above). Strictly speaking, that node id is the bone with the highest bone weight. If the boneweights for a vertex add up to less than 1.0f, then that node id should be added to the boneweights for that vertex with a weight of 1.0f - boneWeightSum.

I haven't run into an SMD where the boneweights for a vertex didn't sum to 1, but the file spec allows it (apparently, according to the valve wiki).

EDIT: If you're interested, I have a conversion utility for SMDs to x-files at
Veazie.org.

It has limitations (the skeleton must have a single root). I haven't written the tutorial for it yet, but it will also load SMD animation files and allow you to combine several animations (naming each) into a single animated x-file.

[Edited by - Buckeye on July 28, 2008 7:13:03 PM]

Please don't PM me with questions. Post them in the forums for everyone's benefit, and I can embarrass myself publicly.

You don't forget how to play when you grow old; you grow old when you forget how to play.

Thanks for your reply.
As I said I can render the model correctly:

fin >> vertex.x >> vertex.z >> vertex.y;
fin >> vertex.nx >> vertex.nz >> vertex.ny;
fin >> vertex.u >> vertex.v; // texture U and V

vertex.v = -vertex.v;

I have problem with the skeleton.
From the half-life 1 sdk doc:
<ID> is the bone <PosX>, <PosY> and <PosZ> are the position in world units (good to 6 significant digits). <RotX> <RotY> and <RotZ> are local Euler rotations in radians. Bones which are not children of the world report their position and rotations in their parent’s local space.


In my node structure:
vPosition.x = pos.x
vPosition.y = pos.y
vPosition.z = pos.z

rotation[0] = RotX
rotation[1] = RotY
rotation[2] = RotZ

I draw the skeleton with a recursive function:
void RenderChildren(D3DXVECTOR3 lastGlobalPos, t3DNode* parent, D3DXQUATERNION lastQuat) {    for (int i = 0; i < parent->pChildNodes.size(); ++i) {        D3DXQUATERNION Dquat;        D3DXQuaternionRotationYawPitchRoll(&Dquat, parent->rotation[1], parent->rotation[0], parent->rotation[2]);        D3DXQuaternionNormalize(&Dquat, &Dquat);        D3DXQUATERNION DcurrentQuat;        D3DXQuaternionMultiply(&DcurrentQuat, &lastQuat, &Dquat);        D3DXQuaternionNormalize(&DcurrentQuat, &DcurrentQuat);        D3DXMATRIX childMat;                D3DXMatrixAffineTransformation(&childMat, 1.0f, NULL, &DcurrentQuat, NULL);        CVector pos = parent->pChildNodes->vPosition;        D3DXVECTOR3 childPos(pos.x, pos.y, pos.z);        D3DXVec3TransformCoord(&childPos, &childPos, &childMat);        childPos += lastGlobalPos;        RenderBone(lastGlobalPos, childPos);		        RenderChildren(childPos, parent->pChildNodes, DcurrentQuat);    }}void RenderBone(D3DXVECTOR3 parentGlobalPos, D3DXVECTOR3 childGlobalPos) {    SVertex vertList[2];    vertList[0].x = parentGlobalPos.x;    vertList[0].y = parentGlobalPos.y;    vertList[0].z = parentGlobalPos.z;        vertList[1].x = childGlobalPos.x;    vertList[1].y = childGlobalPos.y;    vertList[1].z = childGlobalPos.z;        renderLine(vertList, 2);}



What should I do with the euler rotations values (maybe swap or negate some of them)?
And I call RenderChildren(lastGlobalPos, rootNode, identityQuat) where
lastGloabPos is the position of the first node
rootNode is the first node in the skeleton (0 index)
identityQuat (0, 0, 0, 1)

Maybe I have to use
D3DXQuaternionMultiply(&DcurrentQuat, &Dquat, &lastQuat);
instead of
D3DXQuaternionMultiply(&DcurrentQuat, &lastQuat, &Dquat);
If you're satisfied with a mirror image and movement, then use the angles as-is. Your code has transformed the mesh, assuming the translations and rotations will be left-handed (mirror-image).

I use matrices rather than quats (it's just more straight forward to me). I haven't looked into applying quats so I can't comment on your code.

FYI:

The bone matrix is, in pseudo-code:

boneMatrix = rotx(xangle)*roty(yangle)*rotz(zangle)*trans(x,y,z);

childTransform = childMatrix*parentMatrix; [EDIT to correct order of multiplication]

I'm not sure whether QuaternionYawPitchRoll produces the same orientation as rotx*roty*rotz.

EDIT: note that, in spite of what the wiki says, they're not really Euler angles. Euler angles are the angles of a vector with respect to each axis. From my experience, the angles are angles of rotation about each axis in order. You're using them that way, so that's okay. [smile]

[Edited by - Buckeye on July 29, 2008 10:19:20 PM]

Please don't PM me with questions. Post them in the forums for everyone's benefit, and I can embarrass myself publicly.

You don't forget how to play when you grow old; you grow old when you forget how to play.

I did it somehow! :) thanks for the help!

void C3DModel::RenderChildren(D3DXVECTOR3 lastGlobalPos, t3DNode* parent,  D3DXMATRIX lastMat) {        for (int i = 0; i < parent->pChildNodes.size(); ++i) {    	D3DXMATRIX matRotX,matRotY,matRotZ,matTrans;	// Calculate rotation matrix	D3DXMatrixRotationX(&matRotX, parent->rotation[0]);	D3DXMatrixRotationY(&matRotY, parent->rotation[1]);  	D3DXMatrixRotationZ(&matRotZ, parent->rotation[2]); //        D3DXMATRIX childMat = lastMat * matRotX * matRotY * matRotZ;        D3DXMATRIX childMat = matRotX * matRotY * matRotZ * lastMat;        CVector pos = parent->pChildNodes->vPosition;        D3DXVECTOR3 childPos(pos.x, pos.y, pos.z);        D3DXVec3TransformCoord(&childPos, &childPos, &childMat);	childPos += lastGlobalPos;        RenderBone(lastGlobalPos, childPos);        RenderChildren(childPos, parent->pChildNodes, childMat);    }}


But now I'm a little bit confused:
I have to do
D3DXMATRIX childMat = matRotX * matRotY * matRotZ * lastMat;

instead of
D3DXMATRIX childMat = lastMat * matRotX * matRotY * matRotZ;


Why?
Maybe the reason is the different between left-handed and right-handed coord system?
is there any link or doc about this opengl/directx coord system different?
Quote:Maybe the reason is the different between left-handed and right-handed coord system?

Nope. It's just the way that the SMD specifies the relative rotations and translations.
Quote:is there any link or doc about this opengl/directx coord system different?

There are some discussions, but it's just a simple fact that OpenGL uses a right-hand coordinate system (HL & HL2 chose right-handed and z pointing up) and DirectX uses a left-hand coordinate system. No more complicated than that. They're just choices that are made. Microsoft often does things just to be different! [wink]

By the way, you can save a lot of calculations per frame by precalculating all the combined matrices.

As I load the skeleton translations and rotations, I combine the individual node local rotations and translations and store it with the node.
node->local = rotx*roty*rotz*trans;

Note that I include the node translation. You're simply doing that with your childPos += lastGlobalPos;

After the nodes are all loaded, I calculate each node's global matrix.
D3DXMATRIX mat;D3DXMatrixRotationX(&mat,-1.570796f); // rotate about x-axis to match the meshfor(int i=0; i<(int)nodes.size(); i++) {   if( nodes.at(i).parentId == -1 ) CombineTransforms(nodes.at(i).id,mat);}...void CD3DApp::CombineTransforms(int nodeId, D3DXMATRIX& P){	NODE	*node = NodeWithId(nodeId);	D3DXMATRIX& L = node->local;	D3DXMATRIX& C = L * P;	node->global = C;	int siblingId = node->siblingId;	int childId   = node->childId;	if( siblingId > -1 ) CombineTransforms(siblingId,P);	if( childId > -1   ) CombineTransforms(childId,C);}

Then, to render the bones, drawing a line from the parent to the child (as you do,also) --
	self = D3DXVECTOR3(selfNode->global._41,selfNode->global._42,selfNode->global._43);	// process child	childNode = NodeWithId(mnodes,childId);	child = D3DXVECTOR3(childNode->global._41,childNode->global._42,childNode->global._43);	vb->Lock(0,0,(LPVOID*)&v,0);	v[0].pos = self;	v[0].clr = color;	v[1].pos = child;	v[1].clr = color + 0x000f0f0f;	vb->Unlock();

This is followed by a recursive call to this DrawBones call for the child to draw to its children, and for the sibling of the child (if any) to draw to its children, etc., similar to your loop drawing the bones of all the children of each parent. Just a different node structure.

But, calculating all the global matrices once and using them to render the bones is much faster than calculating the matrices every frame.

Please don't PM me with questions. Post them in the forums for everyone's benefit, and I can embarrass myself publicly.

You don't forget how to play when you grow old; you grow old when you forget how to play.

This topic is closed to new replies.

Advertisement