2.2 指定相机的目标
问题
当定义View矩阵时,你需要指定Target向量作为参数,这个向量设置了相机的旋转。
解决方案
当选转相机时,相机的位置会保持不变。这意味着旋转取决于相机的Target点和Up向量的变化。你可以通过使用 (0,0,-1)Forward向量并将旋转施加到这个向量上获取Target向量,这会产生一个新的Target点。你可以使用相同的方法获取Up向量。
工作原理
如教程2-1所示,一个相机需要Position, Target和Up向量才能唯一确定。如果你想让相机绕着某一点旋转,Position向量保持不变,但Target和Up向量都要发生改变。
给定绕着三根轴的旋转角度,一个方法是处理Target位置,但这会带来复杂的运算,还有一个更加清晰和快速的方法。
最简单的例子:相机在初始位置,绕着Up向量
旋转让我们先来看一下相机在(0,0,0)初始坐标的情况。它朝(0,0,-1) Forward方向观察,以默认的(0,1,0) Up向量作为向上方向。在这个情况中,你可以使用以下代码:
Vector3 cameraPosition = new Vector3(0, 0, 0); Vector3 cameraTarget = new Vector3(0, 0, -1); Vector3 cameraUpVector = new Vector3(0, 1, 0); viewMatrix = Matrix.CreateLookAt(cameraPosition, cameraTarget, cameraUpVector);
例如你想创建一个绕Up向量旋转45度的View矩阵。如果你的头就是相机,这会让头部向右旋转45度。当计算新View矩阵时,Position向量和Up向量会保持不变,但你需要获取新的Target向量。你可以通过将使用45度旋转“转换”默认的(0,0,-1) Target向量获取新Target向量。这意味着你获取的这个向量是初始Target向量的旋转版本。下面是代码:
Matrix cameraRotation = Matrix.CreateRotationY(MathHelper.PiOver4); Vector3 cameraPosition = new Vector3(0, 0, 0); Vector3 cameraUpVector = new Vector3(0, 1, 0); Vector3 cameraOriginalTarget = new Vector3(0, 0, -1); Vector3 cameraRotatedTarget = Vector3.Transform(cameraOriginalTarget, cameraRotation); viewMatrix = Matrix.CreateLookAt(cameraPosition, cameraRotatedTarget, cameraUpVector);
注意:矩阵是表示某种变换的有力工具。一个矩阵可以表示旋转,平移,缩放或这些变换的组合。你可以在教程4-2中获取更多的例子。
第一行代码创建了一个表示绕Up轴旋转45度的矩阵,对应PI/4弧度。下面的代码通过这个旋转矩阵转换初始(0,0,-1) Target向量,并将旋转过的Target向量保存在cameraRotatedTarget变量中,这个变量用来创建一个新的View矩阵。
注意:这个变换并不神秘;它是向量和矩阵的乘积,只是简单地进行16次乘法和12次加法。
第二个例子:相机在初始位置,任意旋转
现在来看一个有点复杂的例子。你想使相机绕着任意轴旋转而不是绕着Up向量。例如,绕着(1,0,0) Right轴旋转45度,如果你的头是相机,这会导致斜向上45度观察。
因为你仅旋转相机,Position向量保持不变。和前面一个例子一样,Target向量会变化,因为相机需要观察一个不同的位置。
但是这种情况中Up向量也会发生变化。在前面的例子中你将头部转向右边,Up向量并不会变化。这个例子中,将头部向上旋转,Up向量和Target向量都要发生改变。
变换Up向量和变换Forward向量的方法是一样的:你将旋转存储在一个矩阵中并定义初始Up向量。然后,通过这个旋转矩阵变换初始向量获取新的Up向量。下面是代码:
Matrix cameraRotation = Matrix.CreateRotationX(MathHelper.PiOver4); Vector3 cameraPosition = new Vector3(0, 0, 0); Vector3 cameraOriginalUpVector = new Vector3(0, 1, 0); Vector3 cameraOriginalTarget = new Vector3(0, 0, -1); Vector3 cameraRotatedTarget = Vector3.Transform(cameraOriginalTarget, cameraRotation); Vector3 cameraRotatedUpVector = Vector3.Transform(cameraOriginalUpVector, cameraRotation); viewMatrix = Matrix.CreateLookAt(cameraPosition, cameraRotatedTarget, cameraRotatedUpVector);
这里的“任意”旋转矩阵只是一个简单绕x轴的旋转。这个代码也可以处理任何旋转,例如下面的这个情况,它组合了三根轴上的旋转。下面的代码生成一个矩阵,这个矩阵是绕z轴–45度和y轴22.5度,x轴90度旋转的组合:
Matrix cameraRotation = Matrix.CreateRotationX(MathHelper.PiOver2)* Matrix.CreateRotationY(MathHelper.Pi/8.0f)* Matrix.CreateRotationZ(-MathHelper.PiOver4);
当获取新的Forward向量和Up向量时,相机的位置保持不变。
第三个例子:相机在指定位置,任意旋转
在大多数情况中,你想设置任意旋转并指定相机的位置。例如相机在位置(10,20,30),不旋转。非常简单,相机的Position向量为(10,20,30)。因为没有旋转,所以相机观察(0,0,-1) Forward方向。
注意:记住需要指定Target而不是Target方向!将(0,0,-1)作为Target向量是错误的,因为这会让相机观察点(0,0,-1)。例如你将相机移动到点(-10,20,30),如果仍然指定(0,0,-1)作为Target向量,相机仍会观察点(0,0,-1),这个相机观察的方向就会发生变化!
要让位于(10,20,30)相机朝向(0,0,-1)方向,你需要指定(10,20,29)作为Target向量。你可以通过求相机位置和目标方向的和获取这个向量:
Vector3 cameraPosition = new Vector3(10, 20, 30); Vector3 cameraOriginalTarget = new Vector3(0, 0, -1); cameraTargetPoint = cameraPosition + cameraOriginalTarget;
现在,可以组合前面所学到的东西了。你将定义一个位于(10,20,30)的相机并可以任意旋转。Position向量保持(10,20,30)不变。对Target向量,首先定义为(0,0,-1)方向。要在旋转后获取Forward方向,你需要使用旋转矩阵变换它。最后,要获取Target向量让位于(10,20,30)的相机看向旋转方向,你需要在这个旋转方向上加上(10,20,30)。Up向量用同样的方法获取,下面是最后的代码:
Matrix cameraRotation =Matrix.CreateRotationX(MathHelper.PiOver2)* Matrix.CreateRotationY(MathHelper.Pi/8.0f)*Matrix.CreateRotationZ(-MathHelper.PiOver4); Vector3 cameraPosition = new Vector3(10, 20, 30); Vector3 cameraOriginalTarget = new Vector3(0, 0, -1); Vector3 cameraOriginalUpVector = new Vector3(0, 1, 0); Vector3 cameraRotatedTarget = Vector3.Transform(cameraOriginalTarget, cameraRotation); Vector3 cameraFinalTarget = cameraPosition + cameraRotatedTarget; Vector3 cameraRotatedUpVector = Vector3.Transform(cameraOriginalUpVector, cameraRotation); Vector3 cameraFinalUpVector = cameraPosition + cameraRotatedUpVector; viewMatrix = Matrix.CreateLookAt(cameraPosition, cameraFinalTarget, cameraFinalUpVector);
将旋转过的相机前后/左右移动
现在已经实现了指定位置的相机并朝向正确的方向,新的挑战是前后移动相机。如果你想让相机向前移动,简单地将(0,0,-1) Forward向量添加到Position向量上不会成功,因为首先需要获取对应旋转相机的Forward向量。你已经在本教程中的第一个例子中使用过旋转矩阵变换过(0,0,-1) Forward向量了,有了这个变换过的Forward向量,才可以将它加到Position向量中:
float moveSpeed = 0.5f; Vector3 cameraOriginalForward = new Vector3(0,0,-1); Vector3 cameraRotatedForward = Vector3.Transform(cameraOriginalForward, cameraRotation); cameraPosition += moveSpeed * cameraRotatedForward;
改变moveSpeed的值可以增加/减少相机移动的速度,因为这个值会乘以旋转过的 Forward方向。
同样的方法也可以让相机左右移动。处理的是(1,0,0) Right向量而不是(0,0,-1) Forward向量,仍然需要首先进行变换以获取对应当前相机旋转的Right向量。
float moveSpeed = 0.5f; Vector3 cameraOriginalRight = new Vector3(1, 0, 0); Vector3 cameraRotatedRight = Vector3.Transform(cameraOriginalRight, cameraRotation); cameraPosition += moveSpeed * cameraRotatedRight;
代码
这个方法中相机只需保存当前位置和旋转,位置和旋转任意一个发生变化就要更新View 矩阵。这些变化通常来自于用户输入,可以在教程2-3和2-4中看到具体实现。每个矩阵都需要进行初始化,你首先需将cameraRotation矩阵设置为单位矩阵。
protected override void Initialize() { float viewAngle = MathHelper.PiOver4; float aspectRatio = graphics.GraphicsDevice.Viewport.AspectRatio; float nearPlane = 0.5f; float farPlane = 100.0f; projectionMatrix = Matrix.CreatePerspectiveFieldOfView(viewAngle, aspectRatio, nearPlane, farPlane); cameraPosition = new Vector3(-5, 7, 14); cameraRotation = Matrix.CreateRotationX(-MathHelper.Pi/8.0f)* Matrix.CreateRotationY(-MathHelper.Pi/8.0f); UpdateViewMatrix(); base.Initialize(); } protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); MoveCameraForward(); base.Update(gameTime); } private void MoveCameraForward() { float moveSpeed = 0.05f; Vector3 cameraOriginalForward = new Vector3(0, 0, -1); Vector3 cameraRotatedForward = Vector3.Transform(cameraOriginalForward, cameraRotation); cameraPosition += moveSpeed * cameraRotatedForward; UpdateViewMatrix(); } private void UpdateViewMatrix() { Vector3 cameraOriginalTarget = new Vector3(0, 0, -1); Vector3 cameraOriginalUpVector = new Vector3(0, 1, 0); Vector3 cameraRotatedTarget = Vector3.Transform(cameraOriginalTarget, cameraRotation); Vector3 cameraFinalTarget = cameraPosition + cameraRotatedTarget; Vector3 cameraRotatedUpVector = Vector3.Transform(cameraOriginalUpVector, cameraRotation); viewMatrix = Matrix.CreateLookAt(cameraPosition, cameraFinalTarget, cameraRotatedUpVector); } protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); //render coordcross using specified View and Projection matrices cCross.Draw(viewMatrix, projectionMatrix); base.Draw(gameTime); }
扩展阅读
矩阵乘法的顺序
可参见教程4-2。
改变坐标系统
前面使用的Forward (0,0,-1)和Up (0,1,0)方法是“官方的” XNA向量,也可以使用Vector3 . Forward和Vector3 . Up快捷方式获取。但是这只是一个约定,你可以定义一个完全不同的坐标系统。例如也可以使用(0,0,1)作为Up方向,(0,1,0)作为Forward方向,(1,0,0)作为Right方向,这完全取决于你的需要。
但是这三个向量应该符合一个规则。在XNA中x,y和z轴是右手坐标系。意思是一旦你知道了任意两个坐标轴,就可以知道第三个坐标轴的方向。展开你右手的大拇指和食指,然后弯曲中指使它垂直于食指和大拇指,将这三个手指看成坐标轴,如果是右手坐标系,那么x轴就对应大拇指,y轴对应食指,z轴对应中指。
本教程第一段的坐标系统,对应大拇指指向右方,中指指向上方 。要代表“官方”XNA坐标系统,将你的大拇指指向右方(正x轴 = 右方),食指指向上方 (y轴=上方)。现在你可以看到中指指向后方(正z轴 =后方), 这就是为什么官方Forward (0,0,-1)向量中有个负号的原因!