[翻译]Oreilly.Learning.XNA.3.0之one


PS:自己翻译的,转载请著明出处

                                                                   第九章   3D游戏的开发
                       正如你所看的整个这本书,这有很多很酷的东西,你可以在3D图形中开发在XNA中。然而,由于图形卡的实力和处理器,最*在3D图形领域有巨大的提升。加载任何的最新的第一人称射击游戏,你要你的硬件能支持它,你会非常惊奇细节的程度,在这个游戏中和大量对象在任何特定的时间内围绕这场景飞行。真实的3D图象将是未来的发展方向,如果你想认真的搞游戏开发,在这里您可以想集中利用您的时间和精力。
                       因此,让我们开始吧。由于2D和3D图形在XNA中被不同的处理,接下来创建一个新的工程从本节开始。打开Visual Studio,选择文件->新建->工程。当新工程窗体出现时,选择VisualC#->XNA Game Studio3.0在窗口的左边的菜单树中。命名你的3D工程为Madness,选择工程存放的位置,单击OK。
                       现在你已经创建你的工程,你已经准备好了,让我们看一些在XNA的2D和3D游戏编程的关键的区别。

坐标系
                       第一个区别你会注意,它处理3D图形胜于2D,而不是增加一个*面。恩。。这是对的。谁会想到呢?我知道我可能没有用这个改变你的想法,但是从2D转换到3D需要时间去习惯一下,否则往往是彻头彻尾的迷惑。
                       2D游戏编程在XNA中非常的类似于在一副油画上绘图:你绘制在二维空间*面;坐标(0,0)坐落在屏幕的左上角;X肯定向右移动,同时Y向下移动。
   
                    如果在2D上绘图就象在一副图上绘画,那么在3D上绘图就象录制一个视频用手持的摄象机。与3D相配的是基于三个坐标系。这个坐标系,有时候被称为世界空间,中心的初始位置在坐标(0,0,0).然而,不象在2D中绘图,当在3D中绘制一些东西时,你不能保证这个对象可以在场景中的中心被看见,左上角,或者任何地方。这是为什么?因为在3D中,有两个基本的组成部分去绘制一个场景:在世界中布置对象,布置相机在这个世界中,指定一个方向,它是摄象机面对的方向。在这个场景中只有这个方向上的物体能被摄象机看见。
                       依靠摄象机的位置和它所面对的方向,一个对象你所绘制在3D游戏的初始地方可能是屏幕的中心位置,或在场景的下面,或屏幕的其他位置,甚至不在整个场景中。
                       在我们深入了解这个之前,让我们谈一些关于3D坐标系。如果你熟悉3D坐标系,你会认识到,X轴向右边移动,Y轴向上移动。虽然,Z轴并没有明确的定义。3D坐标系有两种不同的类型,两个中每一个Z-轴移动是一个相反的方向。Z轴的方向移动靠右手坐标系定则来检测。两种可能方向分别是左手和右手坐标系。
                       方法之一区别左手坐标系和右手坐标系是*放你的手,手掌向上,同时手指卷起指向上方就是Y轴。你的大拇指的所指的方向表示它是Z轴的方向。

                        XNA使用一个右手坐标系,意思当你正在寻找原始位置从一个传统的角度,X向右移动,Y向上方移动,Z朝着你移动。

摄象机
                       现在,你理解了坐标系了,让我们讨论下摄象机。正如前面所提到的,绘制一个场景在3D就好象记录一个电影用手持摄象机。你不得不定义摄象机的位置,它的朝向,和其它各种属性。
                       这些属性被保存在一个矩阵对象中。矩阵是一个相当复杂的数学实体,它已经超出了本章和这本书的范围。我只想说矩阵在3D图形里它几乎是所有事情的中心。幸运的是,XNA处理所有的矩阵详细细节在游戏的后台,也就是说,你已经不需要了解它是如何实现的。现在,只需要理解一个或者两个矩阵可以代表一个摄象机。
                       两个矩阵对象组成一个摄象机在XNA中:view和projection矩阵。这view矩阵存有的信息是摄象机坐落在什么地方,它的方向朝向哪里,它的上部方向是哪里。projection矩阵保持有的信息是确定摄象机的属性基于view的角度,摄象机可以看多远,等等。这个矩阵代表从3D世界到场景的2D*面转换。
                       创建一个view矩阵,你使用一个静态方法从Matrix类中称为CreateLookAt的方法.这个方法返回一个矩阵对象和接收参数如图。
                       首先,注意这些参数的每一个数据类型。什么是Vector3?正如你使用一个Vector2在2D游戏开发去代表一个(X,Y)坐标,一个Vector3代表一个3D坐标(X,Y,Z).
                       接下来,注意CreateLookAt方法的第三个参数。同时你可以指定摄象机的一个位置和一个方向,还没有决定一个对象如何绘制在场景中。通过使用手持摄象机,可以使相机转向一侧或者上下颠倒,它面对的方向将影响事情被记录的方式。在XNA中使用一个摄象机也是一样的。指定相机的一个up向量,这样XNA不仅知道如何放置相机在这个世界里,而且也知道摄象机的位置。
                       要创建一个projection矩阵,使用另外的静态方法从这Matrix类的一个称为Matrix.CreatePerspectiveFieldOfView的方法.这方法同样返回一个Matrix对象和接收参数如表

                       projection矩阵建造所谓的view frustum或者field of view为你的摄象机。从本质上讲,它定义一个3D空间区域,它能被摄象机看见,同时被绘制在场景中。物体存在这个区域内的被绘制到屏幕上,除非他们在它们和摄象机之间被其他物体掩盖。在这个"可视*截头"之外的物体不被能绘制到屏幕中。
                       在图9-3中,你可以看见一个绘制的假设的"可视*截头"或者视觉区域。这三个坐标盒子代表的区域,它被绘制在屏幕中。任何在这个盒子外面的对象都不会被绘制,不管它是否在前面被绘制,或者后面,或者在盒子的一边。nearPlaneDistance和farPlaneDistance参数定义盒子的前面和后面,被称为near clipping plane和far clipping plane.在near clipping plane前面不会被绘制,在far clipping plane后面的不会被绘制。


