65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 XNA 为 Blender 3D 模型中的单个骨骼制作动画

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (15投票s)

2011年7月24日

Ms-PL

19分钟阅读

viewsIcon

116087

downloadIcon

4090

许多现有的演示和教程展示了如何播放 3D 应用程序中生成的动画。本教程演示了如何仅通过代码来操作 3D 模型,以在 Blender 中生成的简单 3D 模型中查找和操作骨骼。

Finished product: a bendy tube

目录

引言

周围有一些很好的例子,展示了如何在 3D 包中制作动画并在 XNA 中回放它们,但如果您希望您的动画不仅仅是标准的“行走”、“跑步”、“跳跃”等,该怎么办?如果您希望您的 3D 模型与其 3D 环境交互,该怎么办?那么,您需要通过代码操作来为您的 3D 模型设置单个部分的动画。本教程旨在展示如何使用 Blender 开发一个简单的 3D 模型,放入一些骨架骨骼,最后如何在 XNA 中使用 C# 操作它们。

背景

在任何 3D 游戏中,都有许多物品与其环境交互,从随微风摇曳的树木到拥有机关枪的怪物!所有这些游戏资产或模型都通过使用骨骼或骨架进行移动。骨骼最简单的形式可以与人骨进行比较:一个动画化的人可能在其手臂中有可以相对于彼此移动但保持连接的骨骼。然而,有时相似性就到此为止:3D 动画世界中的骨骼不必遵循与现实世界中的动物相同的规则。骨骼不必位于身体内部,因为它们在我们的 3D 世界中是不可见的。我们动画世界中的人类躯干可能只有一个骨骼,而不是真正人类拥有的许多复杂骨骼。骨骼的复杂性仅取决于我们想要的动画的复杂性。

我发现关于在 XNA 中操作模型中单个骨骼的文档很难找到,尤其是在 XNA 4.0 或 Blender 2.5 中,所以通过这个演示,我试图将各种信息来源汇集到一个单一的来源中。本文是站在巨人的肩膀上写成的,但希望它能为普通人提供有用的参考。

您将需要

以下所有产品均为免费。

如果您想在皮肤上做一些漂亮的绘画,您可能需要使用 Paint.NET,但这不是必需的:http://paint.net/

本文基于 Microsoft Skinned Model 示例,您可以在此处获取原始代码:http://create.msdn.com/en-US/education/catalog/sample/skinned_model

任务

首先,我希望在这里向您展示如何

  1. 在 Blender 中生成一个简单的网格(一个管)
  2. 使用 UV 展开为网格添加蒙皮
  3. 向网格添加骨架(骨骼)
  4. 告诉网格当骨架/骨骼移动时如何变形
  5. 将网格、骨架和蒙皮导出为 XNA 可用的格式
  6. 通过自定义内容管道将模型导入 XNA
  7. 通过代码,通过移动骨架来操作网格

Blender

为了能够使用我们将在 Blender 中生成的模型,XNA。我将使用一个修改版的 SkinningSampleContent 管道,但在我们使用它之前,我们的网格需要一些东西,以便管道能够正确处理它。它需要

  1. 至少有一个骨骼的网格
  2. 我们的网格中的每个顶点都需要绑定到至少一个骨骼(是的,它们可以绑定到多个骨骼)
  3. 网格上的蒙皮(我们将使用 UV 展开来实现此目的)

听起来很简单,对吧?好了,让我们开始吧。如果您还没有下载 Blender,请从 Blender.org 下载。

我是基于 Blender 2.57.1 编写的 - 您需要 2.5 或更高版本;用户界面在 2.5 版本时发生了巨大变化,现在它是一个非常出色的工具,但过去由于 UI 难以使用而受到限制。

步骤 1:创建我们的圆柱体

  1. 默认情况下,当 Blender 启动时,它会创建一个包含立方体、相机和灯光的默认场景。选择 Blender 生成的立方体(右键单击选择,然后按 'x',然后选择 'Delete')。
  2. 确保您的“3D 光标位置”设置为 (0,0,0);如果不是,请按 'n'(鼠标指针在 3D 视图上时)调出 3D 窗口属性,向下滚动到“3D Cursor Location”,然后将鼠标悬停在任何字段上,按 '0' 将光标设置为原点(如果您喜欢长按,也可以将它们全部输入为零)。
  3. 现在添加一个圆柱体(Shift+A -> Mesh -> Cylinder)。
  4. 当您在 Blender 中执行操作时,在工具面板(见图)下方,您会看到我们最新操作(此处为添加圆柱体)的设置,将深度更改为 16。我们的圆柱体需要足够大才能进行操作。

