【题外话】
最近要做一个3D动画演示的程序,由于比较熟悉C#语言,再加上XNA对模型的支持比较好,故选择了XNA平台。不过从网上找到很多XNA的入门文章,发现大都需要一些3D基础,而我之前并没有接触过游戏以及3D相关的开发,所以我来从另一个角度整理下入门XNA。本文尽量少涉及3D及数学方面的知识,因为同类文章介绍的挺多的。
【系列索引】
【文章索引】
虽然微软在推出了XNA 4.0之后就再也没有升级过XNA的版本,但XNA还是在.NET平台上比较方便的3D框架。由于我使用的是VS2013,而XNA 4.0的安装程序只认VS2010,所以需要安装一个使用VS2010 Shell的程序(比如我使用的是SQL Server 2012 Management Studio,当然也可以安装VS2010 Express)才能通过XNA 4.0的安装。安装完后会自动在VS2010下添加相关扩展模板,但不会在更高版本添加,可以参考 http://ryan-lange.com/xna-game-studio-4-0-visual-studio-2012/ 将扩展模板添加到VS2012或2013中(VS2013需要将其中的版本号改为12.0)。
XNA 4.0相对之前的3.1做了很多修改,不仅代码上进行了很多调整,默认创建的项目也与之前的不同。创建XNA 4.0的Windows Game项目后,默认会创建两个项目,分别是WindowsGame以及WindowsGameContent,前者存放程序的逻辑代码,而后者则存放程序所需要的资源(模型、纹理等等),与其他项目不同的是,XNA项目增加了一个Content Reference内容引用,可以将逻辑与资源拆分成不同的项目,即由逻辑代码的项目引用资源项目,当然也可以合并在一起。
在创建的WindowsGame项目中,与其他Windows程序一样都包含一个Program.cs,除此之外同时还有一个Game.cs。需要说明的是,与平常开发Windows应用以基于事件的方式不同,开发XNA(以及其他游戏框架)的应用是以基于轮询的方式。在Game.cs文件中,除了构造方法外还会生成以下几个方法,其执行顺序和微软给出的说明如下:
- protected override void Initialize():需要包含加载任何非图形的资源。
- protected override void LoadContent():在Initialize之后或者在任何需要重新加载资源的时候(比如在DeviceReset事件发生时)执行,需要包含加载游戏需要的图形资源等。
- protected override void Update(GameTime gameTime):在程序逻辑需要处理时执行,需要包含程序状态的管理、用户输入的处理以及数据的更新。
- protected override void Draw(GameTime gameTime):在需要绘制一帧画面的时候执行,需要包含绘制图形的代码。
- protected override void UnloadContent():在程序需要释放资源时执行,需要包含释放资源的代码。
其中程序的主要部分就是Update()和Draw()两个方法,整个程序在运行时,几乎就是这两个方法在不断重复执行。需要说明的是,对于XNA,在默认情况下,执行一次Update()和Draw()是要控制在一定时间的(默认为1/60s,即60FPS),如果执行一次Update()和Draw()的时间小于这个时间将会进行等待,如果超过这个时间则会跳帧(不执行Draw()),当然也可以修改Game类中的TargetElapsedTime来改变这个时间,或者修改IsFixedTimeStep=false使得程序帧数能多大就多大。
对于二维的画面,我们可以直接使用屏幕的坐标系;而对于三维的画面,我们还需要将三维世界投影到二维的屏幕上。那么,我们就需要一个计算如何将三维世界投影到二维屏幕的工具,那么摄像机就是实现这个功能的。实际上,这里的摄像机与我们平时拿摄像机录像是一样的,在屏幕上显示的内容就是摄像机录下的内容。
首先需要说明的是,在XNA中使用的右手坐标系(与Direct3D中使用的左手坐标系Z轴相反),也就是说按正常方向去看的话,向右是X轴正方向,向上是Y轴正方向,而指向自己的(向外)是Z轴正方向,如下图。
在三维框架中,很多信息的存储和表示都是用四维矩阵(Matrix类)来的。所以要表示一个摄像机,通常由两个矩阵组成,分别是 视图矩阵(View Matrix) 和 投影矩阵(Projection Matrix),其中视图矩阵表示了摄像机的位置、摄像机的朝向以及摄像机的上方向;而投影矩阵则表示了摄像机的视角以及视觉范围。虽然听上去很复杂,但是XNA提供了直接由具体的参数创建矩阵的方法。
对于视图矩阵的创建,可以使用如下的方法:
Matrix.CreateLookAt(Vector3 cameraPosition, Vector3 cameraTarget, Vector3 cameraUpVector);
其中cameraPosition为摄像机在空间内的三维坐标;cameraTarget为摄像机所指向目标的三维坐标;cameraUpVector则表明了哪个方向是摄像机的向上方向(如果摄像机正着放的话,那么Y轴正方向为摄像机的向上方向)。
而对于投影矩阵的创建,则可以使用如下的方法:
Matrix.CreatePerspectiveFieldOfView(float fieldOfView, float aspectRatio, float nearPlaneDistance, float farPlaneDistance);
其中fieldOfView表示的是摄像机的视角弧度,范围为(0, Pi),通常为Pi/4(45°);aspectRatio为摄像机的长宽比,通常为屏幕的长宽比;nearPlaneDistance与farPlaneDistance则为当摄像机多近(远)时无法拍摄到物体。在下图中表示了fieldOfView与nearPlaneDistance和farPlaneDistance的关系,可以看到摄像机通过视角角度与距离可以产生一个近剪裁面和远剪裁面,而最终能投影的部分就是处在这两个平面之间的物体。
对于三维模型,XNA平台支持两种格式,分别是.x与.fbx文件,其中后者在很多软件中都支持,比如Maya、MotionBuilder等等。对于模型的导入,只需要将模型文件拖到Content项目中即可。不过需要说明的是,为了保证效率等,XNA程序在运行时并不是调用的fbx等模型文件,而是通过Content Pipeline内容管道进行处理,编译成扩展名为.xnb的一种中间格式,在程序运行时程序实际调用的为这些中间格式,如下图。
Content Pipeline中主要有两个重要的部分,分别是Importer以及Content Processor。其中Importer负责将导入的资源文件解析为XNA可以识别的XNA Game Studio Content Document Object Model (DOM)。系统已经支持了很多的文件格式,比如三维模型支持.fbx和.x,纹理支持.bmp、.dds、.dib、.hdr、.jpg、.pfm、.png、.ppm、.tga等文件等,详情可以参考这里。如果需要的文件格式在XNA框架中不支持,可以自己写新的Importer来支持更多的格式。
而Processor则根据前者解析后的内容存储为Output Type,之后再编译成.xnb文件,在默认情况下使用系统自带的Processor已经足够了,不过当想存储XNA默认没有存储的内容时则需要自己扩展Processor。
虽然上述说了这么多,但加载资源则只需要一行代码即可解决。在上述的Game类中提供了一个ContentManager的实例,名为Content,我们可以使用其来加载我们的模型。ContentManager提供了一个名为Load的泛型方法,将资源类型以及资源的相对路径传入即可读取。比如我们将名为dude.fbx的文件拖到Content项目中,然后只需在上述提到的LoadContent方法中添加如下的一行代码(需要在Game类中定义一个名为model的Model类型):
protected override void LoadContent() { // TODO: use this.Content to load your game content here this.model = this.Content.Load<Model>("dude"); }
而如果要将模型绘制到屏幕上,只要调用Model对象的Draw方法即可。不过Draw方法需要提供 World世界矩阵 以及 View视图矩阵 和 Projection投影矩阵,对于后两个矩阵我们上文已经说明,而世界矩阵与视图矩阵类似,可以使用如下的方法创建:
Matrix.CreateWorld(Vector3 position, Vector3 forward, Vector3 up);
其中position与up均与之前的CreateLookAt类似,为模型在世界中所处的三维坐标和哪个方向是模型的向上方向;而forward则不同,为模型的朝向向量,其仅仅代表方向。当然我们也可以通过创建不同的平移、旋转等矩阵,然后相乘得到世界矩阵。
接下来我们可以在上述提到的Draw方法中添加如下的代码:
protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); // TODO: Add your drawing code here Matrix world = Matrix.CreateWorld(Vector3.Zero, Vector3.Forward, Vector3.Up); Matrix cameraView = Matrix.CreateLookAt(new Vector3(120, 120, 120), Vector3.Zero, Vector3.Up); Matrix cameraProjection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, this.GraphicsDevice.Viewport.AspectRatio, 10.0F, 10000.0F); this.model.Draw(world, cameraView, cameraProjection); base.Draw(gameTime); }
其中Vector3.Zero、Forward、Up为系统预设的几个常量,分别为(0, 0, 0)、(0, 0, -1)以及(0, 1, 0);而常用弧度值在MathHelper中也可以找到,比如Pi、PiOver2(90度弧度)、PiOver4(45度弧度)等等;GraphicsDevice.Viewport.AspectRatio为显示区域的宽高比。
这样我们就可以创建一个在(120, 120, 120)坐标上,朝向坐标原点的摄像机,并在坐标原点创建一个模型(由于dude模型的正面是朝Z轴负方向的,所以这里我们选用的Z轴负方向为模型的朝向)。
文中提到的dude.fbx从微软提供的sample中获得:http://xbox.create.msdn.com/en-US/education/catalog/sample/skinned_model
本文所有代码可以从如下地址下载:https://files.cnblogs.com/mayswind/XNA_Sample_1.zip
虽然现在使用XNA的人越来越少了,但是这个有点类似个人学习笔记的文章还是要正式开坑了。
【相关链接】
- Game Class:http://msdn.microsoft.com/en-us/library/Microsoft.Xna.Framework.Game.aspx
- XBOX LIVE Indie Games:http://xbox.create.msdn.com/en-US/education/catalog/?contenttype=4
- What is the Content Pipeline?:http://msdn.microsoft.com/en-us/library/bb447745.aspx
- Standard Importers and Processors:http://msdn.microsoft.com/zh-cn/library/bb447762
- 3D 游戏控制:http://msdn.microsoft.com/zh-cn/windowsphone/gg620050.aspx