骨骼 skeleton就是表示其物体支架的向量。它提供了网格(meshes)分级,方便驱动一个角色的动画系统。骨骼的表面由皮肤包围,就成为一个立体模型了。这些皮肤是由向量和几何图形组成的。各个骨骼相互影响,以此达到模拟真实动作的效果,如人手运动。
与前面一般动画不同的是 root坐标不一定是世界坐标,比如是人的躯干坐标。
顶点混合( Vertex Blending)
关节处的皮肤在实际世界中是会邹起来的,如下图:
要模拟这个效果就需要用到vertex blending这个技术了。
关节处的vertex是会由两个甚至是多个骨骼影响的,那么就需要利用加权的方法,确定关节处的vertex受骨骼影响的程度。这些权一般是由美术确定的(利用maya等软件),然后保存到3d模型文件,如.x文件。设置好的话,就会产生平滑的动画,而不会像之前的机械手那么生硬的动作。
一般一个顶点最多只需要受4个骨骼影响就可以产生平滑的动画了。
毫无意外的,这些骨骼的信息都应该由一个结构体来表示,如下:
利用这样的结构体代表顶点信息,渲染这些顶点就是vertex blending了。
计算顶点v的最终位置的公式是:
v = w0*v*F0 + w1*v*F1……wn-1*v*Fn-1
等于把这些v受影响的因素都加起来了。其中w0,w1...wn-1是权重,加起来应该是1; F0,F1,...Fn-1是转换矩阵
如果有单位法向量的话,也需要转换单位法向量:
n = w0*n*F0 + w1*n*F1……wn-1*n*Fn-1
这个可以自己编程在shader中计算,shader很灵活的,这也是为什么shader要成为主流的原因之一吧。
下面是vertex shader的代码片段,假设影响vertex blending的是两个骨骼:
uniform extern float4x4 gWorld; uniform extern float4x4 gWorldInvTrans; uniform extern float4x4 gWVP; // The matrix palette. uniform extern float4x4 gFinalXForms[35]; OutputVS VBlend2VS(float3 posL : POSITION0, float3 normalL : NORMAL0, float2 tex0 : TEXCOORD0, float weight0 : BLENDWEIGHT0, int4 boneIndex : BLENDINDICES0) { // Zero out our output. OutputVS outVS = (OutputVS)0; // 计算顶点的公式算法.这个是本章重点 float weight1 = 1.0f - weight0;//因为权重加起来是1的,所以weight1+weight0 = 1; float4 p = weight0 * mul(float4(posL, 1.0f), gFinalXForms[boneIndex[0]]); p += weight1 * mul(float4(posL, 1.0f), gFinalXForms[boneIndex[1]]); p.w = 1.0f;//表示顶点,所以w值为1 // 计算单位法向量的公式算法,这个也是本章重点 float4 n = weight0 * mul(float4(normalL, 0.0f), gFinalXForms[boneIndex[0]]); n += weight1 * mul(float4(normalL, 0.0f), gFinalXForms[boneIndex[1]]); n.w = 0.0f;//表示方向,所以为值0 // Transform normal to world space. outVS.normalW = mul(n, gWorldInvTrans).xyz; // Transform vertex position to world space. float3 posW = mul(p, gWorld).xyz; // Transform to homogeneous clip space. outVS.posH = mul(p, gWVP); . . // 其他代码与本章无关. . }
骨骼Bone的数据结构可以自己写,但是D3DX提供了一个方便的类:D3DXFRAME
这个是D3DX的一个类,表示一个bone,但是为什么不直接叫bone呢?因为这个类也可以代表非bone的架构,比如房子的各个部分的表示等。
typedef struct _D3DXFRAME { LPSTR Name; D3DXMATRIX TransformationMatrix; LPD3DXMESHCONTAINER pMeshContainer; struct _D3DXFRAME *pFrameSibling; struct _D3DXFRAME *pFrameFirstChild; } D3DXFRAME, *LPD3DXFRAME;
但是这个数据结构没有带根节点,那么我们只好自己定义了。再次看的出来“数据结构和算法”的基本功夫是多么重要的。
struct FrameEx : public D3DXFRAME { D3DXMATRIX toRoot; };
然后我们可以循环所有D3DXFRAME类,建造到根节点的变换矩阵。(又是数据结构和算法的知识了)
void SkinnedMesh::buildToRootXForms(FrameEx* frame, D3DXMATRIX& parentsToRoot) { // 这里使用C++的同名变量节省一点空间. D3DXMATRIX& toParent = frame->TransformationMatrix; D3DXMATRIX& toRoot = frame->toRoot; toRoot = toParent * parentsToRoot;//相当于frame->toRoot = toParent * parentsToRoot; FrameEx* sibling = (FrameEx*)frame->pFrameSibling; FrameEx* firstChild = (FrameEx*)frame->pFrameFirstChild; // Recurse down siblings. if( sibling ) buildToRootXForms(sibling, parentsToRoot); // Recurse to first child. if( firstChild ) buildToRootXForms(firstChild, toRoot); }
然后如下调用:
D3DXMATRIX identity; D3DXMatrixIdentity(&identity); buildToRootXForms((FrameEx*)mRoot, identity);
identity表示是对角单位矩阵,也就相当于数学的1,不过这里的是矩阵的1,任何数乘以对角单位矩阵都等于1.
因为我们本来就把根节点放置到世界坐标中,所以不需要转移或者变形。