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)向量中有个负号的原因!