创建一个3D摄象机

                       好,我们来编写一些代码。当处理相机时,它经常使屏幕为你的摄象机创建游戏的一部分。这使它真正容易添加摄象机到新的游戏中(只添加组件到工程中,然后添加它在游戏代码中),更新摄象机并移动它。创建一个新的游戏组件在你的3D工程中通过右击这工程命名和选择Add->New Item..在模板列表中在Add New Item屏幕的右边,选择游戏组件。命名这个组件为Camera.cs,然后点击添加它。

定义视觉区域
                       你需要小心当你选择哪个值,使用它表现你的*或者远*面。大部分XNA游戏开发中将选择一个非常小的数字(如1)设为**面,和一个大的数字(象10000)设为远*面。请记住这个,意思是每个对象在这些*面中(在另外视觉区域中的)将会被绘制在这个场景中。如果有很多对象在你的游戏中,你可能会遇到性能的问题,如果你*和远*面相距甚远的话。
                       注意以前的基本游戏,你正走在旷野中,突然所有的稍有距离的大山正好急急走进视景中?这是因为大山在你的前景*面的背后,当你朝着它移动时,它进入了视觉区域。如果是这样,游戏没有了功能性,反而总是绘制任何的东西在这个世界中,游戏性能可能非常慢,游戏可能不能玩。确保你显示出足够的世界,使游戏可玩,还要考虑去避免性能问题。
                       在我的经验中,在教学生如何在XNA中开发游戏,当一个开发者希望看见一些绘制在场景中的3D对象,却没有出现,这个问题可能与摄象机的视景区域有关或者摄象机的位置和方向有关系。
                       大多数错误在这个区域的是一个对象不能被绘制,因为这个摄象机指向错误的方向,对象不能被看见在这个场景中,即使它们既不在*裁减*面的前面也不在远裁减*面的后面。
                       在Camera.cs类里,添加两个类级别的变量(适当的autoimplemented属性)去代表你的相机视阈和projection矩阵:
1 public Matrix view{get;protected set;}
2 public Matrix projection{get;protected set;}
                       然后。改变构造去接收三个Vector3变量代表相机的最初的位置,目标,up向量。同样,在你的结构中,初始化视阈通过调用Matrxi.CreateLookAt和投影通过调用Matrix.CreatePerspectiveFeldOfView.这里完整的代码为Camera类的结构:
1 public Camera(Game game,Vector3 pos,Vector3 target,Vector3 up):base(game)
2 {
3     view=Matrix.CreateLookAt(pos,target,up);
4     projection=Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4,(float)Game.Window.ClientBounds.Width/(float)Game.Window.ClientBounds.Height,1,100);   
5 }

注意:确保你转化屏幕的宽和高是浮点型的值,当计算屏幕高宽比率时,正如这里所显示的。如果不这样的话,将是整型除以整型,这将造成信息丢失,也可能使你的图形看起来被压扁了。
                       这里唯一棘手的是MathHelper.PiOver4值的使用表示180度被分成4份。一会我们会更多的关于它的讨论在这章里,但是现在,你只需要知道这个角度在XNA中是衡量弧度的。在弧度中,45度这是一个标准的视场角。MathHelper.PiOver4值是一个常量,它很容易的被访问每一次你想使用这个值。
                        下一步,你需要添加你的摄象机组成部分到组成列表中在你的Game1类中。创建一个类级别的Camera变量在你的Game1类中:
1 Camera camera;
                        然后,在Game1类的Initialize方法中,添加下面的代码到示例中,相机和添加它到Game1的组成部分的列表中:
1 //Initialize camera
2 camera=new Camera(this,new Vector3(0,0,5),Vector3.Zero,Vector3.Up);
3 Components.Add(camera);
                        因此,这是怎么回事?你创建你的相机在位置(0,0,5),告诉它去观看原点(Vector3.Zero转换一个空的Vector3代表点(0,0,0)或者原点),指定这(0,1,0)是up(Vector3.Up转换一个Vector3的(0,1,0),它默认代表向上)。
                        编译运行该游戏现在,你会看见一些令人惊奇的相机工作状态。好...也许不是。你也许会想,"恩,这一定不完全"。而在某种意义上来说,你也许是对的。但是在另一方面,这里没有什么东西显示在屏幕上,你现在实际上在3D上"拍摄"。你有了一个相机并使它准备好了;刚才没有任何东西在"拍摄"里。