图 1:我们的管形模型,并标出了 UI 的各个部分

步骤 2:使用 UV 展开在我们的管道上为网格添加蒙皮

我们的管道在 XNA 可以使用之前,需要包裹上一些皮肤。为此,我们将对我们的管道进行“UV 展开”,然后用它产生的皮肤来包裹我们的管道。顾名思义,UV 展开会剥离我们的管道,让我们可以在上面绘制皮肤。

  1. 确保只选择了管状模型,然后按 'tab' 进入编辑模式(您可能已经从步骤 1 进入了编辑模式)。
  2. UV 展开的作用是将我们的 3D 表面“展平”到 2D,以便我们可以用图形工具进行编辑,但在展开之前,我们需要告诉 Blender 如何展开我们的模型。我们通过在模型上定义接缝来做到这一点,Blender 将使用这些接缝来分开管道。您可以定义任意数量的接缝,并将其分解成小块。今天我们将定义 3 条接缝,一条围绕顶部周长,一条围绕底部周长,一条沿着一个边缘。
  3. 将 Blender 置于“边缘选择”模式('ctrl+tab')。
  4. 在模型顶部的一个边缘上按 'Alt+右键单击'(在编辑模式下)以选择一个循环(一个非常有用的快捷键组合,需要记住)。
  5. 按 'Ctrl+e' 并选择 'Mark Seam'。
  6. 对另一端的边缘以及管子的另一端重复此操作(想象一下从一个锡罐上切下两端,然后用一把剪刀沿着罐子的长度切开)。
  7. 右键单击 3D 窗口的顶部分割线,选择“Split”,然后拖动线条将屏幕分成两半,在一个窗口中关闭工具('t')和属性('n'),然后在模式选择区域,将窗口更改为“UV/Image editor”。
  8. 回到 3D 视图,确保您处于编辑模式(tab)并且整个对象都已选中(a),然后按 'u' 并选择 'Unwrap'。您应该能在“UV/Image editor”窗口中看到展开的图像。
  9. 现在我们需要生成一个图像来保存我们可以绘制到圆柱体上的精美艺术品。在我们的 UV 展开下方按加号(+ new)以生成一个新图像。
  10. 将我们的图像置于“Image edit”模式并涂鸦(我画了几个点,我不是艺术家!)。
  11. 在我们的 UV 窗口下的“image”菜单中,选择“Save image”。如果您想跟着做,请将其命名为“CylinderSkin.png”。我们将在 XNA 中需要它。
  12. 当我们的圆柱体处于纹理模式时,您应该能看到这些点,它们被拉伸了,因为 UV 不是成比例的。我在这里不讲这个,也许在另一个教程中会讲,但现在没关系。

UV 展开可能会很棘手,我强烈建议您在 YouTube 上搜索“Blender 2.5 UV unwrap”,有一些很棒的教程会极大地帮助您。用文字很难描述这个过程。

图 2:我们的管形模型显示红色接缝

图 3:UV 展开的管形模型,并标出了 UI 的各个部分

步骤 3:细分我们的圆柱体

到目前为止,我们的圆柱体只是一个直的实心圆柱体。在我们能够弯曲它之前,它需要在中间有可以弯曲的地方。一个好的类比是,目前我们有一个金属圆柱体,我们无法弯曲它,我们想在金属圆柱体中设置可以变形的地方,也许可以有效地将我们的金属圆柱体变成一个柔性软管。有几种方法可以做到这一点:我们可以使用 'w' 来细分,但问题是它会细分所有侧面。对于这个简单的模型来说这没问题,但随着模型变大,它会很快失控,因为我们会得到数千个我们不需要的顶点。所以我们将使用切割方法进行细分。本质上,我们将沿着我们的圆柱体的侧面切片。

  1. 确保我们的圆柱体处于编辑模式(tab),并且所有节点都没有被选中(a),并确保它是黑色的(未选中)。
  2. 切割我们的圆柱体(ctrl+r),点击您的左鼠标按钮,它会在圆柱体中间位置添加一条切割线。
  3. 滚动鼠标滚轮将细分圆柱体,添加约 10 次细分。细分次数越多,我们的动画看起来就越平滑,但我们需要渲染的顶点也越多(还有其他方法可以使我们的模型显得更平滑,我们可以使用修改器来细分我们的网格,但这超出了本教程的范围)。

图 4:我们的圆柱体显示接缝和细分侧面

步骤 4:创建我们的骨骼

