PS:自己翻译的,转载请著明出处
第十章 3D模型
在前面的章中,你想影响一些可爱的三角形和矩形,使用夸张的颜色和很酷的纹理。你已经准备走出去,准备创建一个新的伟大的游戏,在你23岁时退休挣数百万美圆,对吗?错了,不幸的是,我有一些坏消息告诉你:市场上已经没有3D三角形和矩形的游戏了。如何将您的“进攻三角”游戏比较最新的射击和角色扮演游戏?它不能。所以,这些游戏怎么得到很酷的视觉效果的图形,当所有你拥有的是一个简单的三角形?答案在于三维模型。
使用3D模型
在上一章中,我提到了你可以绘制任何你想要的在XNA3D中,如果你使用足够的primitives(就象前面章节中的三角形)。虽然这是真的,这将是一个象脖子上的痛苦一样去绘画一个飞船,一条龙或者任何你想要的,通过指定每个单独的顶点在代码中,绘制上百或者上千个三角形去创建这个对象。
一般来说,当绘制复杂对象,你会使用一个3D模型,从本质上讲,一个3D模型是一个点的集合,它来自primitives的顶点。在模型中,颜色和纹理能被应用。这些模型一般在XNA的外部创建,一个第三方模型的应用。普通的模型塑造工具你可以使用去创建3D模型包含在3D Studio Max,Maya,Blender,Lightware,和Modo.Blender是一个自由工具,操作方便,学生都喜欢的开发工具。
模型创建在这些建模程序中,能被保存为一个不同格式的文件,能被程序的不同类型兼容。XNA支持3D的.X文件格式和.FBX文件格式。加载绘制这些模型文件在你的XNA工程中,允许你去绘制和操作详细和复杂的图片而无需担心的每一个具体顶点和纹理。相反,您就可以专注于移动,旋转,操纵模型和其他游戏相关的问题。
配置这个工程
在我们加载和绘制一个模型时,让我们来应用这本书2D章节中的一课:为这一项目,您从一些面向对象设计的工作开始。这本书的前半部分,你使用一个SpriteManager类,它处理所有精灵的绘制。你同样创建一个基类为所有的精灵,从该基类派生专门的子类。这个方法看上去很不错,所以,你在3D游戏采用同样的设计模式。
从头开始本章通过创建一个新的项目。命名你的游戏3DGame,如图10-1所示。
第一件事,你准备需要一个摄象机。你创建一个摄象机使用GameComponent在前面的章节中,你需要在这里在做一次。为了简单化这个事情,你可以复制Camera.cs文件从前面的章节中,粘贴到这个项目中;或者,如果你没有准备做这些,你可以下载这个资源代码复制Camera.cs文件到这个项目中,你需要改变命名空间从_3D_Madness到_3D_Game,虽然,为了可以使用Camera类在你的项目中。
另外,你可以创建一个新的GameComponent通过右击这个解决方案在你资源管理器,选择Add-New Item...,选择游戏组件模版从右边的表单中。命名这个文件Camera.cs然后点击Add,如图10-2所示:
你需要修改代码生成为你的GameComponent,这样它看上去象这样:
2 using System.Collections.Generic;
3 using System.Linq;
4 using Microsoft.Xna.Framework;
5 using Microsoft.Xna.Framework.Audio;
6 using Microsoft.Xna.Framework.Content;
7 using Microsoft.Xna.Framework.GamerServices;
8 using Microsoft.Xna.Framework.Graphics;
9 using Microsoft.Xna.Framework.Input;
10 using Microsoft.Xna.Framework.Media;
11 using Microsoft.Xna.Framework.Net;
12 using Microsoft.Xna.Framework.Storage;
13 namespace _3D_Game
14 {
15 public class Camera : Microsoft.Xna.Framework.GameComponent
16 {
17 //Camera matrices
18 public Matrix view { get; protected set; }
19 public Matrix projection { get; protected set; }
20 public Camera(Game game, Vector3 pos, Vector3 target, Vector3 up)
21 : base(game)
22 {
23 view = Matrix.CreateLookAt(pos, target, up);
24 projection = Matrix.CreatePerspectiveFieldOfView(
25 MathHelper.PiOver4,
26 (float)Game.Window.ClientBounds.Width /
27 (float)Game.Window.ClientBounds.Height,
28 1, 100);
29 }
30 public override void Initialize()
31 {
32 // TODO: Add your initialization code here
33 base.Initialize();
34 }
35 public override void Update(GameTime gameTime)
36 {
37 // TODO: Add your update code here
38 base.Update(gameTime);
39 }
40 }
41 }
你准备去绘制飞船在这个游戏中它在很远的距离。虽然,你不准备去绘制很多的物体。你需要了解性能问题当调用这方法时,但是影响性能的不是视阈的大小。性能问题出现在你在这个视阈内有大量的物体要绘制,PC要保持太多的对象被绘制。
最后,你需要修改你的Game1类去使用你的摄象机。添加一类-级别变量用下面自动执行属性到Game1类中:
为了在XNA绘制一个模型,你首先需要添加这个模型到这个项目中,用同样对待纹理,声音等的方法。内容管道将编译这个模型,将核实下它是否是一个.X-格式的模型。
很多模型有纹理文件关联它们。有时候这些文件作为模型的.X文件的参考。如果是这种情况,你就要保证你的纹理文件在指定的.X文件的目录中。
首先,创建一个子文件夹在Content文件夹下,在你的解决方案中右击这个Content文件夹,然后选择Add-NewFolder.命名这个文件夹为Models.
如果你没有准备好做这个,下载这章的资源代码。在3D\Game\Content\Models文件夹,找到一飞船的模型,它的名字是spaceship.x。这个模型实际上是微软DirectX SDK中的一个模型飞船。
添加飞船模型到这个项目通过右击你的Content\Models文件夹在资源管理器中,选择Add-Existing Item...浏览到并选择spaceship.x模型。
一旦这个模型在你的程序中,编译你的项目。如果成功编译,那么XNA识别了.X文件,并且能够编译它。换句话说,这个内容管道能通过一个错误说明它不能识别这个文件格式。
注意:如果你的模型要求额外的纹理文件(spaceship.x模型不需要),会得到一个编译错误如果这个纹理文件不在正确的位置。这可能似乎不是什么特别,但是它实际上很有帮助。在XNA游戏之前,我们开发者不得不计算处理模型和它的纹理,这些能在正确的位置都是通过大量乏味反复实验得出来的。内容管道明显的提高了3D的开发在这个方面。
绘制一个模型使用一个BasicModel类
通过前面章节的2D游戏的设计,想创建一个基类为所有的模型。添加一个新的类通过右键单击你的项目在资源管理器,选择Add-Class....命名这个类为BasicModels.cs,如图10-所示。
让我们充实这个BasicModel.class.首先,你需要添加这些命名空间:
2 using Microsoft.Xna.Framework.Graphics;
2 protected Matrix world = Matrix.Identity;
第二个变量是Matrix代表指定模型的世界(world)。这个矩阵代表模型哪里绘制,如何旋转它,缩放它,等等。如果你读了前面的章了,会觉得相当熟悉。
接下来,添加一个BasicModel类的结构。所有这些结构需要做的是接收一个Model类的参数,并且设置模型成员的值。
2 {
3 model = m;
4 }
2 {
3 }
所有这一切实际基于绘制出了这个模型。绘制模型是有点棘手的事情。为了明白模型是如何绘制的,它帮助去理解一个实际的模型是什么。正如前面所提到的,模型被创建在第三方模型软件程序中。一个模型在XNA中代表一个整体的场景从这些工具中的一个,导出应用于XNA中。
这些场景,.X文件导入其中,都可以包含一个以上的对象。这些对象,称为meshes,都被存放在模型中。这个Model类在XNA代表这些meshes作为ModelMesh对象,这个Model类包含一个ModelMesh对象表单在它的Meshes属性中。
一个mesh可以包含材质或者颜色,纹理等等,为绘制特定的mesh.一个mesh的各个部分不需要颜色或者纹理,你可以有多个材质在一个单一mesh上。为了保存这个数据,这个mesh由多个部分组成。ModelMesh类保存一个ModelMeshParts表单在一个称为MeshParts的属性。每一个MeshParts包含绘制MeshPart的材质,和一个Effect对象,它被用来绘制MeshPart。
默认的,Effect用在一个Model的MeshPart,是BasicEffect类型。与上一章的BasicEffect相似。BasicEffect来自于Effect,提供给你一个方法去绘制对象在3D中,不需要创建你自己的自定义HLSL效果文件。在这本书的后面你可以得到些自己的效果文件,但在此时,它太先进,所以我们现在先了解BasicEffects。
最后,每一个ModelMesh有一个转换,它将移动mesh去适应模型的内部位置。
看到这一切的作品,画一个汽车模型有一个方向盘。这整个场景(汽车和方向盘一起)通过一个模型来表示。这个模型可以有两个不同的ModelMesh对象在它的Meshes属性中(一个为汽车一个为方向盘)。每一个Meshes将有一个转变,它放置这个对象在一个正确的地方(汽车放置在同样的位置,例如原点,同时方向盘将有偏移,相应把它放在适当的位置)。另外的,每一个Meshes将有一个材质,它修改mesh的外表(例如,汽车可以用一个材质,使它出现光亮的红色,同时方向盘可以使用一个材质,使它出现阴暗的黑色),和一个Effect,它应该被用来绘制ModeMesh.
现在你明白模型由什么组成,让我们看一些典型的代码,它可以用来绘制一个模型:
2 model.CopyAbsoluteBoneTransformsTo(transforms);
3 foreach (ModelMesh mesh in model.Meshes)
4 {
5 foreach (BasicEffect be in mesh.Effects)
6 {
7 be.EnableDefaultLighting( );
8 be.Projection = camera.projection;
9 be.View = camera.view;
10 be.World = world * mesh.ParentBone.Transform;
11 }
12 mesh.Draw( );
13 }
其次,代码通过在这个方法中的ModelMesh循环,BasicEffect的每一个对象联合每一个ModelMesh,应用默认的光照和设置Projection,View和World属性为这个对象被绘制,为这个projection和view矩阵使用相机,为这个对象的World属性使用world矩阵。world矩阵通过这个变换被乘,确保ModelMesh在模型中被放置在适当的位置。
如果你糊涂了,不要担心;这是有点混乱,但是会变得很清晰。真的,你所需要记住的是,你循环通过一个模型的部分,为这些模型组成部分循环通过这个effects,并且设置属性去绘制正确的部分。
相机的view和projection矩阵在前面已经介绍过了,但是在次提醒你,它们基本上是矩阵变量,告诉XNA哪里放置摄象机,相机的viewing frustum。
这也许没有什么意义在这点上,但是没有关系。重要的是记住模型绘制的代码,它被用来设置你的BasicEffect的Projection,View,和World属性。一旦你做了这个,剩下的代码将为你处理模型的绘制。
稍后你会看到如何应用自定义effects到你的模型中,从现在起,注意每个模型在XNA中的默认使用BasicEffects。
继续添加下面两个方法到你的BasicModel类中:
2 {
3 Matrix[] transforms = new Matrix[model.Bones.Count];
4 model.CopyAbsoluteBoneTransformsTo(transforms);
5 foreach (ModelMesh mesh in model.Meshes)
6 {
7 foreach (BasicEffect be in mesh.Effects)
8 {
9 be.EnableDefaultLighting( );
10 be.Projection = camera.projection;
11 be.View = camera.view;
12 be.World = world * mesh.ParentBone.Transform;
13 }
14 mesh.Draw( );
15 }
16 }
17 public virtual Matrix GetWorld( )
18 {
19 return world;
20 }
添加一个模型管理器
再一次应用下这个游戏的2D部分的设计,想创建一个GameComponent,你使用它作为所有你的模型的管理器在这个游戏中。为了添加一个新的GameComponent,右击资源管理器中的解决方案,选择Add-New Item...,然后选择Game Component模版从右边的表单中,命名你的类为ModelManager.cs,如图10-4所示。
默认的,新的ModelManager类源于GameComponent.添加一个GameCompnent到组成部分表单中在你的游戏中,同时到GameComponent的Update方法和你的游戏的Update方法中(每一次你的游戏的Update方法被调用,你的GameComponent的Update方法也会被调用)。
为了连接你的ModelManager到游戏的循环,添加一个ModelManager类型的类别变量到你的Game1类中:
2 Components.Add(modelManager);
2 {
3 base.Draw(gameTime);
4 }
2 {
3 models.Add(new BasicModel(Game.Content.Load<Model>(@"models\spaceship")));
4 base.LoadContent( );
5 }
2 {
3 // Loop through all models and call Update
4 for (int i = 0; i < models.Count; ++i)
5 {
6 models[i].Update( );
7 }
8 base.Update(gameTime);
9 }
10 public override void Draw(GameTime gameTime)
11 {
12 // Loop through and draw each model
13 foreach (BasicModel bm in models)
14 {
15 bm.Draw(((Game1)Game).camera);
16 }
17 base.Draw(gameTime);
18 }
编译运行你的游戏,你应该看见飞船模型,如图10-5所示:
非常酷!注意这个你的游戏比起在前面章节里处理三角形和纹理要看上去好多了。3D模型使你很快的绘制非常细致和高度真实的对象,将是几乎不可能的,只通过使用primitives。
旋转你的模型
非常好!这个模型看起来不错,继续学习。是时候创建你的第一个BasicModel类的子类了,为了能够基本旋转和移动。右击资源管理器的项目,并且选择Add-Class....,输入SpinningEnemy.cs作为它的名字。
取代这代码生成在SpinningEnemy.cs类用下面的代码:
2 using System.Collections.Generic;
3 using System.Text;
4 using Microsoft.Xna.Framework;
5 using Microsoft.Xna.Framework.Graphics;
6 namespace _3D_Game
7 {
8 class SpinningEnemy: BasicModel
9 {
10 Matrix rotation = Matrix.Identity;
11 public SpinningEnemy(Model m): base(m)
12 {
13 }
14 public override void Update( )
15 {
16 rotation *= Matrix.CreateRotationY(MathHelper.Pi / 180);
17 }
18 public override Matrix GetWorld( )
19 {
20 return world * rotation;
21 }
22 }
23 }
24
现在,接下来怎么办?第一件事要注意,SpinningEnemy类来自于BasicModel.这个结构接收一个Model和简单传递模型到基类的结构中。
关键在于这个类的类级别变量rotation.这个变量允许你去旋转你的对象而不仅仅是绘制它站在那里,绘制它只是用BasicModel类。在Update方法中,你可以看见rotation变量被更新,每一祯以1度围绕Y-轴旋转(记住角度通过弧度来表示在XNA中π is180º, so π/180 equals 1 degree)。
最后,GetWorld方法被调用在基类的Draw方法中,去摆放适当的对象在world中,结合来自基类的world变量,和来自这个类的rotation变量,获得这个对象很好的自旋的效果。
这一切就是这么回事。当你有一个实心的设计,程序变的这么简单!现在所有你需要做的是修改ModelManager到演示你的新类,而不是BasicModel类。在ModelManager类的LoadContent方法中,修改这行,创建一个BasicModel,改变它去
使用SpinningEnemy,正如这里所示:
编译运行你的游戏,现在你可以看见,模型围绕Y-轴自旋,如果10-6所示:
涡!看起来真不错!你可以很容易得到这个类来自于BasicModel类,通过添加代码到Update和GetWorld重写方法,你可以使你的模型飞船随意的移动和旋转。
您可能还注意到,只用了BasicEffect应用在您的代码中,你会给这个飞船一些象样的光照效果,使它看起来很漂亮。在以后的章节中,你会学习到更多关于应用自定义效果使用HLSL,你也会知道如何使你的模型通过应用自定义效果后变的非常棒!
源代码:http://shiba.hpe.cn/jiaoyanzu/WULI/soft/xna.aspx?classId=4
(完)