简单的绘制
                        现在你有一个摄象机并且准备好了,你需要绘制一些东西在屏幕上。所有3D绘图的根源其实是三角形。如果你绘制足够多的三角形,你可以呈现几乎所有可能的图形。放入足够的互相靠*的三角形,使它们足够小,你可以得到一个光滑的圆形的表面。
注意:这实际上不同于我们生活的世界。如果你获得一个完美光滑的表面,仔细看它在一个显微镜的级别,这个表面可能根本就不光滑:它可能满是山脊和山谷。我们的计算机不是足够强大,去模仿这精细的级别,但是它们是肯定会改善。
      事实上,我们就这个问题,很多3D图形是建立在模仿真实的生活上的。照明效果的目的是尽可能模仿真实世界的光(或者至少,真实世界的光是我们现在能理解的),等等。在大多数情况下,游戏一般是更让人愉快的,他们的行为和感觉很象现实的生活。例如,如果半条命的游戏极大地受益于拥有先进物理引擎,让玩家互动的方式, 尽可能的模拟真实世界的物理定律。
                        为了绘制你的第一个三角形,你需要定义一些点,或者顶点,去代表三角形的每一个角。在你的Game1类中,创建一个VertexPositionColor的数组对象在类的级别:
1 VertexPositionColor[] verts;
                        你可以使用这些对象去创建这些点,一个VertexPositionColor对象代表一个顶点,它有一个位置和一个颜色。你指定这些值在这个对象的结构中当你创建它们。在你的Game1类的LoadContent方法中,初始化这些点用这个代码:
1 //Initialize vertices
2 verts=new VertexPositionColor[3];
3 vert[0]=new VertexPositionColor(new Vector3(0,1,0),Color.Blue);
4 vert[1]=new VertexPositionColor(new Vector3(1,-1,0),Color.Red);
5 vert[2]=new VertexPositionColor(new Vector3(-1,-1,0),Color.Green);
                        接下来,在你的Game1类的Draw方法中,你需要指定一些东西称为一个VertexDeclaration.从本质上讲,它告诉在PC上的图形设备,你发送给它一些信息,指定信息的类型,将会发送它。在这种情况下,你准备告诉图形设备,你准备传送它一些VertexPositionColor数据。添加这行只在GraphicsDevice.Clear调用在Draw方法之后:
1 GraphicsDevice.VertexDeclaration=new VertexDeclaration(GraphicsDevice,VertexPositionColor.VertexElements);
                        下一步,在VerteDeclaration之后,添加下面的代码去绘制你的三角形:
 1 //Create effect and set properties
 2 BasicEffect effect=new BasicEffect(GraphicsDevice,null);
 3 effect.World=Matrix.Identity;
 4 effect.View=camera.view;
 5 effect.Projection=camera.projection;
 6 effect.VertexColorEnable=true;
 7 //Begin effect and draw for each pass
 8 effect.Begin();
 9 foreach(EffectPass pass in effect.CurrentTechnique.Passes)
10 {
11      pass.Begin();
12      GraphicsDevice.DrawUserPrimitives<VertexPositionColor>(PrimitiveType.TriangleStrip,verts,0,1);
13      pass.End();
14 }
15 effect.End();
                        运行游戏在这个点上应该显示一个非常好看的三角形。注意三角形的每个顶点上的颜色如何混合别的三角形的中心。这是因为在后台,GPU测量每个象素的距离,从每个顶点构造,颜色为特殊的象素基于在一个计算,包括从每一个顶点的距离和这些顶点的颜色。很酷,啊?
                        如何结束一个象那样的三角形的?你使用的坐标为这三个顶点的,(0,1,0),(1,-1,0),(-1,-1,0).你分别为这些顶点指定颜色是蓝色,红色,和绿色。XNA接收这些坐标,并创建一个三角形。仔细看下图它看起来怎么样,如果你用坐标系覆盖这三角形的图片。
                        正如你所看到的这些数字,三角形的顶点相应的点,你创建在你的顶点数组中。XNA使颜色着色器自动工作通过混合在顶点-最*一个象素到一个顶点顶点之间的颜色,大量的顶点颜色包含在指定的象素的颜色中。
                        让我们回顾一下之前你添加的代码。一个小的三角形的大量代码-想象一下多少代码它可能创建一个Quake3用这种方法!
                        不要让这个阻碍你。让我来解释这里怎么了。首先,你需要明白3D图形如何被绘制在XNA上的。在XNA3D中的任何东西被绘制使用一些称为高级别阴影语言(HLSL).HLSL是基于PC开发语言,允许开发者很容易创建不可思议的水波纹效果象,反射表面,和其他眼状物的糖果。你不需要担心它,在后面的章节我们会详细的解释HLSL。现在所有你需要知道是在XNA3D中任何东西被绘制使用的是HLSL.通过所谓的效果.
                         这个效果类允许你去接触HLSL代码,传递数据到HLSL中,等等。XNA小组是一种足够包含一个类,它来自于Effect,称为BasicEffect.使用这个BasicEffect,你可以有效的,使用默认的HLSL代码不需要预先知道任何关于HLSL的信息。也就是说你可以集中精力在得到你的三角形去显示,不用处理令人头痛的编译的HLSL代码。感谢,XNA小组!
                         现在,回到代码中。首先,你创建一个BasicEffect类型的对象。然后你需要设置world属性。这个属性从本质上代表位置,对象将被绘制的位置。绘制在初始的位置,所以现在,你使用Matrix.Identity.
