OpenGL教程翻译 第十三课 相机空间
原文地址:http://ogldev.atspace.co.uk/(源码请从原文主页下载)
Background
在上几节中我们看见两种类型的顶点变换。第一种类型的变换是改变对象的位置(平移),旋转,尺寸(放缩)。这些变换允许我们在3D世界中把一个对象放在任何位置。第二种类型的变换是透视投影变换,把在3D世界坐标系下的顶点位置投影到2D世界坐标系下(比如一架飞机)。一旦坐标变换为2D坐标,那么非常容易将这些2D坐标映射到屏幕空间坐标。这些坐标实际上被用来光栅化组成对象的图元(可能是点、线或者三角形)。
在之前的所有章节中我们还没接触过相机,我们隐含假定相机位于3D空间的原点。事实上,我们希望能够自由的控制相机,可以把它放在3D世界的任何位置,并投影顶点到在相机正前方的2D平面。这将能够反映出相机和屏幕上的对象之间的正确的关系。
在下面这幅图片中,我们看见相机背对我们置于某处。在相机前面有一个虚拟的2D平面,球被投影到该平面上。相机是倾斜着的,所以平面也是倾斜着的。由于相机视角的限制,此 2D平面能看见的部分只是一个矩形。在该矩形的外部所有东西都将被减掉。把这个矩形渲染到屏幕是我们的目标。
理论上可以生成这样的变化矩阵,实现把一个位于3D空间的对象投影到坐落在世界坐标系任意位置的相机正前方的2D平面上。然而,这其中的数学变换比我们之前遇见的要复杂很多。而当相机被置于3D世界坐标系的源点并且方向沿着Z轴时的投影就简单了很多。比如,一个对象放在(0,0,5)处,同时相机放在(0,0,1)处并且方向朝向Z轴。如果我们把相机和对象朝着原点移动一个单位,那时两者的相对距离和方向(就相机的方向而言)保持不变,而区别只是相机此时位于原点。以同种方式移动场景中的所有物体使得我们能够正确渲染场,用之前学到的方法。
上面的例子很简单,因为相机的方向已经沿着Z轴,并且通常对准坐标系的轴。但是如果相机指向其他方向将会发生什么呢?看下面的图片。简单的说,这是一个2D坐标系统,我们从顶部看相机。
相机的方向原本是指向Z轴的,但是之后顺时针旋转45度。就如你看到的,相机定义它自己的坐标系统,这个坐标系可能与世界坐标系相同(上面的图片),也可能不同(下面的图片)。所以实际上同时存在两个坐标系统。即一个用来指定对象的世界坐标系,和一个与相机的“轴”(target, up and right)对齐的相机坐标系统。这两个坐标系就是我们所知的世界空间和相机/视图空间。
上图中,绿球在世界空间中位于(0,y,z)。在相机空间中,它位于坐标系的左上角(换言之,它的X坐标为负,Z坐标为正)。我们需要找到绿球在相机空间中的位置。此时我们可以先简单的忘记所有关于世界空间的,仅仅只使用相机空间。在相机空间中,相机位于原点方向,指向Z轴。对象的指定和相机有关,并且可以用我们学过的方法来渲染对象。
相机顺时针旋转了45度相当于绿球逆时针旋转了45度。对象的移动总是和相机的移动相反。所以总的来说,我们需要增加两个新的变换,并将它们加入我们已有的变换管线中。我们需要,在让相机移动到原点的过程中保持对象和相机之间相对位置不变,同时也需要让对象沿着与相机旋转方向的反方向转动。
移动相机是非常简单的。如果相机位于(x,y,z),那么平移变换就是(-x,-y,-y)。原因很明显——相机在世界坐标系下用向量(x,y,z)做平移变换,所以想要相机回到原点,那么我们就需要使用此向量的相反向量进行平移。变换矩阵如下所示:
下一步是将相机转向世界坐标系中指定的位置。我们想要找到在相机定义的新坐标系下顶点的位置。所以实际的问题是:我们如何从一个坐标系转换到另一个坐标系?
再看看上面的图。世界坐标系是由三个线性无关的向量(1,0,0), (0,1,0) 和 (0,0,1)定义的。线性无关意思是我们找不到不全为0的x,y,z使得x*(1,0,0) + y(0,1,0) + z*(0,0,1) = (0,0,0)。用几何术语来说,这意味着这三个向量中的任意两个都可以确定一个垂直于第三个向量的平面。很容易可以看到相机坐标系是由向量(1,0,-1), (0,1,0), (1,0,1)确定的。标准化这些向量后,我们得到(0.7071,0,-0.7071),(0,1,0)
和(0.7071,0,0.7071)。
下面的图片展示了向量的位置是如何在两个不同的坐标系被指定的。
我们知道在世界坐标系空间中如何得到代表相机空间坐标轴的单位向量,也知道了在世界坐标系空间中向量的位置(x,y,z)。我们要找的是向量(x‘,y‘,z‘)。我们现在利用点积的一个叫做“标量投影”的属性。标量投影是一个任意的向量A和一个单位向量B点积的结果,产生B向量方向上A向量的延伸。换句话说,这个结果就是向量A在向量B上的投影。在上面的例子中,如果我们让向量(x,y,z)和代表相机X轴的单位向量之间做点积,那么我们将得到x‘值。相同的方法我们可以得到y‘,z‘。(x‘,y‘,z‘)是在相机空间中(x,y,z)的位置。
让我们看看如何把这个想法变成一个完整的标定相机方向的办法。这个办法叫做“UVN相机”,它仅仅是众多指定相机方向的办法中的一个。这个方法就是是,相机被下列的矩阵所定义:
1:N – 由相机指向它的目标的向量。在一些3D的文献中也被称为‘look at‘。这个向量对应于Z轴。
2:V – 直立时,这个向量是竖直向上的。如果你正在编写飞机模拟仿真的程序并且飞机是被反转的,那么向量最好是朝向地面的。这个向量对应于Y轴。
3:U – 这个向量从相机指向其右侧。它对应于X轴。
为了把一个位置从世界坐标系空间转换到被UVN向量定义的相机坐标系空间,我们需要在位置和UVN向量之间进行一个点乘运算。如下面的矩阵所示:
这一节的代码中,你将会注意到着色器中的全局变量‘gWorld‘已经被重命名为‘gWVP‘。这个改变体现了在许多书籍中这一系列变换为我们所知的方式。WVP代表World-View-Projection。
Code Walkthru
在这一节中我决定做一个小的设计改变,把低级的矩阵操作代码从Pipeline类移到Matrix4f类中。Pipeline类让Matrix4f类调用不同的方法初始化它自己并且链接几个矩阵来生成最终的矩阵变换。
(pipeline.h:85)
struct {
Vector3f Pos;
Vector3f Target;
Vector3f Up;
} m_camera;
Pipeline类有一些新的成员来储存相机参数。注意相机参数中缺少‘U‘轴。它将通过在target和up之间进行叉乘计算得到。此外这里有一个新的函数SerCamera来传递这些值。
(math3d.h:21)
Vector3fVector3f::Cross(const Vector3f& v) const
{
const float _x = y * v.z - z * v.y;
const float _y = z * v.x - x * v.z;
const float _z = x * v.y - y * v.x;
return Vector3f(_x, _y, _z);
}
Vector3f类中有一个新的方法来计算两个Vector3f对象的叉积。两个向量的叉积会产生一个向量,它垂直于两个相乘向量组成的平面。当你记住这些向量有方向和幅度但是没有位置时,这个变的更直观。所有拥有相同的方向和大小的向量被认为是相等的,不论它们的起点在哪里。所以你也可以让两个向量的起点位于原点。这意味着你可以创建一个三角形,这个三角形有一个顶点是位于起点而另外两个顶点是向量的终点。这个三角形定义了一个平面,叉积产生了一个垂直于这个平面的向量。在维基百科(Wikipedia)中可以了解到更多关于叉积。
(math3d.h:30)
Vector3f&Vector3f::Normalize()
{
const float Length = sqrtf(x * x + y* y + z * z);
x /= Length;
y /= Length;
z /= Length;
return *this;
}
为了生成UVN矩阵我们需要让这些向量成为单位向量。这个操作术语称作是向量的标准化,它由让向量的每个分量除以向量的长度得来。
(math3d.cpp:84)
voidMatrix4f::InitCameraTransform(const Vector3f& Target, const Vector3f&Up)
{
Vector3f N = Target;
N.Normalize();
Vector3f U = Up;
U.Normalize();
U = U.Cross(Target);
Vector3f V = N.Cross(U);
m[0][0] = U.x; m[0][1] = U.y; m[0][2]= U.z; m[0][3] = 0.0f;
m[1][0] = V.x; m[1][1] = V.y; m[1][2]= V.z; m[1][3] = 0.0f;
m[2][0] = N.x; m[2][1] = N.y; m[2][2]= N.z; m[2][3] = 0.0f;
m[3][0] = 0.0f; m[3][1] = 0.0f;m[3][2] = 0.0f; m[3][3] = 1.0f;
}
这个函数生成了稍后在pipeline类中使用的相机转化矩阵。U,V,N向量计算后置于矩阵的每一行。因为顶点的位置向量将在矩阵的右边被乘(作为一列)。这意味着在U,V,N向量和位置向量之间做点乘。这将生成三个标量值,这三个值是在屏幕坐标系下XYZ的值。
这个函数被提供的参数是target和up向量。“right”向量是通过这两个向量的叉乘得到。因为不确定参数是否为单位向量,所以我们标准化这些向量。在生成U向量后,我们再通过target和right向量之间进行叉乘来计算up向量。在后面我们开始移动相机的时候,将会发现重新计算up向量的原因,只更新target向量而up向量保持不变是较为简单的。然而,这意味着在target和up向量之间的角度将不是90度,导致这个坐标系统变得无效。通过计算right向量和up向量,我们将的得到一个每一对轴之间的角度都是90度的坐标系统。
(pipeline.cpp:22)
constMatrix4f* Pipeline::GetTrans()
{
Matrix4f ScaleTrans, RotateTrans,TranslationTrans, CameraTranslationTrans, CameraRotateTrans,PersProjTrans;
ScaleTrans.InitScaleTransform(m_scale.x,m_scale.y, m_scale.z);
RotateTrans.InitRotateTransform(m_rotateInfo.x,m_rotateInfo.y, m_rotateInfo.z);
TranslationTrans.InitTranslationTransform(m_worldPos.x,m_worldPos.y, m_worldPos.z);
CameraTranslationTrans.InitTranslationTransform(-m_camera.Pos.x,-m_camera.Pos.y, -m_camera.Pos.z);
CameraRotateTrans.InitCameraTransform(m_camera.Target,m_camera.Up);
PersProjTrans.InitPersProjTransform(m_persProj.FOV,m_persProj.Width, m_persProj.Height, m_persProj.zNear, m_persProj.zFar);
m_transformation = PersProjTrans *CameraRotateTrans * CameraTranslationTrans * TranslationTrans *
RotateTrans * ScaleTrans;
return &m_transformation;
}
让我们更新生成一个对象完整的变化矩阵的函数。现在只是新增了两个关于相机的新的矩阵就已经变得相当复杂。在完成了世界坐标系下的变换(结合对象的缩放,旋转,平移)后,我们通过移动相机到原点开始了对相机的变换。这个平移转换通过使用相机位置向量的反向量完成的。所以如果相机在(1,2,3),那么为了让相机回到原点我们需要把相机沿(-1,-2,-3)这个向量移动。之后我们基于相机的target和up向量而生成相机的旋转矩阵。那么相机部分的变换就完成了。最后生成坐标。
(tutorial13.cpp:76)
Vector3fCameraPos(1.0f, 1.0f, -3.0f);
Vector3fCameraTarget(0.45f, 0.0f, 1.0f);
Vector3fCameraUp(0.0f, 1.0f, 0.0f);
p.SetCamera(CameraPos,CameraTarget, CameraUp);
在主渲染循环中我们使用新的方法。为了放置相机,我们从原点沿着Z轴的负方向后移动,然后向右移和向上移动。up向量就是Y轴的正半轴。我们把所有的这些送进Pipeline对象中,Pipeline类将处理接下来的工作。