4.9 使Bone独立运动:模型动画

问题

你想独立的移动模型的每一部分。例如,你想摇低一辆车的车窗或让车轮转动。

解决方案

如教程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的矩阵时,炮塔,炮管,翻盖都会旋转,因为它们都是链接到炮塔上的。

下面是你必须要记住的重点:

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); 
};

程序截图


发布时间:2009/6/8 下午2:05:13  阅读次数:12913

2006 - 2024,推荐分辨率 1024*768 以上,推荐浏览器 Chrome、Edge 等现代浏览器,截止 2021 年 12 月 5 日的访问次数:1872 万 9823 站长邮箱

沪 ICP 备 18037240 号-1

沪公网安备 31011002002865 号