四.几何管道

  当我们设计一个3-D程序时,我们可以使用任何方便的测量单位来定义我们的世界,从微米、毫米,直到光年、秒差距(parsec3.26光年)。程序将描述这个世界的信息传递给Direct3D。这些描述包括:大小、世界中对象的相对位置、以及观察者的位置和方向。Direct3D将这些描绘表现在屏幕上的一个个像素上。这样的过程——将三维世界转换为二位图象的几何变换——就被称为几何管道(geometry pipeline),有时也被称为变换管道(transformation pipeline)。

 

1.  管道综述

  Direct3D中有关几何管道的部分实际上就是一个变换引擎。它对世界中的模型和观察者进行定位,将顶点投影到屏幕上,对顶点进行裁剪。(这个变换引擎也执行光线运算以决定每个顶点的散射和镜面反射属性。详细内容见“灯光和纹理”。)

  几何管道将顶点作为它的输入。变换引擎对顶点使用三种变换(世界、视和投影变换)。对结果进行裁剪,然后传递给光栅。顺序如下图:

pic42.gif (22844 bytes)

  在管道的入口,还没有进行任何变换,所以一个模型的所有顶点都是相对于一个本地坐标系来说明的。这个坐标系的方位一般被称为模型空间,并且每个坐标系又都被称为模型坐标系。

  几何管道的第一个阶段,是将一个模型的顶点从它们的本地坐标系变换到一个由场景中的对象所共用的坐标系中。这个过程被称为世界变换(world transformation)。这个新的方位通常被称为世界空间,并且世界空间中的每一个顶点都使用世界坐标来说明。我们将在“世界变换”中对它进行详细的讨论。

  下一步,顶点按照摄像机重新进行导向。也就是说,程序选择一个观察点,将世界坐标系以摄像机为中心进行重新定位和旋转,从而将世界空间变换到摄像机空间。这就是视变换。详细内容见“视变换”。

  接下来是透视变换。在管道的这一部分,通常将对象按照它相对于观察点的位置进行缩放,使它们具有距离感;使得近处的对象看起来要比远处的大一些。这一变换过程将在“透视变换”部分中进行讨论。为了简单起见,本文将经过透视变换的顶点所在的空间称为透视空间。(一些图形的书中也将透视空间称为“后投影齐次空间(post-perspective homogeneous space)”。)注意:并不是所有的透视变换都要对场景中对象大小进行缩放。这样的透视变换有时被称为仿射变换(affine projection)或正交变换(orthogonal projection

  管道的最后一部分,是对顶点进行裁剪,将不能在屏幕上看到的顶点去掉,这样可以节省光栅处理时间,使它能有更多的时间来处理可见部分的颜色和进行明暗处理。这一过程我们称为裁剪,有关它的详细讨论见“视口和裁剪”。裁剪之后,剩下的顶点按照视口参数进行缩放,并转换到屏幕坐标系中。最后得到的顶点经过光栅处理将存在于屏幕空间(screen space)中。

 

2. 世界变换

下面我们来讨论一下世界变换的基本内容,以及如何设置一个世界变换矩阵。

2.1 什么是世界变换?pic43.gif (2378 bytes)

  世界变换对模型空间(顶点按照模型本身的原点来进行定义)中的坐标进行变换,将它变换到世界空间(顶点按照场景所有对象所共用的一个原点来进行定义)中。实质上,世界变换就是将一个模型放置到世界当中。右图显示了世界坐标系与一个模型的本地坐标系之间的关系。

  世界变换可以包括任何变换:平移、旋转、缩放等等。详细内容见“3-D变换”。

2.2 设置一个世界矩阵

  和其他的变换一样,可以通过一系列的矩阵级联来创建一个世界变换。举一个最简单的例子,当一个模型位于世界坐标系的原点并且它的本地坐标轴的方向与世界坐标系的相同,那么它的世界矩阵就是一个单位矩阵。

  下面的例子使用D3dutil.cppD3dmath.cpp文件中的辅助函数来创建一个世界矩阵,它包括三个旋转和一个平移。

/*
* For the purposes of this example, the following variables
* assumed to be valid and initialized.
*
* The m_vPos variable is a D3DVECTOR that contains the model's
* location in world coordinates.
*
* The m_fPitch, m_fYaw, and m_fRoll variables are D3DVALUEs that
* contain the model's orientation in terms of pitch, yaw, and roll
* angles (in radians).
*/

D3DMATRIX C3DModel::MakeWorldMatrix(void)
{
 D3DMATRIX matWorld, // World matrix being constructed.
      matTemp, // Temp matrix for rotations.
      matRot; // Final rotation matrix (applied to matWorld).

 // Using the right-to-left order of matrix concatenation,
 // apply the translation to the object's world position
 // before applying the rotations.
 D3DUtil_SetTranslateMatrix(matWorld, m_vPos);
 D3DUtil_SetIdentityMatrix(matRot);

 //
 // Now, apply the orientation variables into the
 // world matrix
 //
 if(m_fPitch || m_fYaw || m_fRoll)
 {
  // Produce and combine the rotation matrices.
  D3DUtil_SetRotateXMatrix(matTemp, m_fPitch); // pitch
  D3DMath_MatrixMultiply(matRot,matRot,matTemp);
  D3DUtil_SetRotateYMatrix(matTemp, m_fYaw); // yaw 
  
D3DMath_MatrixMultiply(matRot,matRot,matTemp);
  D3DUtil_SetRotateZMatrix(matTemp, m_fRoll); // roll
  D3DMath_MatrixMultiply(matRot,matRot,matTemp);
  // Apply the rotation matrices to complete the world matrix.
  D3DMath_MatrixMultiply(matWorld, matWorld, matRot);
 }
 return (matWorld);
}

  准备好世界变换矩阵之后,可以调用IDirect3DDevice3::SetTransform方法对它进行设置,同时要将第一个参数声明为D3DTRANSFORMSTATE_WORLD标志。详细内容见“设置变换”。

  性能优化:Direct3D使用世界和视矩阵对它的几个内部数据结构进行配置。我们每次设置一个新的世界或视矩阵时,系统就会计算相关的内部结构。经常设置这些矩阵——成百上千次的,就会使计算量变的很大。我们可以将世界矩阵与视矩阵进行合并,组成一个新的“世界-视(world-view)”矩阵,这样就可以大大的减少计算量。同时,对于世界矩阵和视矩阵的修改、合并,以及对世界矩阵的重新设置,要相应的制作副本。(为了使程序比较清晰,Direct3D很少采用这种优化方法。)

 

3. 视变换

下面我们来讨论一下世界变换的基本内容,以及如何设置一个世界变换矩阵。

 

3.1 什么是视变换?

视变换对世界空间中的观察者进行定位,将顶点变换到摄像机空间中。在摄像机空间中,摄像机(或观察者)位于原点,面向z-轴正方向。视矩阵按照摄像机的位置和方向对世界中的对象进行重新定位。

由许多种创建视矩阵的方法。在所有方法中,摄像机在世界空间中都有一定的逻辑位置与方向,它用来作为创建视矩阵的起点。视矩阵对对象进行平移和旋转,将它们放置到摄像机空间中。一个创建视矩阵的方法是将一个平移矩阵与沿每个轴的旋转矩阵进行合并。如下式所示:

V=T·Rx·Ry·Rz

  公式中,V是要产生的视矩阵,T是一个平移矩阵,RxRz是沿每个轴的旋转矩阵。平移和旋转矩阵基于摄像机在世界空间中的逻辑位置与方向。如果摄像机的逻辑位置是<10,20,100>,那么平移矩阵的目的就是要将对对象沿x-轴移动-10单位,沿y-轴移动-20单位,沿z-轴移动-100单位。公式中的旋转矩阵基于摄像机的方向,也就是摄像机空间的轴相对于世界空间的旋转量。例如,如果摄像机面向下,那么它的z-轴就与世界空间的z-轴偏离90度的角度,如下图所示:

pic44.gif (1855 bytes)

  这个旋转矩阵在每个方向上的旋转角度都相等,但是对于场景中的对象方向相反。这个摄像机的视矩阵包括一个绕x-轴的-90度的旋转。旋转矩阵与平移矩阵合并在一起创建一个新的视矩阵,通过这个矩阵来调整场景中对象的位置和方向,是它们的前表面能朝向摄像机。

  另外一种方法是直接创建一个合并的视矩阵。(D3DUTIL_SetViewMatrix辅助函数使用了这一方法。)这个方法使用摄像机在世界空间中的位置和一个场景中的“观察点(look-at point)”来得到用来描述摄像机空间坐标轴方向向量。观察点减去摄像机的位置,就得到了摄像机的方向向量(n向量)。然后,将n向量和世界空间的y-轴进行叉乘,并进行归一化,得到一个“向右(right)”向量(u向量)。接下来,u向量与n向量进行叉乘,产生一个“向上(up)”向量(v向量)。向量uv和观察方向向量(n)描述了摄像机空间相对于世界空间的方位。Xyz平移因子通过将摄像机位置与uvn向量进行点乘的结果取负值来得到。

  将这些值添入下面的矩阵中,就得到了视矩阵:

pic45.gif (2174 bytes)

  在这个矩阵中,uvn分别是向上、向右和观察方向向量,c是摄像机在世界空间中的位置。这个矩阵中包含了将顶点平移、旋转到摄像机空间中的所需的所有的元素。创建完这个矩阵之后,你可以创建一个能绕z-轴旋转的矩阵,这样就可以是摄像机进行转动了。

有关的详细内容见“设置视矩阵”。

 

3.2 设置一个视矩阵

  D3dutil.cpp文件中的D3DUtil_SetViewMatrix辅助函数可以用来创建一个视矩阵。它包括MagnitudeCrossProductDotProduct D3D_OVERLOADS辅助函数。

 

HRESULT D3DUtil_SetViewMatrix( D3DMATRIX& mat, D3DVECTOR& vFrom,
              D3DVECTOR& vAt, D3DVECTOR& vWorldUp )
{
 // Get the z basis vector, which points straight ahead. This is the
 // difference from the eyepoint to the lookat point.
 D3DVECTOR vView = vAt - vFrom;
 FLOAT fLength = Magnitude( vView );
 if( fLength < 1e-6f )
  return E_INVALIDARG;
 
 
// Normalize the z basis vector
 vView /= fLength;
 // Get the dot product, and calculate the projection of the z basis
 // vector onto the up vector. The projection is the y basis vector.
 FLOAT fDotProduct = DotProduct( vWorldUp, vView );

 D3DVECTOR vUp = vWorldUp - fDotProduct * vView;
 // If this vector has near-zero length because the input specified a
 // bogus up vector, let's try a default up vector
 if( 1e-6f > ( fLength = Magnitude( vUp ) ) )
 {
  vUp = D3DVECTOR( 0.0f, 1.0f, 0.0f ) - vView.y * vView;
  // If we still have near-zero length, resort to a different axis.
  if( 1e-6f > ( fLength = Magnitude( vUp ) ) )
  {
   vUp = D3DVECTOR( 0.0f, 0.0f, 1.0f ) - vView.z * vView;
   if( 1e-6f > ( fLength = Magnitude( vUp ) ) )
    return E_INVALIDARG;
  }
 }
 // Normalize the y basis vector
 vUp /= fLength;
 // The x basis vector is found simply with the cross product of the y
 // and z basis vectors
 D3DVECTOR vRight = CrossProduct( vUp, vView );
 // Start building the matrix. The first three rows contains the basis
 // vectors used to rotate the view to point at the lookat point
 D3DUtil_SetIdentityMatrix( mat );
 mat._11 = vRight.x; mat._12 = vUp.x; mat._13 = vView.x;
 mat._21 = vRight.y; mat._22 = vUp.y; mat._23 = vView.y;
 mat._31 = vRight.z; mat._32 = vUp.z; mat._33 = vView.z;
 // Do the translation values (rotations are still about the eyepoint)
 mat._41 = - DotProduct( vFrom, vRight );
 mat._42 = - DotProduct( vFrom, vUp );
 mat._43 = - DotProduct( vFrom, vView );
 return S_OK;
}

  和世界变换一样,你可以调用IDirect3DDevice3::SetTransform方法来设置视变换,将第一个参数声明为D3DTRANSFORMSTATE_VIEW标志。见“设置变换”部分。

  性能优化:Direct3D使用世界和视矩阵来配置几个系统内部数据结构。我们每次设置一个新的世界或视矩阵时,系统就会计算相关的内部结构。经常设置这些矩阵——成百上千次的,就会使计算量变的很大。我们可以将世界矩阵与视矩阵进行合并,组成一个新的“世界-视(world-view)”矩阵,这样就可以大大的减少计算量。同时,对于世界和视矩阵的修改,它们的合并,以及对世界矩阵的重新设置,要相应的制作副本。(为了使程序比较清晰,Direct3D很少采用这种优化方法。)

 

4. 投影变换

一个投影变换类似于选择一个透镜的焦距,它是三种变换中最复杂的一个。

 

4.1 视锥pic46.gif (8672 bytes)

  视锥就是场景中的一个三维空间,它的位置由视口的摄像机来决定。这个空间的形状决定了摄像机空间中的模型将被如何投影到屏幕上。透视投影是最常用的一种投影类型,使用这种投影,会使近处的对象看起来比远处的大一些。对于透视投影,

  视锥可以被初始化成金字塔形,将摄像机放在顶端。这个金字塔再经过前、后两个剪切面的分割,位于这两个面之间的部分就是视锥。只有位于视锥内的对象才可见。

视锥由凹视野(fov-field of view)和前后剪切面的位置来进行定义:

pic47.gif (3047 bytes)

  在上图中,变量D是从摄像机到空间原点的距离,这个空间是在集合管道的最末端经过视变换得到的空间。要了解变量D如何被用来建立投影矩阵,请看“什么是投影变换?”部分。

 

4.2 什么是投影矩阵?

  投影矩阵是一个典型的缩放和透视矩阵。投影变换将视锥变换成一个直平行六面体的形状。因为视锥的近处比远处小,这样就会对靠近摄像机的对象起到放大的作用,也就将透视应用到了场景当中。

  在视锥中,摄像机与空间原点间的距离被定义为变量D。开始定义透视投影的矩阵时,可以象下面左图这样来使用变量D

pic48.gif (1187 bytes)        pic49.gif (1138 bytes)

  视矩阵将摄像机放置在场景的原点。又因为投影矩阵需要将摄像机放在(0, 0, -D),那么它就要将向量沿z-轴平移-D的距离,如上面右图所示:

将两个矩阵相乘,得到下面的矩阵:

pic50.gif (1178 bytes)

  下图显示了透视变换如何将一个视锥变换成一个新的坐标空间。注意:锥形体变成了直平行六面体,原点从场景的右上角移到了中心。

pic51.gif (4974 bytes)

  在透视变换中,x-y-方向的限制是-11z-方向的限制是前表面为0,后表面为1

  这个矩阵基于一定的距离(这个距离是从摄像机到邻近的剪切面)对对象进行平移和旋转,但是它没有考虑到视野(field-of-view),也没有考虑到对象的z-值可能会相同,从而使深度比较变得困难。下面的矩阵讨论了这一问题,并且调整顶点来说明视口的高宽比例:

pic52.gif (1681 bytes)

  在这个矩阵中,Zn是临近剪切面的z值。变量whQ的意义如下(注意:fovwfovh表示视口的水平和垂直视野,用弧度标示):

pic53.gif (2085 bytes)

  在程序中,使用视野角度来定义xy缩放系数比使用视口的水平和垂直尺寸(在摄像机空间中)并不方便多少。下面两式使用了视口的尺寸,并且与上面的公式相等:

pic54.gif (871 bytes)

  在这些公式中,Zn表示邻近的剪切面的位置,变量VwVh表示视口的高和宽。这两个参数与D3DVIEWPORT2结构中的dwWidthdwHeight成员相关。

  不管你使用那个公式,将Zn值尽量设的大一些是很重要的,因为当z值很接近时,大多数情况下是难以分辨的,由一个取巧的方法,就是在进行深度比较时使用16z-bufferDirect3D中,投影矩阵的第(3,4)元素不能为负数。

  同世界和视变换一样,可以调用IDirect3DDevice3::SetTransform方法来设置透视变换,详细内容见“设置变换”。

 

4.3 设置投影矩阵

  下面的ProjectionMatrix例程函数又四个输入参数,它们用来设置前后剪切面,和视野的水平与垂直角度。视野角度应该比π弧度(180度)小。

D3DMATRIX ProjectionMatrix(const float near_plane,// distance to near clipping plane
             const float far_plane,// distance to far clipping plane
             const float fov_horiz,// horizontal field of view angle, in radians
             const float fov_vert)// vertical field of view angle, in radians
{
 float h, w, Q;
 w = (float)cot(fov_horiz*0.5);
 h = (float)cot(fov_vert*0.5);
 Q = far_plane/(far_plane - near_plane);
 D3DMATRIX ret = ZeroMatrix();
 ret(0, 0) = w;
 ret(1, 1) = h;
 ret(2, 2) = Q;
 ret(3, 2) = -Q*near_plane;
 ret(2, 3) = 1;
 return ret;
} // end of ProjectionMatrix()

  一旦创建完了矩阵,你需要调用IDirect3DDevice3::SetTransform方法来设置它,同时将第一个参数设置为D3DTRANSFORMSTATE_PROJECTION。详细内容见“设置变换”。

 

4.4 一个W-Friendly投影矩阵

  一个顶点经过世界、观察和投影变换之后,Direct3D立即模式可以利用这个顶点的W成分执行雾化效果,并在深度缓冲中执行基于深度的运算。这样的运算需要投影矩阵将W规范化等价于世界空间的Z。简而言之,如果你的投影矩阵的(3,4)系数不是1,那么你就必须用(3,4)系数的倒数对所有的系数进行缩放。如果没有提供一个适当的矩阵,那么雾化效果和深度缓冲就不能得到正确运用。(“什么是投影矩阵?”中提供的矩阵是适合于)基于W的运算的。)

  下图展示了一个不适合的投影矩阵,和一个经过缩放的适合的矩阵:

pic55.gif (4266 bytes)

  在前面的矩阵中,所有的变量都被假定为非零。有关雾化的内容见“目相关对基于Z的深度”。有关基于W的深度缓冲见“什么是深度缓冲?”

  注:Direct3D在基于W的深度运算时使用当前设置的投影矩阵。因此,程序必须设置一个适合的矩阵来的导向要的基于W的特性,即使它们没有使用Direct3D变换管道。

 

上一页 | 目录 | 下一页