找到圆柱体的底部中间。如果您遵循了之前的步骤,可以通过将“3D 光标视图”在 Z 轴上修改为 -8 来实现(在 Blender 中,Z 是向上)。(您需要处于对象模式才能执行此操作,使用 'tab' 切换回对象模式。)

  1. 现在添加一个骨架(Shift 'a' -> Armature -> Single bone)。
  2. 按下 'tab' 按钮,这将使您进入“Edit”模式(现在是骨架,而不是圆柱体)。
  3. 可能看起来什么都没发生,但您会注意到白色圆圈稍微向上移动了一点;在圆圈内左键单击并向上拖动屏幕,直到圆柱体的一半(如果您愿意,可以按 'z' 将骨架的移动限制在 z 轴上)。
  4. 好的,您应该有一个单独的骨骼。现在按 'e' 挤出另一个骨骼,再次拖动它,使其超过圆柱体的顶部(请记住,骨骼只是模型的骨架,它们不是可见项,实际上可以位于网格之外,如果我们愿意的话)。
  5. 您现在应该看到类似图 2 的内容。
  6. 我们要为我们的骨骼命名,以便在 XNA 中找到它们并进行操作。
  7. 在编辑模式下,选择骨架的底部骨骼。在属性面板(按 n 调出)的“item”下,将名称设置为“BottomBone”(见图)。
  8. 为另一根骨骼重复此操作,将其命名为“TopBone”。

图 5:命名我们的骨骼

步骤 5:将您的圆柱体绑定到您的骨骼

在我们的骨骼(骨架)能够影响我们的网格之前,我们需要将它们联系起来。您现在可以操作骨骼了,方法是按 Ctrl-Tab 进入“pose mode”(或从 3D 视图底部的模式选择下拉列表中选择)- 请记住,这只能从对象模式下选择骨架时才有效。所以,让我们将它们绑定在一起。

  1. 确保您处于对象模式(按 tab 可退出编辑模式)。
  2. 右键单击选择圆柱体,然后按住 Shift 键并右键单击您的骨架(骨骼),这样两者都被选中。
  3. 现在选择 'Ctrl-p' - 这将使这两个对象成为父子关系。现在最简单的选择是选择 'With Automatic Weights' - 这将根据顶点在骨骼中的位置按比例分配权重。

好的,等一下!这是怎么回事?还记得在开头,XNA 中动画模型的其中一个要求是每个顶点都分配给至少一个骨骼。如果您遗漏了一个,在将模型加载到 XNA 时就会出错。我们在这里所做的是让 Blender 为我们进行顶点加权;我们可以手动进行“weighted paint”(如果您想查看,可以先选择圆柱体,然后将模式更改为“weight paint”(在 ViewPort 阴影中选择“solid”时效果更好))。

图 6:使用“Automatic Weights”将我们的圆柱体父级化到骨骼

步骤 6:玩转骨骼!

  1. 一切顺利的话,我们应该已经将圆柱体绑定到了骨骼,只右键单击骨架(而不是圆柱体)。
  2. Control-tab 应该会将您带入“pose”模式;在 pose 模式下,您可以操作您的骨骼来使圆柱体弯曲。
  3. 选择 TopBone(右键单击);现在在底部会显示一个白色的圆圈,在里面左键单击并左右拖动,您的管子应该会跳一段舞!
  4. 更好的是,您还可以将视口着色从线框重新设置为纹理,您也应该能看到我们的皮肤(图像)。

图 7:移动骨骼,注意白色圆圈中的鼠标指针

步骤 7:导出到 XNA

在导出之前,您需要启用 XNA 导出器;它默认未启用;Blender 中启用的标准 'fbx' 格式无法在 XNA 中使用。

  1. 在屏幕顶部的“File”菜单中,选择“User Preferences...”。
  2. 在“Import-Export”部分,启用“Export XNA format (.fbx)”。
  3. 回到我们的 3D 窗口,选择“file->export->XNA FBX。将模型保存文件命名为“AnimatedTube.fbx”。

我们完成了 Blender 的工作,我们有两个 XNA 需要的文件

  • AnimatedTube.fbx,我们的模型
  • CylinderSkin.png,我们的蒙皮图像

图 8:将 XNA 添加为导出选项

XNA

所以我们现在有了一个模型,应该可以在 XNA 中使用。要将模型引入我们的游戏,我们需要通过内容管道导入它。内容管道获取我们的内容并对其进行标准化,使其在我们的游戏中显示为标准的结构化对象,我们可以在游戏中使用它们。XNA 部分的项目基于 MS 提供的示例。Skinned model 示例通过一个内置动画(行走)的现有模型来加载其模型。虽然这很棒,但我想演示如何操作一个关节,而不是播放预定义的动画(顺便说一句,如果您使用我演示的与“dude”模型相同的代码,您可以让“dude”做出一些非常不可能的体操动作!)。

