XNA:初学者入门






4.91/5 (34投票s)
XNA、Blender 和 3D 建模入门

引言
这是一篇介绍 XNA 开发的文章,讨论了使用 XNA Game Studio 及其在 Blender 中创建的简单模型所需的工具和最少代码量。
工具
要使用 XNA Game Studio 编写应用程序(例如游戏),您必须下载并安装各种工具。由于 Microsoft 经常更改其网站,因此这些链接不保证有效。如果您无法访问下载页面,请使用 Google 搜索新的链接,并使用适当的关键词。
本文使用以下软件编写:
- Visual Studio C# 2005 Express Edition SP1
- XNA Game Studio Express 1.0 Refresh
- XNA Framework 1.0 Refresh
- Blender 2.44
- Python 2.51
Visual Studio Express
截至本文撰写之时,XNA 开发只能使用 Visual Studio Express 完成。
XNA
XNA 开发需要 Game Studio Express (GSE) 和 XNA Framework
建模工具
如果您要用 XNA 做任何“严肃”的事情,您很快就会发现您需要一个建模工具来创建您的 3D 模型。我推荐 Blender(免费)或 AutoDesk Maya(2000 美元)。猜猜我将在本文和其他文章中提到哪个工具?
Maya PLE 不能用于为 XNA 创建模型(据我所知),因为它不能导出 XNA 所需的格式。
插件
您可能希望利用 Blender 中的插件和脚本,它们使用 Python。
- 在此处下载 Python
Blender 会自动检测 Python 安装,因此您无需进行其他操作。
教程
关于 XNA 和 Blender 的教程非常丰富,分别在 Microsoft 和 Blender 网站以及许多第三方网站上。只需 Google 搜索,您就会找到许多资源。我首先通过 Microsoft 教程来显示 3D 模型。请注意我在此处描述的宽高比错误。它不是致命的,但如果您将 Microsoft 教程作为处理 Blender 中创建的模型的基础,它将导致对象拉伸。
创建 Windows XNA 项目
启动 Visual Studio C# 2005 Express Edition。
创建 Windows 游戏项目
通过选择 Windows 游戏项目图标创建 Windows 游戏项目

输入项目名称并单击“确定”。
创建模型文件夹
模型(宇宙飞船、人物、对象等)通常位于 Content\Models 文件夹中。右键单击项目并添加 Content 文件夹和 Models 子文件夹,这样您的项目树看起来像这样:

如果您构建并运行此项目,您将获得一个带有蓝色字段的窗口。
在 Blender 中创建模型
启动 Blender。Blender 初始模型是一个立方体,尽管您从顶部看立方体,它看起来像一个正方形。

要查看它实际上是一个立方体,请单击数字键盘上的 0(打开数字锁定),您将看到:

导出模型
XNA 无法加载 Blender 文件,因此您必须将 Blender 模型导出为 FBX 格式。您可以在此处阅读有关 FBX 的一些信息。它基本上是 AutoDesk 创建的一种开放标准、平台无关的 3D 文件格式。您也可以导出为 DirectX (.x) 格式,但这会弹出一个复杂的选项对话框,而 FBX 导出则不会。
Blender UI 对 Windows 用户来说可能有点奇怪,所以这里是操作步骤:

- 在顶部的菜单栏上,单击“文件”(不要尝试使用典型的 Alt-F 组合键!)
- 将鼠标向下移动到“导出”
- 将鼠标移到“Autodesk FBX”
- 将出现一个对话框,其中包含两个文本框,第一个显示路径,第二个用于输入文件名。单击第二个编辑框并键入“cube”(不带引号)。
- 单击“导出 FBX”按钮或按两次 Enter 键。
将模型导入 XNA
现在模型已准备好导入 XNA
- 返回 XNA Game Studio,右键单击“模型”文件夹并选择“添加/现有项...”。
- 在文件类型中,选择“内容管线文件”
- 导航到 Blender 文件夹并选择 cube.fbx 文件。
添加渲染模型的代码
渲染模型包括:
- 定义模型的位置
- 定义摄像机的位置
- 定义摄像机方向
- 将模型加载到内容管线
- 绘制模型
所有这些工作都将在 game1.cs 文件和 Game1
类中完成。
定义模型的位置
为简单起见,模型放置在世界坐标 (0, 0, 0) 处。为模型位置创建字段:
protected Vector3 modelPosition = Vector3.Zero;
定义摄像机的位置
为摄像机位置创建一个字段:
protected Vector3 cameraPosition = new Vector3(0.0f, 0.0f, 10.0f);
我是如何得到这些值的?向量由模型的 x、y 和 z 轴位置组成。X 是左右,Y 是上下,Z 是进出。如果您查看上面立方体的屏幕截图,您会注意到一个网格。每条网格线代表一个单位,因此立方体宽 2 个单位,长 2 个单位,高 2 个单位。摄像机位于距离世界中心 (0, 0, 0) 十个单位的位置,这创建了一个合理大小的立方体渲染。
将模型加载到内容管线
为您的模型创建一个字段:
protected Model myModel;
在 LoadGraphicsContent
方法中,加载您的立方体模型:
protected override void LoadGraphicsContent(bool loadAllContent)
{
if (loadAllContent)
{
myModel = content.Load<Model>("Content\\Models\\cube");
}
}
请注意,您不需要指定文件扩展名。XNA Game Studio 将加载相应的文件,因为模型实际上被编译为与您添加到 Content\Models 文件夹中的 FBX 文件不同的文件扩展名。
与此同时,我们还将获取视口(我们的窗口)的宽高比,因为这对于渲染模型使其看起来像立方体是必要的。添加字段:
protected float aspectRatio;
并且,在上述方法中,计算宽高比:
protected override void LoadGraphicsContent(bool loadAllContent)
{
if (loadAllContent)
{
myModel = content.Load<Model>("Content\\Models\\cube1");
}
aspectRatio = ((float)graphics.GraphicsDevice.Viewport.Width) /
((float)graphics.GraphicsDevice.Viewport.Height);
}
绘制模型
修改 Draw
方法,使其遍历模型网格,适当地平移和旋转它们等。
protected override void Draw(GameTime gameTime)
{
graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
Matrix[] transforms = new Matrix[myModel.Bones.Count];
myModel.CopyAbsoluteBoneTransformsTo(transforms);
// Draw the model. A model can have multiple meshes, so loop.
foreach (ModelMesh mesh in myModel.Meshes)
{
// This is where the mesh orientation is set, as well as our camera and
// projection.
foreach (BasicEffect effect in mesh.Effects)
{
effect.EnableDefaultLighting();
effect.World = transforms[mesh.ParentBone.Index];
effect.View = Matrix.CreateLookAt(cameraPosition, Vector3.Zero,
Vector3.Up);
effect.Projection = Matrix.CreatePerspectiveFieldOfView(
MathHelper.ToRadians(45.0f), aspectRatio, 1.0f, 10000.0f);
}
// Draw the mesh, using the effects set above.
mesh.Draw();
}
base.Draw(gameTime);
}
当您运行程序时,您将在蓝色字段上看到一个渲染的立方体。

以下是对上述方法中发生的事情的简要描述。这些子主题中的每一个都值得我提供更多的描述,但我认为这超出了这篇初学者文章的范围。
骨骼
上面的代码是游戏中所有模型的基本渲染循环。XNA 使用骨骼动画技术,您可以在此处阅读。
CopyAbsoluteBoneTransformsTo 将骨骼层次结构扁平化并将骨骼放入数组中,以便在此赋值中可以快速获取父骨骼矩阵:
"cs">effect.World = transforms[mesh.ParentBone.Index];
或者,您可以将其写为:
"cs">effect.World = mesh.ParentBone.Transform;
然而,由于子项中的每个网格都需要访问其父骨骼变换(以便网格在父骨骼的变换基础上构建),因此索引最终比每次都要求父骨骼重新计算其变换要快。
您认为此模型有多少根骨骼?如果您回答 1,因为您认为只有一个模型,那么您部分正确。然而,始终存在一个定义骨骼树最顶层父级的根:

模型网格
一个模型由网格组成(在 Blender 文档中阅读有关网格的信息)。您认为立方体有多少个网格?预期的答案是 6——立方体的每个面一个网格。然而,立方体模型只有一个网格!令人困惑的是实际上名为 mesh
的局部变量,它应该被称为 modelMesh
,因为只有一个网格代表整个模型。因此,必须清楚 XNA ModelMesh
实例和 Blender 网格之间的区别。
立方体的 XNA 网格

由 1 个 ModelMeshPart
对象组成,该对象有 8 个顶点,这是立方体中的顶点数(以上模型有 51 个顶点)。
作为第二个示例,像这样的对象:

(其中四边形——正方形——网格已转换为一个面的三角形,并且其中一个三角形已拉伸)在 XNA 模型中仍仅表示为一个模型网格。
效果
这行代码:
foreach (BasicEffect effect in mesh.Effects)
是表达方式的简写(再次注意我们正在处理模型网格部分):
foreach (ModelMeshPart part in mesh.MeshParts)
{
BasicEffect effect = (BasicEffect)part.Effect;
...
但是,如果多个 ModelMeshPart
共享相同的效果,前者经过优化以避免冗余设置工作(参考)。
效果属性
每个模型网格都基于以下内容进行渲染:
- 它的方向
- 相机的方向
- 相机的视野
如这三个语句所表达的:
effect.World = transforms[mesh.ParentBone.Index];
effect.View = Matrix.CreateLookAt(cameraPosition, Vector3.Zero, Vector3.Up);
effect.Projection = Matrix.CreatePerspectiveFieldOfView(
MathHelper.ToRadians(45.0f), aspectRatio, 1.0f, 10000.0f);
World
属性为该模型网格(从模型的世界的角度)建立世界矩阵,这通常是父骨骼变换和仅应用于该骨骼的任何局部变换的组合。View
属性建立相机的方向——它的位置、它正在看的地方和它的方向。Projection
属性建立相机如何将其视图投射到屏幕上——视野、视口的宽高比以及相机可以看到的边界(距离):近平面和远平面。
每个模型网格都需要设置 World
属性以反映父骨骼中的任何变化和任何局部变化。当相机位置和方向改变时,需要设置 View
和 Projection
属性。在这个简单的例子中,我们可以预先计算这些值:
effect.World = transforms[mesh.ParentBone.Index];
effect.View = lookAt;
effect.Projection = projection;
其中 lookAt
和 projection
是在 LoadGraphicsContent
方法中初始化的 Matrix
对象。
lookAt = Matrix.CreateLookAt(cameraPosition, Vector3.Zero, Vector3.Up);
projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45.0f),
aspectRatio, 1.0f, 10000.0f);
动画模型
上面立方体的截图看起来非常无聊,根本不像一个立方体。让物体自行旋转是最简单的动画形式之一。以下代码将使立方体围绕 X 和 Y 轴旋转,使其具有良好的旋转效果。首先,添加到:
protected float modelRotation = 0.0f;
到 Game1
类。此字段用于保存旋转角度。
在 Update
方法中,添加一行代码以根据更新之间经过的时间递增模型旋转:
protected override void Update(GameTime gameTime)
{
// Allows the game to exit
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
modelRotation += (float)gameTime.ElapsedGameTime.TotalMilliseconds *
MathHelper.ToRadians(0.1f);
base.Update(gameTime);
}
在 Draw
方法中,修改 World
属性赋值以旋转模型网格:
protected override void Draw(GameTime gameTime)
{
graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
Matrix[] transforms = new Matrix[myModel.Bones.Count];
myModel.CopyAbsoluteBoneTransformsTo(transforms);
// Draw the model. A model can have multiple meshes, so loop.
foreach (ModelMesh mesh in myModel.Meshes)
{
// This is where the mesh orientation is set, as well as our camera and
// projection.
foreach (BasicEffect effect in mesh.Effects)
{
effect.EnableDefaultLighting();
effect.World = transforms[mesh.ParentBone.Index] *
Matrix.CreateRotationX(modelRotation) *
Matrix.CreateRotationY(modelRotation);
effect.View = Matrix.CreateLookAt(cameraPosition, Vector3.Zero,
Vector3.Up);
effect.Projection = Matrix.CreatePerspectiveFieldOfView(
MathHelper.ToRadians(45.0f), aspectRatio, 1.0f, 10000.0f);
}
// Draw the mesh, using the effects set above.
mesh.Draw();
}
base.Draw(gameTime);
}
这创建了一个漂亮的旋转立方体:

结论
本文总结了我为基本了解 XNA 以及如何使用 Blender 等工具开始开发 XNA 可以渲染的模型所做的研究和实验。我一直在这里记录我的尝试和磨难,随着我在正在开发的概念上取得进展,我将继续这样做。我主要对将 XNA 用作渲染空间而不是游戏开发感兴趣,但我也可能只是为了好玩而偏离主题,并编写一个我多年前在 Windows 3.1 中编写的简单游戏。