注意:不要认为Matrix.Identity很反常。我们适用更多矩阵乘积的基础在后面的章节中。现在,想一下Matrix.Identity作为一个为一个对象被绘制默认的位置;这个默认的位置是在原始位置的周围。
                          接下来,设置效果的view和projection,使用这个信息从你的摄象机组成部分里。然后,你设置效果的VertexColorEnabled属性为true.没有这一点,颜色不会被绘制,你的三角形将在屏幕上显示为白色。最后,你开始你的效果调用BasicEffect.Begin.
                          每一个效果有一个或多个technique。每一种技巧有一个或多个passes.在前面的代码开始这个效果,然后开始相互传递在当前的technique。这里都将做出更多的解释,当我们了解HLSL更深,但是现在,考虑Begin和End调用类似的到调用SpriteBatch.Begin,就象你前面章节的那样的。关于SpriteBatch,所有的sprites不得不绘制在Begin和End调用中。同样应用在下面的代码:所有的passes(译者:通道)必须执行通过它们自己的Begin和End调用,它们必须在Effect.Begin和End之间,这个对象的所有的绘制代码必须在EffectPass.Begin和End之间被调用。
                          这个代码,它实际上绘制三角形在这个屏幕上,它在调用EffectPass.Begin和End之间,是一个调用GraphicsDevice.DrawPrimitives.让我们仔细看这个方法:
1 GraphicsDevice.DrawUserPrimitives<VertexPositionColor>(PrimitiveType.TriangleStrip,verts,0,1); 
                          DrawUserPrimitives是一个普通方法,因此,你必须指定顶点的类型,你准备绘制它(这种情况下,VertexPositionColor).第一个参数是绘制三角形的一个方法,一会我们来了解它。现在,只需要了解当给定的三个顶点,传递PrimitiveType.TriangleStrip到这个参数将导致XNA去创建一个三角形来自于这三个顶点。下一个参数是顶点数组,你保存你的顶点信息在它里面。第三个参数是到这个数组的偏移量,或者这个index在开始绘制顶点在屏幕上(译者:数组中从哪个顶点开始绘制)。你想从你的顶点数组开始,这样你指定0为这个参数。最后的参数是原始(译者:三角形)的数量,或者多少个primitives你希望去绘制在这个方法调用。一个三角形就是一个primitive,并且你希望去绘制一个三角形,这样传递这个值1为这个参数。

矩阵乘法
                         好吧,自己喜欢,shmancy三角形。你可能想要使它做些别的事情,对吗?那么,我们来讨论下旋转和转换。正如前面所提,矩阵在3D图形的你所有做的事情都在后台。特别是当你准备移动,旋转,或者缩放一个对象。在以前的代码中,你不得不设置BasicEffect的world属性到Matrix.Identity.让我们看看如果我们能获得更多的意义。
                         你可以考虑下BasicEffect的world属性作为一个矩阵,它告诉XNA哪里去绘制,去绘制什么,怎么样去放置它在世界中用一个适当的旋转和缩放。这是类似于一个坐标,在它上面绘制一个项目(事实上,一个3D坐标被包含在矩阵内),保持所有的信息为这个旋转以及缩放。这里不详细介绍了,但是如果您有兴趣进一步调查, 有很多资源数学课本和互联网上会教你所有你想了解矩阵乘法。
                         这本书的意图,作为XNA的介绍,你只需要知道矩阵乘积是在所有旋转,缩放,转换等等之后。在3D图形里。这矩阵通过Matrix.Identity代表称为单位矩阵。这单位矩阵是一个特别矩阵,when it comes to multiplication,behaves just like the number one-(译者:句子很怪异)这是,任何矩阵乘积通过单位矩阵产生它自己作为这个乘法的乘积。
                         这是什么意思,单位矩阵代表一个新的起点。如果你想绘制一个对象在原点,你最好设置它的World属性到Matrix.Identity.在第一个三角形例子里做这个,这个三角形在原点周围绘制。
                         所以,为什么你还要在乎这个呢?好了,在某中程度上这是很好理解的,在屏幕的后台发生了什么。对我们来说幸运的是,尽管,XNA处理很多的矩阵计算是通过它自己的Matrix类。

移动和旋转
                         在这节,我们将了解如何移动和旋转一个对象在3D世界里。在你的工程中,你有一个很酷的三角形。如果你想移动这个对象到左边一点,你可以通过Matrix.CreateTranslation的方法这样做。
                         一个*移移动一个对象或者一个点在一个指定的方向通过一个顶点。因此,CreateTranslation方法带来一个Vector3作为一个参数。它同样有一个重载,它接收浮点XYZ的值。
                         现在,如果你想真实的不断的移动你的对象或者旋转它,而不是仅仅改变它的位置,需要一个变量去表现你的对象的世界。创建一个Matrix变量在这个类的水*在你的Game1类,初始化它到Matrix.Identity:
1 Matrix world=Matrix.Identity;
                         然后,修改这行代码在你的Draw方法中,这里你设置BasicEffect.World属性去使用这个新变量:
1 effect.World=world;
                         接下来,添加下面的代码到你的Update方法中:
1 //Translation
2 KeyboardState keyboardState=Keyboard.GetState();
3 if(KeyboardState.IsKeyDown(Keys.Left))
4       world*=Matrix.CreateTranslation(-.01,0,0);
5 if(KeyboardState.IsKeyDown(Keys.Right))
6       world*=Matrix.CreateTranslation(.01,0,0);
                         现在,编译运行这个游戏,你会注意到当你压下左和右方向键时,对象相应的移动了。

