问题
你想独立的移动模型的每一部分。例如,你想摇低一辆车的车窗或让车轮转动。
解决方案
如教程4-1中解释的那样,一个模型是由可单独绘制的ModelMesh组成的。每个ModelMesh都链接到一个Bone,这些Bone互相联系,之间的位置关系是由矩阵显示的。
每个模型都有一个root Bone,所有其他Bone对象直接或间接与它链接,图4-11显示了这种结构的一个例子。
如果你想对图中的root Bone进行变换-例如,对这个root Bone进行缩放,那么它的所有child Bone对象(包括child Bone对象的child Bone对象) 会自动以同样的方式缩放。第二个例子,如果你想对存储在右前门Bone中的矩阵进行旋转,只有门本身,它的车窗和门锁会跟着旋转,这正是你需要的。
工作原理
在你对模型应用一个动画之前,你需要对模型的Bone结构有个概览,前面的教程中已经解释了如何可视化Bone结构,看到哪个ModelMeshes链接到哪个Bone对象上。
让我们看一下在XNA Creators Club网站上找到的坦克模型的Bone结构,这个结构在前一个教程中已经写过了。你有一个root Bone,当你缩放这个Bone的矩阵时,整个坦克都会进行缩放。接下来看一下炮塔的Bone,它是root的子Bone,如果你旋转这个Bone,每个链接到这个Bone的ModelMesh和它的child Bone对象都会旋转。所以当你旋转炮塔Bone的矩阵时,炮塔,炮管,翻盖都会旋转,因为它们都是链接到炮塔上的。
下面是你必须要记住的重点:
- 模型中所有的可独立绘制的,可变换的部分都存储在不同的ModelMesh对象中。每个ModelMesh对象都链接到一个Bone对象。
- 每个Bone对象存储这个Bone相对于它的parent Bone的位置,旋转和缩放信息。
- 当你设置一个Bone的变换时,这个变换还要影响到这个Bone的所有child Bone对象。
CopyAbsoluteBoneTransformsTo方法的必要性(额外的解释)
上面列表中的最后一点不是很容易。在绘制每个ModelMesh时,你需要设置它的世界矩阵,因为你想让ModelMesh放置在正确的3D空间中。问题是这个世界矩阵定义的是ModelMesh在3D空间中的位置,但是,Bone矩阵包含的矩阵存储的相对于它的parent ModelMesh的位置!
以坦克的火炮为例,火炮的Bone矩阵包含一个诸如(0,0,-2)的偏移量:相对于它的父:炮塔向前移动2个单位。
如果你只是简单地将火炮的世界矩阵设置为火炮ModelMesh的矩阵,这会导致火炮ModelMesh被绘制到相对于3D空间的初始位置(0,0,0)偏离(0,0,-2)的地方。
但这不是你想要的结果!你实际是想将火炮放置到相对于它的parent:炮塔偏离(0,0,-2)的位置。
所以在绘制火炮前,你需要将它的Bone和它的parent的Bone (将两者相乘)组合起来。而且还要回溯到根节点,因为这种情况中最终矩阵还要和炮塔的parent Bone(坦克的车身)组合。通过这种方式,你获取了火炮的最终世界矩阵。
因为这个矩阵是相对于坦克初始位置的,所以叫做火炮的绝对变换矩阵(absolute transformation matrix)。
幸运的是,XNA提供了组合这些矩阵的功能。在绘制模型前,你需要调用模型的CopyAbsoluteBoneTransformsTo方法,这个方法会计算所有的组合并将它存储在结果的绝对矩阵数组中。这些绝对矩阵不再包含相对于parent Bone的变换信息;而只包含相对与模型的root的信息。结果是,这些包含在modelTransforms数组中的矩阵包含了坦克模型中所有ModelMesh的绝对变换信息。
你可以使用这些矩阵作为模型中每个ModelMesh的绝对世界矩阵:
myModel.CopyAbsoluteBoneTransformsTo(modelTransforms); foreach (ModelMesh mesh in myModel.Meshes) { foreach (BasicEffect effect in mesh.Effects) { effect.EnableDefaultLighting(); effect.World = modelTransforms[mesh.ParentBone.Index]; effect.View = fpsCam.GetViewMatrix(); effect.Projection = fpsCam.GetProjectionMatrix(); } mesh.Draw(); }
虽然看起来变得更难了,但这样做会带来巨大的好处 。在这个例子中,只要炮塔的Bone矩阵发生旋转,炮管的(0,0,-2)平移矩阵也会跟着一起旋转。这是因为当CopyAbsoluteBoneTransformsTo方法结束后,火炮的绝对转换矩阵也会包含它的parent Bone 矩阵的旋转信息。而且,所有链接到炮塔的child ModelMesh,诸如炮管和翻盖,也会自动随着炮塔一起旋转。
设置模型中指定的ModelMesh的动画
知道了模型的结构后,现在可以实现模型动画了。
在本例中,你想提升炮管。要做到这一点,使用前一个教程中的方法看一下炮管的Mesh part与哪个Bone相链接,而你将对这个Bone的矩阵施加一个旋转。
但是,这个Bone矩阵中存储了火炮相对于炮塔的原始位置,如果你用旋转矩阵覆盖了这个矩阵,原始位置就丢失(或很难找到)!这样就无法以后对炮管施加动画了,因为你总是想从初始矩阵开始进行旋转,而这个初始矩阵的值已经丢失。
所以在加载了模型后,你需要创建一个原始Bone矩阵的备份,存储相对于parent的位置,这要用到CopyBoneTransformsTo方法:
Matrix[] originalTransforms = new Matrix[myModel.Bones.Count]; myModel.CopyBoneTransformsTo(originalTransforms);
注意:对每个ModelMesh,你都需要存储相对于parent ModelMesh的位置信息,所以你使用CopyBoneTransformsTo method。CopyAbsoluteBoneTransformsTo给你相对于模型初始位置的位置信息,如前面在“CopyAbsoluteBoneTransformsTo方法的必要性”一节中解释的。
你需要将这个代码放置在LoadContent方法中。
存储好矩阵后,你可以安全地覆盖存储在Bone对象中的矩阵了,在项目中添加一个canonRot变量:
float canonRot = 0;
可以让玩家在Update方法中调整这个变量:
if (keyState.IsKeyDown(Keys.U)) canonRot -= 0.05f; if (keyState.IsKeyDown(Keys.D)) canonRot += 0.05f;
现在你可以使用键盘控制这个变量,将以下代码放到Draw方法中,在这行代码之前还要计算模型的绝对世界矩阵:
Matrix newCanonMat = Matrix.CreateRotationX(canonRot) * originalTransforms[10]; myModel.Bones[10].Transform = newCanonMat;
在前面的教程中你可以在坦克模型的结构中看到,炮塔的ModelMesh链接到Bone 10。这个代码将炮管的相对于炮塔的初始位置存储在矩阵中,沿着向右向量旋转(使之上下旋转),并在模型中存储组合矩阵。当运行代码后,炮管会根据键盘输入上下旋转。
如果你想旋转整个炮塔,你需要对炮塔的Bone矩阵做同样的事情,首先添加turretRot变量:
float turretRot = 0;
然后在Update方法中添加键盘控制代码:
if (keyState.IsKeyDown(Keys.L)) turretRot += 0.05f; if (keyState.IsKeyDown(Keys.R)) turretRot -= 0.05f;
在Draw方法中调整对应的Bone矩阵:
Matrix newTurretMat = Matrix.CreateRotationY(turretRot) * originalTransforms[9]; myModel.Bones[9].Transform = newTurretMat;
如你在前一个教程中看到的,炮塔的Bone索引是9。首先获取初始矩阵,然后绕着Y轴旋转让炮塔左右旋转。
注意:改变炮管矩阵和改变炮塔矩阵的顺序先后不会影响结果。在绝对矩阵中的Bone对象间的关系只有在调用CopyAbsoluteBoneTransformsTo方法时才会存储。
如前所述,如果旋转炮塔,那么炮塔的children (本例中是炮管和翻盖)也会跟着一起旋转,这是因为炮管的Bone矩阵通过CopyAbsoluteBoneTransformsTo方法和它的child Bone矩阵组合在了一起。
代码
在加载模型后,请确保你存储了原始Bone矩阵:
protected override void LoadContent() { device = graphics.GraphicsDevice; basicEffect = new BasicEffect(device, null); cCross = new CoordCross(device); myModel = Content.Load<Model>("tank"); modelTransforms = new Matrix[myModel.Bones.Count]; originalTransforms = new Matrix[myModel.Bones.Count]; myModel.CopyBoneTransformsTo(originalTransforms); }
在update过程中,我们可以改变旋转角度:
KeyboardState keyState = Keyboard.GetState(); if (keyState.IsKeyDown(Keys.U)) canonRot -= 0.05f; if (keyState.IsKeyDown(Keys.D)) canonRot += 0.05f; if (keyState.IsKeyDown(Keys.L)) turretRot += 0.05f; if (keyState.IsKeyDown(Keys.R)) turretRot -= 0.05f;
最后绘制模型,你需要用旋转矩阵覆盖原始Bone矩阵并构建绝对Bone矩阵,而这些绝对矩阵必须作为ModelMesh的当前矩阵:
protected override void Draw(GameTime gameTime) { device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.CornflowerBlue, 1, 0); cCross.Draw(fpsCam.ViewMatrix, fpsCam.ProjectionMatrix); //draw model Matrix newCanonMat = Matrix.CreateRotationX(canonRot) * originalTransforms[10]; MyModel.Bones[10].Transform = newCanonMat; Matrix newTurretMat = Matrix.CreateRotationY(turretRot) * originalTransforms[9]; myModel.Bones[9].Transform = newTurretMat; Matrix worldMatrix = Matrix.CreateScale(0.01f, 0.01f, 0.01f); myModel.CopyAbsoluteBoneTransformsTo(modelTransforms); foreach (ModelMesh mesh in myModel.Meshes) { foreach (BasicEffect effect in mesh.Effects) { effect.EnableDefaultLighting(); effect.World = modelTransforms[mesh.ParentBone.Index] * worldMatrix; effect.View = fpsCam.ViewMatrix; effect.Projection = fpsCam.ProjectionMatrix; } mesh.Draw(); } base.Draw(gameTime); };