[翻译]Oreilly.Learning.XNA.3.0之three
第十一章 创建一个第一人称视角的相机
在3D游戏中其中之一个重要事情是相机。选择最好类型的相机,恰当的使用它要么成功要么毁灭一个游戏。他的内容一般随着故事情节,声音,和图形品质有关。虽然相机的使用有头等重要的意义,在许多方面它是一个个人喜好的问题,一个摄象机,它为某些玩家工作的很好,这些玩家不喜欢别的形式的摄象机。许多游戏,因此允许玩家去选择不同的相机角度,允许它们去选择相机,和最适合自己的游戏风格。
在这章,我们执行一个第一人称视角的相机,并讨论关于它的移动的问题,相机在3D空间里(如一个飞行模拟器)与陆地移动的相机。
一个移动的3D相机的组成部份
在这章,你要完成第10章的代码然后在开始。打开这个项目,使用这章剩下空间,来编写代码。
在第9章和第10章,我们讨论了设立一个3D摄象机和在XNA3D中相机的基础的组成部分。你创建一个相机GameComponent,添加它到你的解决方案中在前面的章节使你可以看见这个三角形和你绘制的飞船模型。
快速复习下,摄象机由两个不同的矩阵组成:projection矩阵和view矩阵。projection定义摄象机的viewing frustum,或者视阈。记住,视觉的领域定义一个相机前面范围,它能被相机看见。视觉的领域由几个部分所组成:一个相机角度,一个横纵比率,和近景和远景剪裁平面。projection矩阵一般来说不会在游戏中改变。同时你的相机在3D中移动和旋转,viewing frustum通常保持恒定。
view矩阵定义相机坐落在世界的哪里,它如何被旋转。view矩阵由三个向量创建:相机的位置,相机朝向的地方,和向量表明哪个方向是相机的Up方向。
与此相反的投影矩阵,它不需要去改变当一个相机移动时,view矩阵不断的改变去反射新的旋转,相机的位置。
让我们看看你的Camera类:
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): base(game)
21 {
22 view = Matrix.CreateLookAt(pos, target, up);
23 projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4,(float)Game.Window.ClientBounds.Width /(float)Game.Window.ClientBounds.Height,1, 3000);
24 }
25 public override void Initialize()
26 {
27 // TODO: Add your initialization code here
28 base.Initialize();
29 }
30 public override void Update(GameTime gameTime)
31 {
32 // TODO: Add your update code here
33 base.Update(gameTime);
34 }
35 }
36 }
事实上,而不是修改view矩阵,重建这个矩阵在每一祯里。记住,view矩阵由一个位置向量组成,一个方向向量,和up向量。通过创建类别变量为这些向量的每一个,你可以修改如相机的位置,方向,和UP的方向,通过简单的修改相应的向量变量,然后用新的向量重建相机的view矩阵。
移动和旋转你的相机,需要添加下面三个类别变量到你的Camera类:
2 public Vector3 cameraPosition { get; protected set; }
3 Vector3 cameraDirection;
4 Vector3 cameraUp;
注意,cameraDirection变量与相机的目标不同(相机正在看这的实际的点)。view矩阵通过Matrix.CreateLookAt方法被创建,它接收三个参数:摄象机的位置,目标,和up向量。第二个参数,相机的目标,代表实际点,它是相机要观察的。与此相反,cameraDirection变量代表一个相关的方向,它是相机面对的方向,而不是一个目标,相机正在观察的目标。为了确定实际的目标指向,相机正在看的,你需要一起添加你的cameraPosition和cameraDirection向量。(参看图11-1)
这个意思是,两个重要的东西相关于你的cameraDirection向量。首先,代替传递cameraDirection作为你的Matrix.CreatLookAt方法的第二个参数,需要传递cameraPosition+cameraDirection。其次,你的cameraDirection向量不能(0,0,0)-这个变量必须包含一个值表示一些不同于初始位置,因为它表示这个方向,你相机正在观察的方向,这个向量(0,0,0)没有方向。
好了,现在是应该清理代码了,继续创建一个方法在你的相机类中,它能定义你的新view矩阵使用你的三个新摄象机向量:
2 {
3 view = Matrix.CreateLookAt(cameraPosition,cameraPosition + cameraDirection, cameraUp);
4 }
接下来,设置你的摄象机的Vector3变量在你的结构中。你准备接收这些三个变量在这个结构中,但是当前你不能分别保存它们;你刚才使用它们在这个结构中创建一个view矩阵调用这个Matrix.CreateLookAt方法。在构造中删除代码行,它创建你的view矩阵:
2 cameraPosition = pos;
3 cameraDirection = target - pos;
4 cameraDirection.Normalize( );
5 cameraUp = up;
6 CreateLookAt( );
请注意,一个Normalize的调用被用在cameraDirection上。这个Normalize方法接收任何向量和转换它成一个向量with a magnitude(or length)of one。为什么这样做这很明显,基本上,你将会用这个向量不仅仅去代表相机的方向,而且移动相机朝这个方向。
最后,调用CreateLookAt创建一个初始化的view矩阵基于指定的向量。
移动的方向
想想看,当你向前走,一般移动的方向是什么?一般来说,你走向的方向是你面对的。这也和很多游戏相机相同的。也有一些例外,它允许玩家看着一个方向,却移动向另一个方向,但是作为一般规则,你移动一个相机朝在这个方向,它就是面对的方向。
这是什么意思吗?嗯,因为 你已经有一个向量,它代表这个方向,相机面对的方向,你可以使用这个向量去移动你的相机前进。正如你所见的,你可以移动一个相机前进通过简单的添加方向向量到位置向量。这个移动的位置向量在方向向量的方向上,并且在它所面对的方向上轮流移动这相机前进。
那么,为什么规格化这个方向的向量呢?记住规格化向量会使它有一个长度或者一个数量级。用一的一个长度来处理一个向量,使它很容易去应用这个事,例如不同的速度值到相机的移动中(译者:可能就是添加一个移动量的大小)。
移动第一人称相机
随着cameraDirection向量被规格化,代表这个方向摄象机正在被观察,你可以很容易移动相机前进通过简单添加cameraDirecetion到cameraPosition中。做这个将会移动相机朝这相机的目标形成一条直线。向后的移动也很容易:简单的从cameraPosition减去cameraDirection。
因为这个cameraDirection向量被规格化(有一个数量级),相机的速度永远是固定的。为了允许你自己去改变相机的速度,添加一个类别float变量去代表速度:
2 if (Keyboard.GetState( ).IsKeyDown(Keys.W))
3 cameraPosition += cameraDirection * speed;
4 if (Keyboard.GetState( ).IsKeyDown(Keys.S))
5 cameraPosition -= cameraDirection * speed;
2 CreateLookAt( );
现在,你可以移动你的摄象机前进或者后退,你想添加其他的移动功能。任何好的3D第一人称摄象机同样有strafing(或者左右移动)支持。你的相机变量是位置,方向和up,所以你如何找到这些变量,将您的相机横向移动?我们可以解决这个问题用一点点的矢量数学。为了向一侧移动,考虑下什么是你需要的:需要一个向量,它指向你的相机的一侧。如果你有这个向量,移向一侧就象移动前进一样简单。你可以简单的添加sideways(译者:侧面)向量到相机的位置向量。如图11-2所示,你当前有一个向量为这个相机的up方向,和相机面对的方向的向量,但是你没有代表相机侧面的向量。
这里有矢量数学可以帮助你:cross product(译者:类似于三个方向的垂直坐标系)是一个二元运算,执行在3D空间上的两个向量,这导致另外一个向量垂直于输入的两个向量。因此,通过接收相机的up和方向向量的cross product,你会最终得到一个向量垂直于这两个向量(来自摄象机的一边)。如图11-3。你的负的up和方向向量的cross product产生一个垂直向量来自于其他的方向(相机的另外的一侧)。
注意:这不是关键的,你已经知道这些向量数学是如何工作的,但是如果你很好奇,随时阅读,一些数学课本。现在所有你需要明白的他确实是真的:任何两个向量的cross product产生第三个向量垂直于另外两个,你相机的up和方向向量的cross product产生一个向量,表明你相机的侧面方向。
它可以帮助你绘制一个站立和向前看的人。人的方向向量可能指向正前方,它的up向量可能指向正上方。up和方向向量的cross product从本质上方向是人的左臂膀所指向的方向,臂膀方向垂直于它的up和方向向量。唯一的向量,它适合这个标准,它们指向人对外面的一边(译者:左右两臂膀的方向)。
XNA提供一个方法,创建一个向量基于两个其他向量的cross product。它是静态的方法在Vector3类称为Cross.传入任何两个向量到Cross方法中,这个结果向量将会是传入两个向量的cross product。
为了使这个相机使用A和D键移动从一边到另一边,立即插入下面的代码在你刚才添加的代码后面,之前的代码它移动相机前进后退。
2 if (Keyboard.GetState( ).IsKeyDown(Keys.A))
3 cameraPosition += Vector3.Cross(cameraUp, cameraDirection) * speed;
4 if (Keyboard.GetState( ).IsKeyDown(Keys.D))
5 cameraPosition -= Vector3.Cross(cameraUp, cameraDirection) * speed;
旋转一个第一人称的相机
所有摄象机的旋转是与之前讨论的旋转相同。从本质上讲,一个相机能yaw,pich,和roll就象一个对象能够这样做。在此提醒您,yaw,pitch,和roll的旋转如图11-4所绘制的。
在这个类中,我已经教了XNA,事情之一就是在传统上不同于一些学生的理解,事实上yaw,pitch,和roll旋转,当应用一个对象或者相机,它移动和旋转在3D中,不必要是围绕相应的X-,Y-,Z-轴旋转。
例如,描绘这个相机在你当前游戏中有的。这个相机坐落在Z-轴,面对负Z的方向。如果你想旋转相机用一个roll,你可以旋转相机围绕着Z-轴。然而,如果相机旋转90度会发生什么?难道它正在朝向正X轴的方向?如果你执行一个旋转围绕着Z-轴在这个点上,你最好旋转用一个pitch比roll好。
更容易想到的yaw,pitch,和roll作为相关的向量变量到你的相机里。例如,一个yaw,而不是围绕Y-轴旋转,围绕着相机的up向量旋转。类似的,一个roll围绕着相机的方向向量旋转,一个pitch围绕由这个对象的一侧产生的向量旋转,垂直于up和方向向量。任何想法如何获得垂直向量?对,使用它之前添加机枪扫射的能力:它是up和方向向量的cross product。图11-5显示了如何yaw,pitch,和roll旋转在一个3D相机上被完成。
其中这些旋转你选择去执行在你的特定的完整的游戏中,依靠什么样的经验,你想给玩家的。例如,一典型的空间模拟器会有能力去 yaw,pitch和roll在一个无约束的样式。一个直升机的模拟器可以允许yaw,pitch,和roll一些程度,但不能允许你执行一个完整的roll(对直升机来说是一个相当艰巨的任务)。陆地上的射手可以只允许一个yaw和pitch,尽管一些游戏允许roll旋转为一些特殊动作就象踢你的头,然后头倾斜看周围角落(译者:被人飞起一脚,然后头看别的地方伸出舌头)。
一旦你决定哪些旋转你允许它发生在你的相机上,下一步是执行它们,每一个相机的旋转可以被完成,通过旋转一个或更多个你的相机的向量。一个yaw,pitch,或者roll,它有助于评估这个旋转使用这些步骤:首先,检测三个相机向量中哪个需要旋转;其次,指出哪个轴你需要这些向量去围绕旋转;最后,检测哪个方法需要去完成这个。
Rotating a Camera in a Yaw
让我们建立一个yaw为相机。三个相机向量(位置,方向和up),只有一个能改变当执行一个yaw是方向向量时。描绘一个人站立,执行一个yaw旋转(移动他的头从一边到另一边)。人的up向量不能改变,同样他的位置也是,但是方向向量(她正在看的方向)很明显的被改变了。
这个轴你想旋转这个方向向量围绕的为一个yaw是相机的up向量。这个方法一般用来旋转一个Vector3,是Vector3.Transform,它接收两个参数:源向量和初始向量,一个矩阵代表一个旋转或者平移去应用这个向量。
注意:当执行一个yaw旋转为一个相机,为什么旋转围绕这相机的up向量而不是Y-轴呢?这个Y-轴不一定是相机的up.它可以是陆地上射击游戏,但是考虑一个飞行模拟器,它自由的飞翔和旋转在三维空间里。在这种情况下,你总是想yaw围绕着相机的up向量旋转。
在你添加Vector3.Transform去执行yaw之前,你想添加一些代码去允许你的摄象机去俘获鼠标的动作。一个典型的第一人称结构使用WASD键来移动,并且鼠标来旋转摄象机。这样,去俘获鼠标的动作,添加下面的类别变量到你的Camera类中:
2 Mouse.SetPosition(Game.Window.ClientBounds.Width / 2,
3 Game.Window.ClientBounds.Height / 2);
4 prevMouseState = Mouse.GetState( );
现在你准备好编写你的yaw旋转。在你的Camera类的Update方法中,添加下面的代码在调用CreateLookAt的上面:
2 cameraDirection = Vector3.Transform(cameraDirection,
3 Matrix.CreateFromAxisAngle(cameraUp, (-MathHelper.PiOver4 / 150) *(Mouse.GetState( ).X - prevMouseState.X)));
4 // Reset prevMouseState
5 prevMouseState = Mouse.GetState( );
编译并运行该游戏在这一点上,你会看到,不仅仅你移动到3D空间,而且你现在可以yaw这个相机左右。它看上去有点笨拙,因为你不能完全旋转你的相机,但是不久后会实现。
注意:如果你的相机,相反相对于你的鼠标(如果你移动鼠标向右并且它旋转相机向左),你已经可以在MathHelper.PiOver4的左边加上负号在这个代码中。添加这个,它应该正常工作。
Rotating a Camera in a Roll
当旋转用一个roll,接下来相同的步骤去指出该做什么:问下你自己当执行一个roll哪个向量会旋转,哪个轴你应该围绕旋转,什么方法需要使用。
用一个roll,这唯一的相机向量它改变了,是相机的up向量。这个向量,你想旋转你的相机的up向量左右,它是相机的方向向量。添加下面的代码到你的Camera类的Update方法中,就象前面的prevMouseState=Mouse.GetState()行:
2 if (Mouse.GetState( ).LeftButton == ButtonState.Pressed)
3 {
4 cameraUp = Vector3.Transform(cameraUp,Matrix.CreateFromAxisAngle(cameraDirection,MathHelper.PiOver4 / 45));
5 }
6 if (Mouse.GetState( ).RightButton == ButtonState.Pressed)
7 {
8 cameraUp = Vector3.Transform(cameraUp,Matrix.CreateFromAxisAngle(cameraDirection,-MathHelper.PiOver4 / 45));
9 }
让我们使这个飞船不在旋转,这样你可以得到一更好的了解你的相机如何工作的。在ModelManager的LoadContent方法中,改变飞船的类型,这个可以由SpinningEnemy到BasicModel创建:
Rotating a Camera in a Pitch
编写一个pitch稍微有点复杂比编写一个yaw或者roll.首先,想一下为什么需要被改变当你pitch。你可以想下只有你的方向变化,但是这里有个问题:你的up向量由pitch改变了吗?
这是一个地方,你需要停下来想一想什么样的功能您想在您的相机里得到。一般来说,在一个飞行模拟器你旋转你的方向和你的up向量用一个pitch.原因是什么?请记住,用一个yaw你旋转围绕你的up向量。在一个飞行模拟器中,你想使你的up向量改变用一个roll和pitch去使你的yaw旋转的更真实。
在一个陆地射手pitching怎么样?你想旋转你的up向量用一个pitch在一个游戏的关卡中?再一次,请记住当你yaw,你这样做围绕着up向量。想象一下追捕敌人,寻找两个或三个故事翻上墙去看它是否在休息。然后,你旋转用一个yaw去扫描剩下建筑物的水平。你希望你的旋转在那种情况下基于Y-轴,旋转围绕着up向量(如果它通过你的pitch被改变)将导致一不希望的旋转。
一个解决方案是使用了矢量旋转的yaw用一个飞行模拟器,并且使用Y-轴为yaw旋转的路基相机。然而,有另外一个东西需要被考虑:通常在一个路基射手你不能让他pitch一个360度。当你向上看,一般来讲,你不能看到正上方;你可以pitch你的相机直到它是10-15度这个范围,不能在远了。原因之一在XNA中,如果在你的up向量和你的方向向量之间的角度很小,XNA不知道如何绘制,你叫它绘制什么,你多少可能有点奇怪。但是如果你准备设置一个限制在多少你可以pitch,你可能只是旋转你的up向量用一个pitch在一个象这样的游戏中。
无论哪种方式,这总有一些事情值得你去考虑的。在这个例子中,你不准备使用一个飞行模拟器的方法,这样你将会旋转up和方向向量。现在,你知道你会旋转,你需要找出哪个轴围绕旋转的。pitch旋转围绕一个向量,它是摄象机的侧面。记住使用Vector3.Cross去得到一个向量垂直你的相机的这个up和方向向量当机枪猛烈的扫射时(译者:这个老外写的真的很怪)?使用同样的向量去旋转你的方向和up向量用pitch。
在Camera类的Update方法,添加下面的代码只在prevMouseState=Mouse.GetState()行之前:
2 cameraDirection = Vector3.Transform(cameraDirection,
3 Matrix.CreateFromAxisAngle(Vector3.Cross(cameraUp, cameraDirection),(MathHelper.PiOver4 / 100) *(Mouse.GetState( ).Y - prevMouseState.Y)));
4 cameraUp = Vector3.Transform(cameraUp,Matrix.CreateFromAxisAngle(Vector3.Cross(cameraUp, cameraDirection),(MathHelper.PiOver4 / 100) *(Mouse.GetState( ).Y - prevMouseState.Y)));
Coding the Camera for the 3D Game
这一章的前几节,你创建一个自由飞行的3D摄象机。你现在准备接收相机,为了这个游戏改变它,通过这本书剩下的章节重建它。如果你想保持你的自由飞行的摄象机代码,你应该使您应该复制您的项目以保存现有的你编写的代码。
如果你下载了这章的源代码,你会发现自由飞行的相机代码在称为Flying Camera文件夹。这个代码它被用来直达这章后面的代码,也位于这个资源文件夹中,这个文件夹称为3D Game。
这个游戏,你准备去创建这个游戏,在这本书剩下部分,将使用一个固定的相机,它可以旋转45度用一个pitch和45度用yaw。稍后,你添加一些代码使飞船飞向这个摄象机,你得把它击落。
因为你不能移动你的相机,你也不会旋转用roll,您可以进入Camera类的Update方法,删除这个代码,使该功能可以使用。
要做到这一点,删除下面的代码(其中移动相机向前/向后和侧面)由的Update方法相机类别:
2 if (Keyboard.GetState( ).IsKeyDown(Keys.W))
3 cameraPosition += cameraDirection * speed;
4 if (Keyboard.GetState( ).IsKeyDown(Keys.S))
5 cameraPosition -= cameraDirection * speed;
6 // Move side to side
7 if (Keyboard.GetState( ).IsKeyDown(Keys.A))
8 cameraPosition +=Vector3.Cross(cameraUp, cameraDirection) * speed;
9 if (Keyboard.GetState( ).IsKeyDown(Keys.D))
10 cameraPosition -=Vector3.Cross(cameraUp, cameraDirection) * speed;
2 if (Mouse.GetState( ).LeftButton == ButtonState.Pressed)
3 {
4 cameraUp = Vector3.Transform(cameraUp,Matrix.CreateFromAxisAngle(cameraDirection,MathHelper.PiOver4 / 45));
5 }
6 if (Mouse.GetState( ).RightButton == ButtonState.Pressed)
7 {
8 cameraUp = Vector3.Transform(cameraUp,Matrix.CreateFromAxisAngle(cameraDirection,-MathHelper.PiOver4 / 45));
9 }
你的相机yaws和pitches360度在每个方向,它的坐标是什么呢。虽然,你想cap在22.5度在每一个方向(总共45度在一个yaw和45度在pitch)(译者:外国人写的很怪)。
要做到这一点,你需要添加四个变量在类别在Camera类中(两个代表总共的允许的旋转用pitch和yaw和两个代表当前的旋转用pitch和yaw):
2 float totalYaw = MathHelper.PiOver4 / 2;
3 float currentYaw= 0;
4 float totalPitch = MathHelper.PiOver4 / 2;
5 float currentPitch = 0;
替换下面yaw代码在Camera类的Update方法:
2 cameraDirection = Vector3.Transform(cameraDirection,Matrix.CreateFromAxisAngle(cameraUp, (-MathHelper.PiOver4 / 150) *(Mouse.GetState( ).X - prevMouseState.X)));
2 float yawAngle = (-MathHelper.PiOver4 / 150) *(Mouse.GetState( ).X - prevMouseState.X);
3 if (Math.Abs(currentYaw + yawAngle) < totalYaw)
4 {
5 cameraDirection = Vector3.Transform(cameraDirection,Matrix.CreateFromAxisAngle(cameraUp, yawAngle));
6 currentYaw += yawAngle;
7 }
接下来,替换下面的pitch代码在Camera类的Update方法:
2 cameraDirection = Vector3.Transform(cameraDirection,Matrix.CreateFromAxisAngle(Vector3.Cross(cameraUp, cameraDirection),(MathHelper.PiOver4 / 100) *(Mouse.GetState( ).Y - prevMouseState.Y)));
3 cameraUp = Vector3.Transform(cameraUp,Matrix.CreateFromAxisAngle(Vector3.Cross(cameraUp, cameraDirection),(MathHelper.PiOver4 / 100) *(Mouse.GetState( ).Y - prevMouseState.Y)));
用这个:
2 float pitchAngle = (MathHelper.PiOver4 / 150) *(Mouse.GetState( ).Y - prevMouseState.Y);
3 if (Math.Abs(currentPitch + pitchAngle) < totalPitch)
4 {
5 cameraDirection = Vector3.Transform(cameraDirection,Matrix.CreateFromAxisAngle(Vector3.Cross(cameraUp, cameraDirection),pitchAngle));
6 currentPitch += pitchAngle;
7 }
编译并运行您的游戏,你会看到,你可以旋转相机用pitch和yaw,但是这个角度限制在45度在yaw和pitch方向。
在下一章,你会得到相机的设置和添加一些逻辑为这个3D游戏。不是首先,让我们看看下一章会做什么。
What Happened to the Up Vector?
你现在准备去模拟一个固定的相机(不是一个3D飞行相机)。由于这个,你准备使你的相机总是yaw围绕一个固定的up向量-在这种情况,(0,1,0)是up向量你开始时,它不会被改变。如果你去修改up向量用pitch,然后旋转用yaw围绕着修改up向量,你的相机可以旋转稍微偏离中心。
试试看,将以下代码添加Update方法的到pitch旋转部分,只在这行currentPitch+=pitchAngle之前,
源代码:http://shiba.hpe.cn/jiaoyanzu/WULI/soft/xna.aspx?classId=4
(完)