发生了什么?
                         这是怎么会事?我正在移动一个对象还是移动了整个世界?不要让变量名称叫"world"迷惑了你。
                         这世界矩阵不能代表整个世界,虽然。考虑到这变量作为一个矩阵,表示这个世界看起来怎么样对于一个给定你绘制的对象。为什么它被称为一个world matrix?可能是因为世界矩阵获得了一个对象并把它熔入到了世界中。
                         在这种情况下,世界矩阵把你的三角形融入到了世界中。在世界矩阵中变量是一个坐标,它告诉XNA开始绘制对象,支持它可以旋转,缩放。总之,它描述一个特定的对象如何被绘制。
                         如果你绘制一个第二个三角形,你最好创建一个不同的世界矩阵为这个三角形,应用不同的设置(可以稍微旋转一下,缩放它,移动它)。当你绘制第二个三角形,它可能绘制相应世界矩阵;第一个三角形的世界矩阵与第二个三角形毫不相干,反之亦然。
                         如何旋转一个对象?这里有几种方法,它一般用来旋转对象,你想要围绕哪个轴旋转对象就用哪一个方法。类似于地球围着它自己的轴旋转,在XNA中每个旋转都围绕一个轴。因此,当执行一个旋转时,你必须指定一个轴来旋转。
                         添加下面的代码,为这个转换:
1 //Rotation
2 world*=Matrix.CreateRotationY(MathHelper.PiOver4/60);
                         这些代码使用其中之一的基本的旋转方法通过Matrix类提供:Matrix.CreateRotationY.这个方法旋转一个对象围绕Y-轴通过这个参数里的指定的度数。这些度数是弧度,不是角度。在弧度里,这个参数MathHelper.PiOver4/60将旋转对象.75度。这里没有什么科学意思为什么我会挑选这个数字;它看起来象是在*滑中的应用。随意改变的转速以获得不同的结果。
注意:使用*=而不是=,当设置世界变量时。不仅因为它会逐步添加值到矩阵和*移在每一祯中,它会造成对象不段的移动或旋转,而且还因为Matrix变量保持多个方面相关与一个对象如何安放在世界中(位置,旋转,缩放等).
    你通过Matrix.CreateTranslation设置位置,如果你使用=而不是*=,当你通过Matrix.CreateRotationY来设置旋转时,你可能失去了之间做的*移。
                        编译运行这个游戏在这点上,你可以看见三角形不停的旋转围绕着Y-轴。同样可以控制水*移动通过左和右的方向键。
                        你可以注意到,当你移动三角形,这不仅仅是你移动了它,而且你改变了旋转的中心。事实上,技术上是不会改变旋转的中心(当你旋转使用Matrix.CreateRotationY你正旋转个对象围绕原始点);你准备做的是修改三角形的旋转中心。当游戏开始,三角形绘制围绕原始点的中心,意思是当Matrix.CreateRotationY被调用,三角形看起来在这个地方旋转(围绕原始点,它是当前的围绕的中心)。当你移动对象到右边,这三角形现在看起来围绕原始点的轨道旋转。但是,实际上发生的是三角形移动到右边,然后你旋转它围绕原始点,通过调用Matrix.CreateRotationY.
                        但是等一下!当三角形旋转180度,它会消失,之后,再次出现了一个完整的旋转。怎么办?

Backface Culling
                        当三角形消失了你看见了什么,是一个称为backface culling过程。Culling是一个过程在3D图形中,它限制大量东西绘制在屏幕上,去提高性能。从本质上讲,backface culling的目标是绘制只有原始面对摄象机的一面。在这个程序中,你三角形只有一面被绘制。
                        默认情况下,XNA将会精选初始的对象被逆时针方式绘制。回忆下什么时候为这个三角形创建顶点?用顺时针方式绘制你的顶点。
                        如果你绘制这些顶点用逆时针顺序,并运行这个游戏,你希望看见的三角形实际被culled(译:裁剪),因此不能看见。在这种情况下,只有当它被旋转了180度你才会看见它。
                        所以,culling都为你做了些什么?它提高了你的游戏的性能。想象下足球。足球的内部会是什么样子?你留心过吗?明显是没有。所以,为什么浪费宝贵的处理器时间去绘制足球的内部结构呢?

                        正如一个真实生活,足球是个空心的,所有的对象在3D图形中都是空心的。它们都是由"skin"组成,围绕一些空的空间绘制,使用一系列的三角形。没有人在乎这些skins内部看上去象什么,绘制它们没有意义。
                        要感谢culling步骤,你的游戏将自动检测哪些应该被绘制,基于顶点的方向,它们被绘制。
                        你可以关闭culling通过把下面的代码放到Draw方法调用的GraphicDevice.Clear方法下面:
1 GraphicsDevice.RenderState.CullMode=CullMode.None;
                        虽然你通常不会关闭culling,因为它是一个极其强大的性能助推器,它可以帮助调试。例如,如果你想你应该看见一些东西在屏幕上,但是它没有显示出来,你可以关闭culling看看,真实对象是否那,但被culled是出于某种原因。

