3D数学是一门和计算几何相关的学科,广泛应用于使用计算机来模拟3D世界的领域,比如图形学、仿真、游戏等等。本文主要介绍了游戏开发中常用的3D数学基础知识以及D3DX中提供的相关接口。
    本文部分图片来自于 http://learnopengl.com 
   3D坐标系 
    3D坐标系分为左手坐标系和右手坐标系,如下图所示,两个坐标系之间无法通过旋转进行转换。其中OpenGL使用的是右手坐标系,而Direct 3D使用的是左手坐标系, 本文使用左手坐标系进行介绍。 
     
 
   相机模型 
   为了将一个3D空间中的物体在2D屏幕上显示,需要进行一系列转换,在转换过程中会用到几个矩阵,分别为模型矩阵(Model)、视图矩阵(View)、投影矩阵(Projection),下图展示了整个转换流程。
     
 
       简化后的流程如下图所示。
     
           - 首先我们使用局部空间定义模型坐标,这样在构建模型时就无需考虑位置、大小等问题。
-  [模型变换] 通过模型矩阵(Model),我们将模型从局部空间放到世界空间,模型变换包括平移、缩放、旋转。 
-  [视图变换] 我们观察3D世界的位置可以虚拟成一个摄像机,该摄像机可以在3D世界的任一位置,为了简化运算,我们将摄像机变换到世界空间中的原点,并朝向z轴正方向。为了保证摄像机的视场恒定,需要通过视图矩阵(View)将模型坐标从世界空间转换到观察空间中。 
-  [投影变换] 将模型放到观察坐标系后,会通过投影矩阵(Projection)进行投影变换,将3D场景投影到2D平面上。实现投影的方式包括正交投影和透视投影,在后面会进行介绍。 投影变换后只有坐标在-1到1之间的点在摄像机的可见范围内。 
-  [视口变换] 可视范围内的模型会通过视口变换显示在窗口中。 
如果对以上过程难以理解的话,可以类比我们使用摄像机拍摄一个物体的过程:
       - 将物体放置到某个地方(模型变换)
- 将摄像机放到某个位置,并对准某个方向,这时以摄像机为世界中心(视图变换)
- 设置相机焦距,调整缩放比例(投影变换)
- 拍摄照片到胶卷中(视口变换)
齐次坐标 
   在数学中,一个N维向量与N*N的矩阵相乘,可以得到一个新的N维向量。
    空间中的坐标是三维的笛卡尔坐标,将三维的笛卡尔坐标转换成四维的 齐次坐标 后,就可以使用矩阵乘法完成坐标的旋转、平移、缩放和投影等操作。 
       为什么要使用四维的齐次坐标?
         - 三维矩阵乘法无法实现平移操作