内容管道

Skinned model 示例通过自定义内容管道加载其 'dude.fdx' 模型。我们将对管道进行一些修改,使其不加载我们“tube”模型的动画,因为我们没有动画。要做到这一点,请打开 Skinned Model 解决方案,转到 SkinnedModelPipeline 项目,删除 ProcessAnimations(两者)方法,并注释掉动画字典的初始化。

// Convert animation data to our runtime format.
//Dictionary<string, AnimationClip> animationClips;
//animationClips = ProcessAnimations(skeleton.Animations, bones);

并在以下行中为我们的 animationClips 传递 null

model.Tag = new SkinningData(animationClips, bindPose, inverseBindPose, skeletonHierarchy);

更改为

model.Tag = new SkinningData(null, bindPose, inverseBindPose, skeletonHierarchy);

我们在这里所做的是告诉内容管道停止在我们的模型中查找动画 - 当然,如果您有动画,请保留此设置。您可以进行更彻底的重写,但我希望对 Microsoft 示例的修改尽可能少。有趣的是,在 SkinnedModel 示例中,Microsoft 选择不扩展 Model 类,而是将额外的蒙皮数据附加到标准 Model 类的 tag 字段中。

导入我们的模型

导入我们的模型很简单:将这两个文件拖放到“SkinnedSampleContent”项目中。完成之后,选择模型(AnimatedTube.fbx),然后在 Properties 中,将其“ContentProcessor”属性更改为“SkinnedModelProcessor”。这样,XNA 就知道在导入我们的模型时,它将通过我们刚刚修改的自定义管道进行。

这里有一个小陷阱,如果您的模型因某种原因无法导入,您无法调试内容管道。当它执行导入时,管道似乎没有在调试器中运行。我还不确定如何解决这个问题(如果有人知道,请分享!)。

图 9:确保“Content Processor”设置为使用“SkinnedModelProcessor”管道

SkinnedModelWindows 项目中的 AnimationPlayer 类

MS Skinning 示例附带的动画播放器用于读取 fbx 文件中的动画关键帧并执行它们表示的动画。我们的 fbx 文件没有任何动画,所以我们可以移除 UpdateUpdateBoneTransforms。我所做的唯一一件事是使 BoneTransformsWorldTransformsSkinTransforms 公开(您在长期运行时不会这样做,但它允许我们在此处进行快速演示)。

// Current animation transform matrices. 
public Matrix[] boneTransforms; 
public Matrix[] worldTransforms; 
public Matrix[] skinTransforms;

稍后我们将需要它,以便能够直接访问变换。

SkinningSample 项目

现在我们开始实际操作我们的骨骼。为此,我们需要将一个变换应用于我们的一个骨骼。为此,我们获取初始的 BindPose 并将其存储起来,然后当我们想要操作骨骼时,我们是相对于骨骼的初始状态进行的。我们需要一个地方来存储这些初始状态;我们在字段部分有一个数组来存储初始状态。

Matrix[] _originalBonesMatrix;

现在我们修改 SkinningSample 类中的 LoadContent(这里是我们的主代码运行的地方),如下所示:

/// <summary>
/// Load your graphics content.
/// </summary>
protected override void LoadContent()
{
    // Load the model.
    currentModel = Content.Load<Model>("AnimatedTube");
    // Look up our custom skinning information.
    SkinningData skinningData = currentModel.Tag as SkinningData;

    // copy the initial bone transforms to keep for later,
    // all new transforms will be performed
    // relative to the initial position
    _originalBonesMatrix = new Matrix[skinningData.BindPose.Count];
    
    // make copy of the initial BindPose data
    // by iterating through Bones in the BindPose 
    // and copying their skinning data into our store of initial info.
    int currentBone = 0;
    while (currentBone < skinningData.BindPose.Count)
    {
        _originalBonesMatrix[currentBone] = skinningData.BindPose[currentBone];
        currentBone++;
    }
    if (skinningData == null)
        throw new InvalidOperationException
            ("This model does not contain a SkinningData tag.");

    // Create an animation player
    animationPlayer = new AnimationPlayer(skinningData);

    // Copy our bonetransforms into our model, displaying our movement
    skinningData.BindPose.CopyTo(animationPlayer.boneTransforms, 0);
}

现在,为了理解 XNA,理解它的运行方式很重要。

  • 第一:它通过内容管道传递我们的内容,使其可供 XNA 使用。
  • 第二:它执行 LoadContent 将我们的内容加载到当前运行的游戏中。
  • 第三:它现在处于一个连续的循环中(直到我们退出或它崩溃),从 Update 方法跳转到 Draw 方法。我们的任务是在这些方法中编写内容;传统上,Update 会更新相机、网格、骨骼、输入等,而 Draw 只会将所有内容绘制到屏幕上。