More on Rotations
                        另一件事你可能注意到了,当使用你的三角形时,是程序第一次开始,三角形看上去好象自转。如果你移动三角形到左边,它仍然翻转,但是现在它看上去在原点转圈。这是因为为了*移和旋转,使因而发生的效果大不同。
                        当一个旋转被应用,旋转总是翻转对象围绕这初始点。在你的代码中,你首先应用这个*移,然后旋转在每一祯之间。所以,当程序首先被加载,这对象在初始点上被绘制,旋转导致它围绕初始点翻转,它给出了在这个空间自转的效果。
                        无论如何,一旦你移动对象向左,添加旋转到这个对象,这样,它围绕着初始点翻转(现在是对象的右边)导致它有一个转圈的效果。
                        为了让你的对象自转不管它在哪里,你首先都需要应用这个旋转,然后是*移。这会导致旋转被应用同时对象在初始点(带来spinning in place的效果),*移将随后移动对象,它的旋转到指定的位置。
                        要作到这一点,而不是有一个单一对象代表世界为这个角度,需要添加两个变量去表现对象的世界(你可以同样去掉Matrix变量调用前面添加的world):
1 Matrix worldTranslation=Matrix.Identity;
2 Matrix worldRotation=Matrix.Identity;
                        接下来,修改代码在update方法去应用这个旋转和*移到只想得到的矩阵。改变下面的代码:
1 //Translation
2 KeyboardState keyboardState=Keyboard.GetState();
3 if(keyboardState.IsKeyDown(Keys.Left))
4       world*=Matrix.CreateTranslation(-.01,0,0);
5 if(keyboardState.IsKeyDown(Keys.Right))
6       world*=Matrix.CreateTranslation(.01f,0,0);
7 //Rotation
8 world*=Matrix.CreateRotationY(MathHelper.PiOver4/60);
到这里:
1 //Translation
2 KeyboardState keyboardState=Keyboard.GetState();
3 if(KeyboardState.IsKeyDown(Keys.Left))
4      worldTranslation*=Matrix.CreateTranslation(-.01f,0,0);
5 if(keyboardState.IsKeyDown(Keys.Right))
6      worldTranslation*=Matrix.CreateTranslation(.01f,0,0);
7 //Rotation
8 worldRotation*=Matrix.CreateRotationY(MathHelper.PiOver4/60);
                        这允许游戏保持你的旋转分离出*移,允许你应用它们为了你想要去当你绘制你的对象时。为了做到这样,改变代码在Draw方法中,设置BasicEffect.World参数到这里:
1 effect.World=worldRotation*worldTranslation;
                      每次旋转,*移,缩放添加在这种方式将会不同的效果在对象被绘制时,依赖于这个顺序你应用它们。例如,看看会发生什么变化,如果你改变了前面的代码:
1 effect.World=Matrix.CreateScale(.5f)*worldRotation*worldTranslation;
                      应用一个缩放.5f在这个顺序的前面导致你的三角形被绘制只有一半的大小。然而,看看你是否能分辨这个代码和那个代码的区别:
1 effect.World=worldRotation*worldTranslation*Matrix.CreateScale(.5f);
                      这是一个微妙的变化,但是它有一个重要的效果。这代码的最后一行将应用.5缩放到在这个顺序中的左边。这意味这三角形在第二个例子中很快变成了一半,由于这个缩放将影响*移。在第一个例子,缩放同样影响任何事情在它的左边时,但是因为这里什么也没有,这缩放将只用于绘制三角形,不是*移。
                      如果你想模拟围绕着太阳旋转?你如何做这个?好,考虑下什么行星围绕太阳旋转。通常情况下,它自转同时也公转,对吗?为了使物体自转,你首先应用一个旋转。然后需要移动这个对象用一个固定的距离围绕太阳(这种情况下,是原始点)旋转。最后,使这个对象"公转"围绕初始点,你最好应用另外一个旋转,它使对象围绕着初始点旋转。
                      尝试下改变代码这样,设置world到下面的行中:
1 effect.World=worldRotation*worldTranslation*worldRotation;
                      编译运行这个游戏在这时。移动对象或左或右,你应该看见它自己翻转同时围绕原始点公转。很酷,啊?我建议,现在你花点时间熟悉旋转和*移,去得到一个好的感觉。用不同的顺序应用它们看看会发生什么。

Even More Rotations
                      到目前为止,你已经看见如何去旋转,*移和缩放一个对象了,但是你翻转它仅仅使用了一个方法,它称为CreateRotationY,它旋转一个对象围绕着Y-轴。
                      这里有另外个旋转,它值得注意。你可能已经猜到了,这里有个CreatRotationY,也同样会有个CreateRotatonX和CreateRotationZ方法。这是一个不错的猜测,作为结果,你说得对。这些方法可用于旋转一个物体分别围绕这Y - ,X或Z轴。
                      另外一个方法被应用于旋转的是Matrix.CreateFromYawPitchRoll.从本质上讲,这个方法允许你去创建一个旋转,它同时结合围绕X-,Y-和Z-轴的旋转。yaw旋转一个对象围绕Y-轴,pitch旋转一个对象围绕X-轴,roll旋转围绕Z-轴。
                      这里还有另外一个方法,它应该被提到:Matrix.CreateFromAxisAngle.这个方法接收一个参数以Vector3的形式,它代表一个轴在旋转一个对象(而不是指定X,Y,Z)的上面,以旋转对象的一个角度。例如,想象一下你正在创建一个太阳系的模型。你想指定一些任意轴让地球自转,因为地球的轴是倾斜的,但并不完全相同的X,Y,和Z轴。
                      我建议在这里,你暂停一下,花点时间去应用不同旋转的类型到你的对象中,去获得一个很好的感觉它们都能做什么。试下应用一个旋转使用CreateFromYawPitchRoll通过改变旋转代码在你的Update方法中:
1 //Rotation
2 worldRotation*=Matrix.CreateRotationY(MathHelper.PiOver4/60);
3 到这:
4 //Rotation
5 worldRotation*=Matrix.CreateFromYawPitchRoll(MathHelper.PiOver4/60,0,0);
                      然后实验用不同这些代码的版本,通过放入不同的值为三个参数中的每一个。

Primitive Types
                      所以,你准备长时间的巡航,享受XNA3D-land的旅程....但是它仍然没有让人兴奋。你已经创建一个cool-looking三角形和在周围移动它。同时这个三角形用混合颜色得到一个很酷的效果,你可能从来没有见过一完整的游戏包括象这样的颜色。一般来说,当用3D绘制,你先绘制primitives,然后应用纹理到这个primitives.
注意:应用一个纹理?它是什么?在前面的章节中,一个纹理代表一个2D位图或者另外的图象。纹理在3D图形里是经常映射到3D物体表面(如你的三角形在前面的章节中)。这个映射一个2D纹理到3D表面的过程是提到作为纹理的应用。
                      因此,在这章的剩下来,我们将集中在应用一个纹理到你的三角形中。虽然,你准备使用一个矩形的纹理,这样你需要转换你的三角形成一个矩形。这是如何实现的?好,你不能一开始就绘制矩形,你需要用三角形来绘制。虽然,两个几乎互相靠*三角形可以组成一个正方形或者长方形。
                      改变这个代码在你的LoadContent方法中,初始化你的顶点数组:
1 verts=new VertexPositionColor[4];
2 verts[0]=new VertexPositionColor(new Vector3(-1,1,0),Color.Blue);
3 verts[1]=new VertexPositionColor(new Vector3(1,1,0),Color.Yellow);
4 verts[2]=new VertexPositionColor(new Vector3(-1,-1,0),Color.Green);
5 verts[3]=new VertexPositionColor(new Vector3(1,-1,0),Color.Red);
                      下一步,在你的Draw方法中,你需要改变最后GraphicsDevice.DrawUserPrimitives的参数改变成2。
记住,这个参数不是一个顶点数量,而是一个原始(译者:三角形)数量;它告诉图形设备多少个primitives你希望去绘制在这次调用中。现在你希望去绘制两个三角形,这样你需要设置适当的参数:
1 GraphicsDevice.DrawUserPrimitives<VertexPositionColor>(PrimitiveType.TriangleStrip,verts,0,2);
                      现在编译运行这个游戏,你将会看见三角形被改变成一个矩形。
                      这个awe-inspiring的矩形是如何做出来的?这个关键是调用到Graphice.DrawUserPrimitives的第一个参数。这里有三个不同的方法去绘制初始的三角形在XNA中;你可以使用一个三角形列表来绘制,一个三角形条带,或者一个三角形扇子。在你的调用GraphicsDevice.DrawUserPrimitives中,你当前告诉图形设备去绘制你的顶点使用一个PrimitiveType.TriangleStrip.让我们来看看在不同的选项。
                      一个三角形表单将接受第一个指定的三顶点,创建一个来自于它们的三角形。然后建立一个另外的三角形,它来自其他额外一批三个顶点。这个三角形表单可能是最复杂的方式去绘制三角形,但是它同样是最有效的当你指定三个新的顶点为每个被绘制的三角形。(如图9-12)
                      三角形条带同样创建一个来自于指定的三个顶点的三角形,但是它随后创建一个新的三角形有每一个额外的顶点通过使用新的顶点和前面两个顶点。注意着些三角形的不同之处,即使你使用相同的点,指定它们用同样的顺序,在前面的三角形表单例子中。(如图9-13)
                      三角形风扇将类似的创建一个新的三角形用第一个指定的三个顶点,创建一个新的三角形用各自额外的顶点,但这样做它使用了新的顶点,以前的顶点和第一个顶点。因此,一个扇面被构成了,如果9-14(再一次使用同样的来自于前面例子的点)。
                      现在花点时间去熟悉不同的原始的类型。试一下改变这原始类型由PrimitiveType.TriangleStrip到PrimitiveType.TriangleList和PrimitiveType.TriangleFan用这四个当前你有的点,确保理解每种情况都发生了什么。
注意:哇!等一下!当您运行的游戏使用的是三角形列表,游戏崩溃了,是怎么回事?三角形列表要求三个顶点为每个初始三角形,所以图形设备希望接收六个顶点,而你只提供了4个。为了解决这个问题,你可以改变DrawUserPrimitives的最后的参数成1(意思是你只想绘制一个初始的三角形);XNA随后只绘制出一个三角形,使用在数组中第一组三个顶点。

                       你同样可以解决此问题通过添加两个或者更多顶点到你的数组中;在这种情况下,XNA绘制两个三角形使用提供的六个顶点。