- 可用于透视投影,第四个分量越大,说明该点越远。(x,y,z,w)最后投影到平面上的点为(x/w,y/w,z/w)
    以上这两点在后面的内容都会有所说明。在D3DX中,可以通过D3DXMATRIX操作齐次矩阵。对于四维向量,如果表示一个点,则第四维为1,如果表示一个向量,则第四维为0。
   模型变换 
   模型变换包括平移、缩放及旋转三类变换。
   平移矩阵 
   平移并不是一种线性变换(平移一个向量是无效的操作),所以只使用三维是无法构造平移矩阵的。举例来说,原点(0,0,0)乘以任何矩阵得到的结果都只会是(0,0,0)。
       将(x,y,z)平移到(x+Δx,y+Δy,z+Δz),构造的矩阵如下。
                   [           x                  y                  z                  1              ]            ⎣        ⎢        ⎢        ⎡                     1        0        0        Δ x                  0        1        0        Δ y                  0        0        1        Δ z                  0        0        0        1                     ⎦        ⎥        ⎥        ⎤             =   [           x + Δ x                  y + Δ y                  z + Δ z                  1              ]      
       在D3DX中,使用D3DXMatrixTranslation创建一个平移矩阵。 
            D3DXMATRIX *D3DXMatrixTranslation(
    D3DXMATRIX *pOut,
    FLOAT x, FLOAT y, FLOAT z
);
            缩放矩阵 
       让(x,y,z)沿着x,y,z轴分别放大qx、qy、qz倍,构造的矩阵如下。
                   [           x                  y                  z                  1              ]            ⎣        ⎢        ⎢        ⎡                      q        x                   0        0        0                  0         q        y                   0        0                  0        0         q        z                   0                  0        0        0        1                     ⎦        ⎥        ⎥        ⎤             =   [            q        x            x                   q        x            y                   q        x            z                  1              ]      
       在D3DX中,使用D3DXMatrixScaling创建一个缩放矩阵。 
            D3DXMATRIX *D3DXMatrixScaling(
    D3DXMATRIX *pOut,
    FLOAT sx, FLOAT sy, FLOAT sz
);
            旋转矩阵 
   在3D空间中旋转需要一个角度和一个轴,而所有的旋转都由绕X,Y,Z轴分别旋转组合而成。
       以绕X轴旋转θ为例,构造的旋转矩阵如下。
                   [           x                  y                  z                  1              ]            ⎣        ⎢        ⎢        ⎡                     1        0        0        0                  0        cos θ        − sin θ        0                  0        sin θ        cos θ        0                  0        0        0        1                     ⎦        ⎥        ⎥        ⎤             =   [           x                  y cos θ − z sin θ                  y sin θ + z cos θ                  1              ]      
    在D3DX中,使用D3DXMatrixRotationX创建一个绕X轴旋转的矩阵。
                D3DXMATRIX *D3DXMatrixRotationX(
    D3DXMATRIX *pOut,
    FLOAT Angle
);
                类似的,还有D3DXMatrixRotationY,D3DXMatrixRotationZ两个函数。
    此外,D3DX还提供了一个绕任意轴旋转的函数D3DXMatrixRotationAxis,该函数中需要指定旋转轴。
                D3DXMATRIX* D3DXMatrixRotationAxis(
    D3DXMATRIX  *pOut,
    const D3DXVECTOR3 *pV,
    FLOAT       Angle
);
             多个旋转在组合的时候,会产生一个严重的问题—— 万向节死锁 ,使得坐标无法正常旋转,这里不讨论细节,可以自行Google。解决此问题的最好方案是使用 四元数 。 
   组合 
   在实际工程中,我们将一个模型坐标从局部空间转换到世界空间,需要进行选择、平移、缩放等多步操作。由于矩阵乘法是满足结合律的,我们可以将这几种操作的矩阵先乘起来,形成所谓的模型矩阵,最后再将局部空间坐标乘以模型矩阵,即可得到其在世界空间中的坐标。
   这种组合后一次性变换对性能的提高也是颇有意义的。对于一个庞大的向量集合,我们不再需要对每个向量都依次进行平移、旋转、缩放操作,而是乘一个模型矩阵就搞定了。
   此外,这几个操作的顺序问题也是必须要注意的。先旋转(缩放)再平移、还是先平移再旋转(缩放),得到的结果是完全不一样的,需要根据实际需求谨慎选择顺序。
   D3DX提供了如下两个函数分别用于点和向量的变换。其中D3DXVec3TransformCoord用于点变换,并假定第四个分量为1,D3DXVec3TransformNormal用于向量变换,并假定第四个分量为0。 
            D3DXVECTOR3 *D3DXVec3TransformCoord(
    D3DXVECTOR3* pOut;
    CONST D3DXVECTOR3* pV,  // pointer to transform
    CONST D3DXMATRIX* pM    
);
D3DXVECTOR3 *D3DXVec3TransformNormal(
    D3DXVECTOR3* pOut;
    CONST D3DXVECTOR3* pV,  // vector to transform
    CONST D3DXMATRIX* pM    
);
            D3DX可以通过IDirectDevice9::SetTransform方法来设定当前渲染用到的模型矩阵。 
            SetTransform(D3DTS_WORLD, pM);
            视图变换 
   视图变换就是将世界空间变换成以摄像机为中心的视图空间。
   我们使用postion,up,right,look四个变量来定义摄像机,其中postion是摄像机的位置,其他三个分别表示摄像机的上向量、右向量与观察向量。
   设我们最终得到的视图变换矩阵为V,那我们希望通过这个矩阵将postion变换到原点,right变换为x轴,up变换为y轴,look变换为z轴,即该变换需满足以下条件
                        ⎩        ⎪        ⎪        ⎨        ⎪        ⎪        ⎧                     p = (  p        x            ,  p        y            ,  p        z            )        r = (  r        x            ,  r        y            ,  r        z            )        u = (  u        x            ,  u        y            ,  u        z            )        d = (  d        x            ,  d        y            ,  d        z            )                  p V = ( 0 , 0 , 0 )        r V = ( 1 , 0 , 0 )        u V = ( 0 , 1 , 0 )        d V = ( 0 , 0 , 1 )                 
       据此可以推出矩阵V
                 V =          ⎣        ⎢        ⎢        ⎡                      r        x                    r        y                    r        z                   − p r                   u        x                    u        y                    u        z                   − p u                   d        x                    d        y                    d        z                   − p z                  0        0        0        1                     ⎦        ⎥        ⎥        ⎤                
       D3DX中可以使用D3DXMatrixLookAtLH方法获取视图矩阵 
            D3DXMATRIX *D3DXMatrixLookAtLH(
    D3DXMATRIX *pOut,
    CONST D3DXMATRIX *pEye, CONST D3DXMATRIX *pAt, CONST D3DXMATRIX *pUp
);
            获得视图矩阵后,通过IDirectDevice9::SetTransform方法来设定当前渲染用到的视图矩阵。 
            SetTransform(D3DTS_VIEW, pM);
            投影变换 
   投影变换是将三维空间投影到二维平面上的过程。投影变换分为透视投影和正交投影,透视投影类似于人眼的观察视角,会产生近大远小的视觉效果,而正交投影不会,也因此主要用于一些工程建模软件中。
   D3DX以近平面作为投影平面,投影过后,会将投影平面内的x坐标转化到范围[-w,w],y坐标范围[-w,w],z坐标范围[0,w],投影完成后,实际坐标(x’,y’,z’)=(x/w, y/w, z/w),D3DX会裁剪掉范围外的投影。
       同样的模型在透视投影和正交投影下的渲染效果。
     
       获得视图矩阵后,通过IDirectDevice9::SetTransform方法来设定当前渲染用到的投影矩阵。 
            SetTransform(D3DTS_PROJECTION, pM);
            透视投影 
   透视投影实际上是一个视锥体,根据视角角度、近平面以及远平面可以构造一个四棱台,只有这个四棱台范围内的模型才是可见的。
     
 
       假设窗口的高度是height,宽度是width,近平面到摄像机的距离为n,远平面到摄像机的距离为f,可以得到透视投影变换矩阵如下,具体的推导过程可以自行google。
                          ⎣        ⎢        ⎢        ⎢        ⎢        ⎢        ⎢        ⎢        ⎢        ⎡                              w i d t h / 2               n                    0        0        0                  0                 h e i g h t / 2               n                    0        0                  0        0                 f − n               f                             n − f               n f                              0        0        1        0                     ⎦        ⎥        ⎥        ⎥        ⎥        ⎥        ⎥        ⎥        ⎥        ⎤                
       D3DX中可以使用以下两个方法获取透视投影变换矩阵。其中第二个函数中fovy代表纵向的视角角度,aspect代表宽高比,稍微修改上面的变换矩阵就可以得到等价的变换矩阵(根据cot(fovy/2)=2n/height,aspect=width/height进行推导)。
            D3DXMATRIX* D3DXMatrixPerspectiveLH(
  D3DXMATRIX *pOut,
  FLOAT w, FLOAT h, FLOAT zn, FLOAT zf
)
D3DXMATRIX* D3DXMatrixPerspectiveFovLH(
  D3DXMATRIX *pOut,
  FLOAT fovy, FLOAT Aspect, FLOAT zn, FLOAT zf
)
            正交投影 
   正交投影相对透视投影要简单一些,其可视范围是一个长方体。
     
 
       正交投影变换矩阵的推导也比较简单,就是一个缩放然后平移的过程。
                          ⎣        ⎢        ⎢        ⎢        ⎢        ⎢        ⎢        ⎢        ⎢        ⎡                              w i d t h               2                    0        0        0                  0                 h e i g h t               2                    0        0                  0        0                 f − n               1                             n − f               n                              0        0        0        1                     ⎦        ⎥        ⎥        ⎥        ⎥        ⎥        ⎥        ⎥        ⎥        ⎤                
       D3DX中可以使用以下方法获取正交投影变换矩阵。 
            D3DXMATRIX* D3DXMatrixOrthoRH(
  D3DXMATRIX *pOut,
  FLOAT w, FLOAT h, FLOAT zn, FLOAT zf
);
            视口变换 
   投影变换后的坐标满足x范围[-1,1],y范围[-1,1],z范围[0,1],w=1,视口变换是将窗口从投影窗口转换到屏幕的一个矩形区域中。其中左上角(-1, 1, 0, 1)映射到(X, Y, MinZ, 1),右上角(1, -1, 0, 1)映射到(X+Width, Y+Height, MinZ, 1),
       视口变换矩阵如下,其中涉及到的参数含义如下,(x,y)表示矩形区域左上角的坐标,width和height分别表示宽和高,[MinZ,MaxZ]指定深度缓存的范围,一般设置为[0,1]即可。
                          ⎣        ⎢        ⎢        ⎢        ⎢        ⎢        ⎢        ⎡                              2               w i d t h                    0        0        x +          2               w i d t h                              0        −          2               h e i g h t                    0        y +          2               w i d t h                              0        0        M a x Z − M i n Z        M i n Z                  0        0        0        1                     ⎦        ⎥        ⎥        ⎥        ⎥        ⎥        ⎥        ⎤                
    D3DX使用IDirectDevice9::SetViewport来设置视口,设置后D3DX将自动完成视口变换。
                typedf struct _D3DVIEWPORT9 {
    DWORD x, DWORD y, DWORD width, DWORD height, DWORD MinZ, DWORD MaxZ
} D3DVIEWPORT9;
D3DVIEWPORT9 vp = { 0, 0, 800, 200, 0, 1 };
m_pDevice->SetViewport(&vp);
            总结 
   以上介绍了在D3DX中用到了一些3D数学基础知识,通过这些知识,可以了解3D模型渲染到屏幕上的整个过程。实际图形程序的开发中,还会涉及到很多其他的数学知识,比如几何碰撞检测、可见性检测、光照渲染等等,在需要使用时可以进一步学习。
                                                http://vimersu.win/blog/2016/07/04/dx-lesson02-3dknowledge/