首先,我们修改后的 Update 方法

protected override void Update(GameTime gameTime)
{
    HandleInput();
    UpdateCamera(gameTime);
    // so we can see movement without pressing any controls
    // we'll use the game time to introduce a Y axis bend
    float time = (float)gameTime.ElapsedGameTime.TotalMilliseconds;
    bendY += time * 0.001f;
    // find the matrix index for the bone we want to move (this doesn't check
    // to see if you have a bone called this so be carful, or even better add some checking)
    int boneId = currentModel.Bones["TopBone"].Index - 2;
    // find the matrix index Bone by multiplying by it's original 'BindPose' position
    animationPlayer.boneTransforms[boneId] = 
      Matrix.CreateFromYawPitchRoll(0, bendY, bendZ) * _originalBonesMatrix[boneId];
    // update the world position 
    animationPlayer.UpdateWorldTransforms(Matrix.Identity);
    // update the skin transforms based on new bone movement and world matrix 
    animationPlayer.UpdateSkinTransforms();
    base.Update(gameTime);
}

关注点

int boneId = currentModel.Bones["TopBone"].Index - 2;

这可能看起来很奇怪,我减去了 2,但如果您检查对象,一切都会有意义,我们的 Bind Pose 由三组矩阵组成

  1. 基础圆柱体位置
  2. BottomBone
  3. TopBone

但我们的网格由 5 个对象组成

  1. Root Node
  2. 圆柱体
  3. Armature
  4. BottomBone
  5. TopBone

这就是我们实际执行骨骼移动的地方。我们获取骨骼的原始位置,并乘以旋转变换,然后将其复制回 boneTransforms 以供 Draw 应用。

animationPlayer.boneTransforms[boneId] = 
  Matrix.CreateFromYawPitchRoll(0, bendY, bendZ) * _originalBonesMatrix[boneId];

最后,为了好玩,让我们添加另一个键盘按键处理程序,以便我们可以操作我们的骨骼,所以进入 UpdateCamera,我们添加

if (currentKeyboardState.IsKeyDown(Keys.V))
{
    bendZ += 0.1f;
}
if (currentKeyboardState.IsKeyDown(Keys.B))
{
    bendZ -= 0.1f;
}

另一个需要注意的点是 Blender 中的轴“Z”轴垂直于屏幕,而 XNA 中的“Z”轴直接从屏幕向外和向内。所以为了解决这个问题,在 SkinningSampleGame 类的字段部分添加以下内容;这将使我们的模型旋转 90 度。

float cameraArc = -90;

运行时

我在下载中包含的代码仅适用于 Windows 版本,您可以随意在 Phone 7 或 Xbox 360 上运行它。一切顺利的话,您现在应该可以运行该解决方案,然后按 'v' 和 'b' 键来弯曲圆柱体。

术语表

  • Bone:一个不渲染的人工构造,用于方便地操作网格的顶点组
  • BindPose:当我们的所有骨骼从 Blender 导出且未进行任何操作时的初始位置
  • Model:我们的网格、骨骼、纹理的组合构成我们的完整模型
  • Weight Paint:对我们的网格进行人工着色,以显示网格如何绑定到骨骼
  • Armature:一个或多个骨骼的集合,以分层方式排列

您可能会遇到的错误消息及其修复方法

“网格“Cylinder”,使用 SkinnedEffect,包含的几何图形缺少通道 0 的纹理坐标”

首次运行代码时可能会遇到此问题。您的代码甚至不会命中断点,它只是(似乎)无法编译。这是因为内容管道已经检查了您的模型,但找不到 UV 展开的纹理。请回到 Blender,确保您已完成 Blender 部分的步骤 2。

“错误规范化顶点骨骼权重。BoneWeightCollection 不包含任何加权值。”

您的网格中有未绑定到任何骨骼的节点,这在 XNA 中是不允许的。您需要回到 Blender,确保正确地将您的网格绑定到骨骼。使用加权绘制视图检查,确保所有节点都已着色。

资源

总结

自从我用 Amstrad 6128 的第一天起,我就一直想写一个游戏,真遗憾花了这么长时间!我惊讶地发现找到这类资源的难度很大,并计划在反响好的情况下撰写更多内容。如果您有疑问,请告诉我您在这方面做得怎么样,以及 Blender 部分是否清晰。我确实走了几次,但有些概念很难解释。如果您有任何问题,请给我发邮件。

好了,现在该回到我的游戏了!

历史

初始版本。

© . All rights reserved.