Applying Textures
                      行了,你现在有一个长方形,你准备应用一个纹理到你的矩形中。首先,你不得不告诉图形设备,你准备使用纹理与你的顶点。目前,该类型的对象您使用的是代表你的顶点的是VertexPositionColor,它告诉XNA,你想为你的顶点使用一个位置和颜色。需要改变这个使用一个不同的对象类型称为VertexPositionTexture,它表示一个顶点,它有位置和纹理。

为你的顶点数组变量改变类型在类的顶部:
1 VertexPositionTexture[] verts;
                     下一步,改变代码在LoadContent方法,初始化顶点。VertexPositionTexture的结构接受两个参数:Vector3代表顶点的位置,Vector2代表一个纹理坐标。
                      什么是"纹理坐标"?它是一个非常好的问题。一个纹理坐标是一个方式去映射一个坐标在一个纹理上到primitive(译者:三角形)的一个顶点。当纹理原始以这种方式,你确定一个纹理的点,它对应顶点,然后XNA处理处理指定纹理的部分,映射它从而在primitive(译者:三角形)上。
                      一个纹理坐标通过两个空间(U,V)坐标来表示,U是水*的,V是垂直的。一个图象的左上角通过纹理坐标(0,0)来表示,图象的右下角被纹理坐标(1,1)来表示,不管图象的大小。为了指定一个点在一个纹理的中间,你可以使用纹理坐标(0.5,0.5).
                      你可以思考下U和V作为一个纹理大小的百分数,用1来相当于100%或者宽度0等于0%。任何给定纹理的(U,V)坐标如图9-15所示。
                      当你初始化你的顶点,你需要定义在这个纹理上的哪个点,将映射到哪个顶点,用适当的(U,V)坐标来赋值到这个顶点。然后,当你绘制primitives,XNA将映射这个纹理在这个primitives(译者:三角形)上。
                      改变你的顶点初始化代码在LoadContent方法中,使用VertexPositionTexture对象,设置(U,V)坐标:
1 verts = new VertexPositionTexture[4];
2 verts[0= new VertexPositionTexture(new Vector3(-110), new Vector2(00));
3 verts[1= new VertexPositionTexture(new Vector3(110), new Vector2(10));
4 verts[2= new VertexPositionTexture(new Vector3(-1-10), new Vector2(01));
5 verts[3= new VertexPositionTexture(new Vector3(1-10), new Vector2(11));  
                      其次,改变顶点声明在Draw方法中。记住,这个顶点声明从本质上告诉图形设备,你发送的信息的类型,所以它知道如何去处理这个信息。目前,你已经告诉图形设备,你将会发送VertexPositionColor数据。改变这个顶点声明在你的Draw方法中:
1 GraphicsDevice.VertexDeclaration =new VertexDeclaration(GraphicsDevice,VertexPositionTexture.VertexElements);
                      你同样指定VertexPositionColor普通方法类型在你的DrawUserPrimitives调用在你的Draw方法里。用VertexPositionTexture改变它:
1 GraphicsDevice.DrawUserPrimitives<VertexPositionTexture>(PrimitiveType.TriangleStrip, verts, 02);
                      好的,现在你已经准备去绘制VertexPositionTexture顶点,它会让你应用一个纹理到你的矩形中。现在所有你需要做的是创建一个纹理,并且应用它。
                      正如你在2D精灵所处理的那样,你需要添加一个图象文件到你的工程中。如果你没有准备好,下载这个源代码。
                      右击Content文件夹在解决方案中,选择Add-New Folder,并且命名这个新文件夹为Textures.然后,右击这个新Textures文件夹,选择Add-Existing Item....,浏览到tree.jpg文件。选择那个文件添加它到你的工程中。
                      下一步,您必须添加一个新的类级别变量您Game1类的纹理:
1 Texture2D texture;
                      并且载入纹理到你的LoadContent方法中。确保你使用这个资产名称为你的图象在这个参数到Content.Load方法(如果你使用一个自己的图象,差别是不能命名为"Colors"):
1 texture = Content.Load<Texture2D>(@"Textures\trees");
                      最后,在你的Draw方法,你需要告诉BasicEffect,它的纹理被使用,并且启用这个纹理。改变颜色在Draw方法中:
1 effect.VertexColorEnabled = true;
                      (这个顶点可以使用的颜色)到这(可以使用的纹理):
1 effect.Texture = texture;
2 effect.TextureEnabled = true;
                     好了,现在你已经准备好。编译并运行该应用程序,您会看到和前面相同的旋转矩形,但是现在纹理被应用到primitives而不是你以前看见的混合的颜色。根据你所选择的图象去添加到你的工程中,你应该看见相似的东西在9-16中。
                     不错!现在这是一个非常酷的游戏。你已经创建一个spinning,旋转,移动,树型图片....可怕!在下一章中我们将开始使用3D模型而不仅仅是一个三角形或者矩形,那就是当开发真正得到三维乐趣。
注意:如果你打算就这样开发,这需要你绘制更多顶点用这种方法描述在这章,有一些事情你可以做的事情,使自己更容易值得注意。我们将不会涉及这一深度在这本书,因为我们将侧重于绘图与三维模型,而不是个别顶点绘图,因为在这里显示,但使用IndexBuffer ,您可以重复使用单点在三维空间中的多点你的游戏。
源代码:http://shiba.hpe.cn/jiaoyanzu/WULI/soft/xna.aspx?classId=4
(完)

posted on 2009-08-17 14:45  一盘散沙  阅读(793)  评论(1编辑  收藏  举报

导航