请注意,XNA 三维入门开始了哦:
1.X轴以向右为正方向,Y轴以向上为正方向。但是,至于Z轴的方向就不是那么明确了。存在两种不同的三维坐标系,两者之间的Z轴正方向相反。Z轴的指向决定了坐标系的朝向,或者说是使用左右手的习惯。这两种可能的坐标系分别为:左手系,右手系。
区分左手系还是右手系的方法之一就是,伸出你的手平放,掌心向上,四指指向X轴正方向同时向Y轴正向卷起。大拇指所指向的方向就是坐标系Z轴正方向。
NA采用的是右手坐标系,这就意味着当您以正常的角度向坐标原点看去的时候,X轴向右为正方向,Y轴向上为正方向,Z轴的正方向指向你。
2.摄像机
XNA中的摄像机由两个Matrix对象构成:视图(view)矩阵和投影(projection)矩阵。
视图矩阵存放以下信息:摄像机所在的位置、所指的方向和角度。
投影矩阵存放以下信息:视角和视觉范围等等。投影矩阵代表了从3D空间到2D平面的变换。
为创建一个视图矩阵,您需要用到Matrix 类里面一个名为CreateLookAt的静态方法。该方法返回一个Matrix对象,并需要接受如下参数:
参数 类型 描述
cameraPosition Vector3 摄像机的位置坐标
cameraTarget Vector3 摄像机朝向的目标
cameraUpVector Vector3 指明那个方向是向上
Vector3代表的是三维坐标(X, Y, Z)。
为了创建一个投影矩阵,您需要使用到Matrix 类里面另外一个名为Matrix.CreatePerspectiveFieldOfView的静态方法。该方法返回一个Matrix对象,并需要接受如下参数:
参数 类型 描述
fieldOfView float 摄像机的视角弧度,通常是45度或者π/4
aspectRatio float 摄像机长宽比,通常使用屏幕宽度除以屏幕高度
nearPlaneDistance float 当距离摄像机多近时,无法看清物体
farPlaneDistance float 当距离摄像机多远时,无法看清物体
投影矩阵为您的摄像机建立一个视锥或者视野。本质上,它决定了三维空间中哪些区域是摄像机可见的,可以绘到屏幕上的。在该区域内的物体都可以出现在屏幕上,除非物体被出现在它们与摄像机之间的物体挡住了。处于视锥范围之外的物体不会出现在摄像机屏幕上。
通过调用Matrix.CreateLookAt来初始化视图矩阵;通过调用Matrix.CreatePerspectiveFeldOfView来初始化投影矩阵。
public Matrix view {get; protected set;} public Matrix projection { get; protected set; } public Camera(Game game, Vector3 pos, Vector3 target, Vector3 up) : base(game) { view = Matrix.CreateLookAt(pos, target, up); projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4,
(float)Game.Window.ClientBounds.Width /(float)Game.Window.ClientBounds.Height, 1, 100); }
上边代码中 view变量表示一个视图矩阵,projection变量表示投影矩阵,CreateLookAt方法和CreatePerspectiveFieldOfView方法的参数可以对比上文的参数列表。
MathHelper.PiOver4来表示π的四分之一,在弧度制中,π等于180°。进一步地,π/ 4是45°,这是一个标准视角。MathHelper.PiOver4的值是一个便于使用的常量。
3.绘制基元
所有3D图形的基础都是三角形。如果你画出足够多的三角形,就能得到几乎所有形状的图形。
为了绘制你的第一个三角形,您需要定义一些点,或者说是顶点(vertices),来表示三角形的每个角。你也需要一个称为VertexBuffer的对象,它被用来储存顶点信息以供在图形设备上使用。
public class Camera : Microsoft.Xna.Framework.GameComponent { public Matrix view { get; protected set; } public Matrix projection { get; protected set; } public Camera(Game game) : base(game) { // TODO: Construct any child components here } public Camera(Game game, Vector3 pos, Vector3 target, Vector3 up) : base(game) { view = Matrix.CreateLookAt(pos, target, up); //fieldOfView 摄像机的视角弧度,通常是45度或者π/4 //aspectRatio 摄像机长宽比,通常使用屏幕宽度除以屏幕高度 //nearPlaneDistance 当距离摄像机多近时,无法看清物体 //farPlaneDistance 当距离摄像机多远时,无法看清物体 projection = Matrix.CreatePerspectiveFieldOfView( MathHelper.PiOver4, (float)Game.Window.ClientBounds.Width / (float)Game.Window.ClientBounds.Height, 1, 100); } public override void Initialize() { base.Initialize(); } public override void Update(GameTime gameTime) { base.Update(gameTime); } }
Camera类是一个摄像机组件,定义了视图矩阵和投影矩阵需要在Game1类中进行使用。
public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; // 摄像机对象 Camera camera; // 一个自定义顶点格式结构,包含位置和颜色信息 VertexPositionTexture[] verts; //代表名单三维顶点流的图形设备 VertexBuffer vertexBuffer; // 特效对象 BasicEffect effect; public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; } ////// Allows the game to perform any initialization it needs to before starting to run. /// This is where it can query for any required services and load any non-graphic /// related content. Calling base.Initialize will enumerate through any components /// and initialize them as well. /// protected override void Initialize() { // 初始化摄像机对象 camera = new Camera(this, new Vector3(0, 0, 5), Vector3.Zero, Vector3.Up); //将组件添加到组件集合中 Components.Add(camera); base.Initialize(); } /// /// LoadContent will be called once per game and is the place to load /// all of your content. /// protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); verts = new VertexPositionTexture[4]; verts[0] = new VertexPositionColor(new Vector3(0, 1, 0), Color.Blue); verts[1] = new VertexPositionColor(new Vector3(1, -1, 0), Color.Red); verts[2] = new VertexPositionColor(new Vector3(-1, -1, 0), Color.Green); //储存顶点信息以供在图形设备上使用 vertexBuffer = new VertexBuffer(GraphicsDevice, typeof(VertexPositionTexture), verts.Length, BufferUsage.None); vertexBuffer.SetData(verts); // 初始化特效 effect = new BasicEffect(GraphicsDevice); } protected override void UnloadContent() { } protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) base.Update(gameTime); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); // Set the vertex buffer on the GraphicsDevice GraphicsDevice.SetVertexBuffer(vertexBuffer); //绘制的对象的位置 effect.World = Matrix.Identity; effect.View = camera.view; effect.Projection = camera.projection; effect.VertexColorEnabled = true; // 每一个effect有一个或多个technique(技术)。每一个technique有一个或多个pass(通道) foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Apply(); GraphicsDevice.DrawUserPrimitives (PrimitiveType.TriangleStrip, verts, 0, 2); } base.Draw(gameTime); } }
在Game1类中需要一个称为VertexBuffer的对象,它被用来储存顶点信息以供在图形设备上使用;同时也有VertexPositionColor[] verts这样一个数组,表示点和颜色
的数组;也需要使用一个叫BasicEffect的东西来绘制您的基元,实际上,XNA 3D中的任何东西都需要使用效果(通常是高级着色器),BasicEffect类一般允许您使用默
认着色器代码来3D绘制。
在LoadContent中对VertexPositionColor数组进行了初始化,指定了 xyz的坐标和对应的颜色;同时初始化了VertexBuffer,在参数中传递了图形驱动对象,同时指定了类型,此处为VertexPositionColor,也就是数组的类型。
在Draw方法中GraphicsDevice.SetVertexBuffer(vertexBuffer)设置VertexBuffer为我们定义的VertexBuffer对象;同时设置View和Projection为Camera中的View和Projection,设置VertexColorEnabled 为true否则颜色不会被绘制(还有对应的设置图片的属性);Matrix.Identity是物体被绘制的默认位置,这个默认位置在坐标原点附近。
关于effect,还需要了解还有更多的信息。每一个effect有一个或多个technique(技术)。每一个technique有一个或多个pass(通道)。您的代码中紧接着的
for循环将会循遍technique每个pass,用以绘制三角形。为了在绘制technique时开始一个pass,您将必须调用EffectPass.Apply()。后面我们深入探讨HLSL
时,将会更多地探讨这些。但现在只要认为EffectPass.Apply()与前章提到的SpriteBatch.Begin和SpriteBatch.End相似就行了。调用Begin和End之时通过
使用SpriteBatch绘制所有的精灵。这同样适用于这些代码:所有的通道必须通过调用自己的Apply来执行。
DrawUserPrimitives是一个泛型方法,因此,你必须指定你将要绘制的顶点的类型(在本例中,类型为VertexPositionColor)。第一个参数是绘制三角形的方法,下文会介绍枚举的不同值的区别。现在,只需要知道给出三个顶点之后,传入到PrimitiveType.Triangle Strip参数会使得XNA使用这三个顶点构建一个三角形。第二个参数是顶点数组,那里面存储了顶点信息。第三个参数是这个数组的偏移量,也就是最开始绘制的顶点的序号。你需要从数组的最开始的元素进行绘制,因此指定这个参数为0。最后一个参数是图元数量,或者说是你希望绘制的图元的数量。一个三角形就是一个基元,并且你需要绘制一个三角形,因此你传入1作为参数。
P.S: PrimitiveType枚举的值
PrimitiveType.TriangleList 表 每一组三顶点构成一个三角形
PrimitiveType.TriangleStrip 带 前三个顶点构成第一个三角形,然后每个新顶点和它之前两个顶点再构成一个新三角形
详细信息详见MSDN。
上文示例会得到一个渐变颜色的三角形,问题来了,你是怎么绘制出三角形呢,聪明的你应该猜到了,就是数组的三个点,一个点对应一个颜色,然后XNA用这三个点和三个颜色创建了渐变的三角形.
4.矩阵乘法--移动旋转--背面消隐--旋转
我们已经有了一个神奇的三角形,接下来谈谈旋转和平移;在3D图形编程中做的一切,背后都是矩阵。尤其是当你试图移动、旋转、缩放一个对象时。
BasicEffect的World属性就是一个矩阵,该矩阵告诉XNA在哪绘制您要绘制的东西、如何经过恰当的旋转和缩放把要绘制的东西放置在空间中.
Matrix.Identity所代表的矩阵也称之为单位矩阵(identity matrix)。单位矩阵是一种特殊的矩阵,在矩阵乘法里它的作用相当于数字1。也就是说,任何一个矩阵与单位矩阵相乘的结果都是矩阵本身。
移动与旋转:
所谓平移(translation)就是按照向量指定的方向来移动一个物体或者一个点。因此,Matrix.CreateTranslation以Vector3类型作为参数。它也有一个能接受浮点类的X、Y和Z的重载方法。
如果您想动态持续地移动或旋转您的物体,而不是改变它的位置然后静止不动,那您就需要用一个变量来代表物体的世界坐标。在Game1类里面定义一个名为Matrix的类变量,然后把它初始化为Matrix.Identity:
Matrix world = Matrix.Identity;
接着,修改Draw方法里面如下的代码,设置BasicEffect.World的属性为刚才定义的新变量:
effect.World = world;
然后,把以下代码添加到Update方法里,位于base.Update方法的调用之前:
KeyboardState keyboardState = Keyboard.GetState( ); if (keyboardState.IsKeyDown(Keys.Left)) world *= Matrix.CreateTranslation(-.01f, 0, 0); if (keyboardState.IsKeyDown(Keys.Right)) world *= Matrix.CreateTranslation(.01f, 0, 0);
编译并运行游戏,你会注意到当你按下向左或向右键时,物体将向对应的方向移动。
旋转物体:
在平移物体的代码下添加如下代码:
world *= Matrix.CreateRotationY(MathHelper.PiOver4 / 60);
这段代码运用了Matrix类所提供的基本旋转方法中的Matrix.CreateRotationY。该方法让物体绕着Y轴旋转,旋转的角度由参数决定。度数是用弧度制表示的,而不是度数制。在弧度制里,π是180度。参数MathHelper.PiOver4/60会将物体旋转0.75度,度数大家可自行修改。现在编译并运行游戏,您将看到三角持续地绕Y轴旋。您仍然可以通过向左或向右按键控制它的水平移动。
你可能注意到了,当移动三角形时,你不仅仅移动了它本身,同时也改变了旋转的中心。其实从技术上说,你并没有改变旋转的中心(只要旋转用的是Matrix.CreateRotationY,物体就围绕着原点旋转)。
你只是改变了三角形所处的位置和旋转中心的关系。当游戏开始时,三角形是绘在原点处的,意味着当调用Matrix.CreateRotationY时,三角形在原地(当前中心是原点)旋转。当你把物体移动到右边,三角
形开始围绕原点旋转。事实上是这么一回事,相当于先把三角形移动到右边,然后调用Matrix.CreateRotationY方法使它围绕原点旋转。
Tip:
当给world变量赋值的时候,用的是*=而不是=。这点非常重要,不仅仅是因为它会在每一帧里持续地增加旋转度数,从而使得物体可以持续地移动和旋转,同时也因为Matrix变量包含了物体如何放置在世界坐标系中的多个方面的信息(位置、旋转和缩放等)。
您通过使用Matrix.CreateTranslation设置物体位置,而如果您使用了=而不是*=来通过Matrix.CreateRotationY设置旋转度数,您就会失去之前设置的平移变换的信息。
运行我们的游戏,你会看到一个非常神奇的效果,当旋转180度的时候,物体消失了,当做完一个 360度旋转之后又出来了,这是为什么呢,且看下文的“消隐”?
背面消隐:
你看到三角形消失的处理过程称为背面消隐(backface culling)。消隐是一种3D图形学中的处理过程,它为了提高性能而限制显示在屏幕上的物体数目。本质上来说,背面消隐目的是只在屏幕上绘制图元朝向摄像机的一面。在这个程序里,只显示了三角形的一个面。
默认情况下,XNA会消隐在将逆时针方向被绘制的图元。
如果你用逆时针的顺序来创建顶点,然后运行游戏,你将会看到,一开始三角形就被消隐掉了,因此不可见。在这种情况下,只有当三角形转过了180°以后,你才能看见它。
消隐的最主要目的就是提高的程序的性能,所以一般不会关闭,当然也可以手动进行关闭(在调试状态下看看是否物体显示正常)。
手动关闭消隐:
RasterizerState rs = new RasterizerState(); rs.CullMode = CullMode.None; GraphicsDevice.RasterizerState = rs;
关于旋转和平移的关系:
之前的平移和旋转中,当程序开始运行时,三角形原地自转;如果您把三角形移动到左边,它仍然会旋转,但现在它好像是围绕着原点转圈而不是原地自转。这是因为平移和旋转的先后顺序不同会产生不同的效果。
当应用旋转变换时,物体总是围绕原点旋转。在您的代码中,每一帧都是先执行平移然后旋转。所以当加载应用程序时,物体绘制在原点处,旋转指令使得它围绕原点旋转(自转),这就出现了在原点自转的效果。
然而,一旦您移动物体到左边,然后执行旋转指令,让它围绕原点旋转(现在原点已经在物体的右边),将促使它产生围着原点转圈的效果。
为了使物体不管在哪里都是自转,您需要先应用旋转变换,然后才应用平移变换。当物体在原点时,先让它产生自转的效果,然后执行平移变换把物体移动到合适的位置。物体将保持自转,而不是围绕原点公转
为了实现旋转和平移的更好发生(即旋转在物体平移后所在的位置的远点),则需要修改之前的代码:
首先增加两个变量,分别表示平移的world和旋转的world:
Matrix worldTranslation = Matrix.Identity
Matrix worldRotation = Matrix.Identity;
然后修改Update方法中旋转和平移的代码如下:
KeyboardState keyboardState = Keyboard.GetState( ); if (keyboardState.IsKeyDown(Keys.Left)) worldTranslation *= Matrix.CreateTranslation(-.01f, 0, 0); if (keyboardState.IsKeyDown(Keys.Right)) worldTranslation *= Matrix.CreateTranslation(.01f, 0, 0); // Rotation worldRotation *= Matrix.CreateRotationY(MathHelper.PiOver4 / 60);
可以看到平移和旋转使用两个不同的变量,这样就不会出现冲突,等等还没结束...
修改Draw中Effect.Word如下:
effect.World = worldRotation * worldTranslation;
这样平移和旋转的关系才做到了统一一致。
Tip:
每一个以这种方式添加的旋转、平移、缩放都会对将要绘制的物体产生不同的效果,这取决于您运用它们的先后顺序。举个例子,当您把之前的代码改为如下代码会发生什么? effect.World = Matrix.CreateScale(.5f) * worldRotation * worldTranslation; 先应用0.5倍的缩放使得绘制出来的三角形只有原来的1/2。看看您能否指出出下面的代码和上面的代码的不同之处: effect.World = worldRotation * worldTranslation * Matrix.CreateScale(.5f); 这是一个很微妙的变化,但是将产生重大的影响。后一行代码将对Matrix.CreateScale左边的所有变换进行缩放0.5倍。这意味着在第二种情况中物体只是移动了一半远,因为缩放变换影响到了平移变换。在第一种情况中,缩放变换也影响到它左边的所有变换,但是它左边什么都没有,所以它只会影响三角形的绘制,但不会影响到平移变换。
再次旋转:
目前为止,你已经知道怎么平移、旋转和缩放物体,但是你只是通过一个名为CreateRotationY的方法来产生旋转,该方法使物体围着Y轴转动。
还有一些值得一提的旋转变换。你可能已经猜到了,既然有CreateRotationY,应该也有CreateRotationX和CreateRotationZ方法。。这些方法分别能使物体绕X轴、Y轴和Z轴转动。
另一个应用旋转的方法是Matrix.CreateFromYawPitchRoll。从本质上来说,这种变换会让物体同时绕着X轴、Y轴和Z轴进行旋转。如图所示,偏航(yaw)使一个对象围绕Y轴旋转,俯仰(pitch)使一个对象围绕X轴旋转,翻转(roll)使对象围绕Z轴旋转。
另一个方法也应该在这里提一下:Matrix.CreateFromAxisAngle。这个方法有一个Vector3类型的参数。这个参数代表物体绕旋转时所围绕的轴(而不是特定的X轴、Y轴或Z轴),同时还有旋转物体所需的旋转角度。举个例子,假设你正在创建一个太阳系。
你就要给地球指定一个特定的轴使地球绕该轴自转,因为地轴是倾斜的,没有与X轴、Y轴或Z轴重合。
大家可以使用不同的方法,设置不同的值感受下效果的不同。
5.基元类型
修改LoadConten中初始化数组的代码如下:
verts = new VertexPositionColor[4]; verts[0] = new VertexPositionColor(new Vector3(-1, 1, 0), Color.Blue); verts[1] = new VertexPositionColor(new Vector3(1, 1, 0), Color.Yellow); verts[2] = new VertexPositionColor(new Vector3(-1, -1, 0), Color.Green); verts[3] = new VertexPositionColor(new Vector3(1, -1, 0), Color.Red);
接着,在Draw方法中,你需要把GraphicsDevice.DrawUserPrimitives的最后一个参数改为2。请记住,这个参数不是顶点的个数,而是图元的个数。在调用时,该参数告知图形设备您需要多少个图元。现在你需要绘制两个三角形,你需要配置合适的参数。
GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleStrip, verts, 0, 2);
现在编译并运行游戏,你将看到三角形已经变成了一个矩形。
为什么如此神奇呢,从三角形变成了矩形呢?
GraphicsDevice.DrawUserPrimitives的第一个参数。在XNA中有三种不同的方法绘制三角形图元:triangle list(表)、triangle strip(带)和triangle fan(扇)。
调用GraphicsDevice.DrawUserPrimitives就是告知图形设备用PrimitiveType.TriangleStrip来绘制顶点
三角表(triangle list)取出第一组特定的三顶点然后用它们来创建三角形。然后用额外的一组顶点建立另外的一个三角形,如图。三角表可能是绘制三角形最不复杂的方式,但是它同样是最没有效率的方法,因为您要为每个被绘制的三角形指定三个新的顶点。
三角带(triangle strip)同样创建一个指定了三个顶点的三角形,但是随后它通过一个新的顶点和先前的两个顶点来创建一个新的三角形。看看图,看看这些三角形的不同之处,尽管和在前面列表三角形例子中的点相同、顺序一样。
三角扇(triangle fan)类似的用第一组指定的三个顶点创建一个新的三角形,然后用各自额外的顶点创建一个新的三角形,但这样做它使用了新的顶点、前一个顶点和第一个顶点。因此,构成了一个扇面,如图(再一次同样使用来自于前面例子的顶点)
尝试一下通过当前已有四个顶点来改变基元类型从PrimitiveType.TriangleStrip到PrimitiveType.TriangleList和PrimitiveType.TriangleFan,确保您已经理解了每种情况。
6.应用纹理
我们已经有了一个矩形,是否可以考虑为矩形设置一个纹理呢。
首先,需要通知图形设备,准备对顶点使用纹理。当前,用来代表顶点的对象类型是VertexPositionColor,它告知XNA您想为您的顶点使用某个位置和某种颜色。
需要修改一个名为VertexPositionTexture的对象类型,它表示一个含有位置和纹理的顶点。在类的顶部为您的顶点数组变量改变类型:
VertexPositionTexture[] verts;
下一步,修改LoadContent方法中初始化顶点的代码。VertexPositionTexture的构造方法有两个参数:代表顶点位置的Vector3,代表一个纹理坐标的Vector2。
什么是“纹理坐标”?非常好的问题。纹理坐标就是将XNA纹理上的坐标映射到图元顶点的一种方法。当以这种方式添加纹理时,您就把纹理上的点和相应的顶点对应起来。然后XNA获取纹理的特定部分,并将它映射到相应的图元上。
纹理坐标用二维坐标(U, V)来表示,其中U是水平的,V是垂直的。不管图象的大小,图象左上角用纹理坐标(0, 0)来表示,图象右下角用纹理坐标(1, 1)表示。为了指定纹理中间的一个点,您可以使用纹理坐标(0.5, 0.5)。
您可以把U和V当作纹理大小的百分数,用1来便是100%的高或者宽,0表示0%。
当您初始化顶点,您需要决定纹理上那些点将映射到哪个顶点,并赋一个合适(U, V)坐标给那个顶点。然后,当您绘制图元,XNA将这个纹理映射到相应的图元上。
修改LoadContent中初始化顶点的代码:
verts = new VertexPositionTexture[4]; verts[0] = new VertexPositionTexture(new Vector3(-1, 1, 0), new Vector2(0, 0)); verts[1] = new VertexPositionTexture(new Vector3(1, 1, 0), new Vector2(1, 0)); verts[2] = new VertexPositionTexture(new Vector3(-1, -1, 0), new Vector2(0, 1)); verts[3] = new VertexPositionTexture(new Vector3(1, -1, 0), new Vector2(1, 1));
修改在LoadContent中初始化VertexBuffer的代码如下:
vertexBuffer = new VertexBuffer(GraphicsDevice, typeof(VertexPositionTexture),verts.Length, BufferUsage.None);
其实主要就是修改了,第二个参数的类型,从Color到Texture.
然后修改Draw方法中DrawUserPrimitives方法的泛型的代码如下:
GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleStrip, verts, 0, 2);
制作纹理:
定义一个二维对象:
Texture2D texture;
在LoadContent中加载图片(图片大家随意修改,至于这个语法,大家可以自行查资料,这里不再赘述):
texture = Content.Load(@"Textures\trees");
最后一步喽,设置纹理到Effect:
把在Draw方法中如下的代码:
effect.VertexColorEnabled = true;
改为:
effect.Texture = texture; effect.TextureEnabled = true;
哈哈哈,大功告成,我们做了一个会自己旋转的同时有背景纹理的矩形基元,请期待我的下一篇XNA讲解,如有意见和建议请指出。
我是源码哦!!!