制作3D游戏所需的数学基础 - 平面

来源:互联网 发布:空气比热容比测定数据 编辑:程序博客网 时间:2024/04/28 12:05
今天我们所要讨论的是平面。对于3D游戏制作来说,平面就像是直线在2D游戏制作中的地位一样,它可以帮你完成各种各样的任务。平面被定义为一个无限大,无限薄的空间薄片,就像一张大纸。组成模型的各种三角形存在于各自的平面中。当你有了可以用来表示一个3D空间薄片的平面的时候,你就可以执行各种运算,就像点、多边形的分类和剪裁。

    因此,你怎样来表现平面呢?最好的方式就是从定义3D中平面的等式中构建一个结构。这个等式为:ax + by + cz + d = 0
那么这些数值代表什么呢?三元组<a,b,c>表示平面的法线。从概念上讲,法线是一个直接穿出平面的单位矢量。完整的数学定义是,平面上的法线是一个跟该平面上所有的点都垂直的矢量。

    等式中的d表示从平面到原点的垂直距离。这段距离的长度就是从原点出发,向平面作垂直直线直到跟平面相交,所得到的线段的长度。最后的三元组<x,y,z>表示满足等式的任何点。所有满足等式的点<x,y,z>的集合其实就是位于平面上的所有点。

    我给大家看的所有的图片都是经过严密组织的(呵呵,有点吹牛,ogdev上的朋友们不要笑话我),由于我们必须在2D画面中表示3D平面,因此图片中的3D平面都将被画成一条直线放到边缘,这可以使图形的绘制变得简单。如果有其他更容易的方式可以在2D画面中表现3D图片的无限性的话,请大家告诉我,呵呵,好让我也学习一种新的方法。

    下面是平面的2个例子。第一个平面的法线是背离原点的,因此d是负数(如果你没弄明白的话,可以自己尝试一些取样值,看得到的d是不是负数)。第二个平面的法线是朝向原点的,因此d为正数。大家可能已经猜出来了,如果我们的平面穿过原点,那么将会怎样?对了,d的值为0。图5.13和图5.14显示了这些关系:
A3DwithDX9_5_13.gifwidth: 350 pxsize: -1 bytesdouble click to view all
图5.13 法线背离原点,d为负数

A3DwithDX9_5_14.gifwidth: 350 pxsize: -1 bytesdouble click to view all
图5.14 法线朝向原点,d为正数

    大家应该注意到一个重点,那就是从技术上说,要使平面等式ax + by + cz + d = 0有效,法线<a,b,c>不一定是单位长度的。由于如果法线是单位长度的话,事情处理起来就好办一些,因此本书上的法线都是单位长度的。

    基本的plane3结构定义如下:
表5.14 The plane3 structure
=======================================================

    struct plane3 {
    
        point3 n; // Normal of the plane
        float d; // Distance along the normal to the origin
    
        plane3( float nX, float nY, float nZ, float D) :
            n( nX, nY, nZ ), d( D )
        {
           // All done.
    
        }
        plane3( const point3& N, float D) :
            n( N ), d ( D )
        {
            // All done.
        }
    
        // Construct a plane from three 3D points
        plane3( const point3& a, const point3& b, const point3& c);
    
        // Construct a plane from a normal direction and
        // a point on the plane
        plane3( const point3& norm, const point3& loc);
    
        // Construct a plane from a polygon
        plane3( const polygon<point3>& poly );
    
        plane3()
        {
           // Do nothing
        }
    
        // Flip the orientation of the plane
        void Flip();
    };
=======================================================

    用平面上给出的3个点构造一个平面是一件很简单的任务。你只需要使用由三个点组成的两个矢量(<point2 &#8722; point0> and <point1 &#8722; point0>)进行交叉乘积就可以找出平面的法线。产生法线并把它设置为单位长度后,你就可以通过把法线和3个点中任意一个点进行点乘积,并取该结果的相反数,从而找出d。表5.15是使用3个点构造一个平面的代码:
表5.15 用平面上的3个点构造一个平面
=========================================
    inline plane3::plane3(
       const point3& a,
       const point3& b,
       const point3& c )
    {
       n = (b-a)^(c-a);
       n.Normalize();
       d = -(n*a);
    }
=========================================

    如果你已经有了平面的法线和一个点,那么第一步就可以省略了。见表5.16
表5.16 用平面上的一个点和法线构造一个平面
==========================================
inline plane3::plane3( const point3& norm, const point3& loc) :
        n( norm ), d( -(norm*loc) )
    {
          // all done
    }
==========================================

    最后,使用一个给出的带Point3元素的多边形构造一个平面。这个过程其实是从多边形中先取得3个点,然后使用上面给出的构造器而已。见表5.17:
表5.17 使用给出的polygon<point3>结构构造一个平面
==========================================
   inline plane3::plane3( const polygon<point3>& poly )
    {
        point3 a = poly.pList[0];
        point3 b = poly.pList[1];
        point3 c = poly.pList[2];
    
        n = (b-a)^(c-a);
        n.Normalize();
        d = -(n*a);
    }
==========================================

    这里有一点很重要。那就是你的n边多边形上的点是否共面。如果有一些点跟其它点不共面的话,那么就会产生问题。这就是使用三角形来表现几何的一个优点—三点确定一个平面。


定义跟平面的位置关系

    在平面上,还有一个很重要的操作就是定义跟平面相关的点的位置。如果你要把点代入到平面的等式,那么就可能分为三类:第一类是在平面的前面;第二类是在平面的后面;还有一类是跟平面共面。平面的前面就是该平面的法线出来的一面。
    在这里,精度再一次被要求。由于不再是理论上的操作,因此平面不可能是无限薄,我设定每一个平面都具有epsilon(该值你可以任意的想象)的厚度。
    那么你如何确定跟平面相关的点的方向呢?其实很简单,你只要将该点的x,y,z的值带入到平面的等式,然后根据得到的结果判断点的方向:如果该数值是0(或者通过加上、减去epsilon后趋近于0),就说明该点满足平面的等式,那么就是位于平面上,称为共面的点;如果该数值比0大,那么就是表示你从原点出发沿着平面法线的方向到达该点的路径要比到达平面的路径远,说明该点在平面的前面;如果该数值小于0,那么该点就在平面的后面。注意,等式的前三项可以简单的看为输入的矢量和平面的法线的点乘积。图5.15是该操作的可视化表示,而表5.18则是该操作的代码:


A3DwithDX9_5_15.gifwidth: 350 pxsize: -1 bytesdouble click to view all
图5.15 跟平面相关的点的分类

表5.18 :plane3::TestPoint
===========================================
    // Defines the three possible locations of a point in
    // relation to a plane
    enum ePointLoc
    {
        ptFront,
        ptBack,
        ptCoplanar
    };
    // we're inlining this because we do it constantly
    inline ePointLoc plane3::TestPoint( point3 const &point ) const
    {
         float dp = (point * n) + d;
         if(dp > EPSILON)
         {
             return ptFront;
         }
         if(dp < -EPSILON )
         {
             return ptBack;
         }
         return ptCoplanar; // it was between EP and -EP
}
===========================================

    一旦你有了对点进行分类的代码,那么对其他类型(比如多边形)的分类也变得微不足道了,请看表5.19的代码。不过这里可能存在四种结果,第一种是多边形在平面的前面;第二种是多边形在平面的后面;第三种是多边形跟平面完全重合;还有一种是多边形的一部分在平面的前面,另一部分则在平面的后面,我们称这种情况为多边形切割平面;当然这里的切割只是一个术语,实际上元素不可能切割任何东西。
表5.19 多边形分类代码
================
    // Defines the four possible locations of a point list in
    // relation to a plane. A point list is a more general
    // example of a polygon.
    enum ePListLoc
    {
        plistFront,
        plistBack,
        plistSplit,
        plistCoplanar
    };
    
    ePListLoc plane3::TestPList( point3 *list, int num ) const
    {
        bool allfront=true, allback=true;
    
        ePointLoc res;
    
        for( int i=0; i<num; i++ )
        {
             res = TestPoint( list[i] );
    
             if( res == ptBack )
             {
                allfront = false;
             }
             else if( res == ptFront )
             {
                allback = false;
             }
        }
        if( allfront && !allback )
        {
            // All the points were either in front or coplanar
            return plistFront;
        }
        else if( !allfront && allback )
        {
             // All the points were either in back or coplanar
             return plistBack;
        }
        else if( !allfront && !allback )
        {
             // Some were in front, some were in back
             return plistSplit;
        }
        // All were coplanar
        return plistCoplanar;
    }
===============


Back-face Culling(后面-正面选择?)

    既然你已经知道了如何定义点跟平面的关系,你现在可以进行Back-face Culling了,这是3D图形最基本的优化技术之一。
    假设你有一个三角形,它的元素排列顺序是采用当前比较流行的方式,即从三角形的前面看三角形,元素是按照顺时针方向排列。Back-face Culling允许你可以在这种三角形定义方式下,使用平面等式剔除那些背离你(看不到)的三角形。从概念上说,任何一个封闭的网格,例如立方体,都会存在一种情况,即有一些三角形面对你,而有一些三角形则背离你。你必须知道,那些背离你的多边形你是永远都看不到的,他们往往被面对你的那些三角形挡住了。当然,如果你被允许可以查看立方体的内部,这个说法就不成立了。但是,如果你确实想优化你的引擎,那么这一点绝不允许发生。
    你可以使用平面等式先判断一下那些三角形中哪些是面对照相机(camera),哪些是背离照相机,然后把那些背离照相机的三角形抛弃掉,而不需要在屏幕上绘制所有的三角形。那么是如何来完成这个工作的呢?给出三角形的三个点,你就可以定义出一个这个三角形所在的平面。由于你已经知道这个三角形元素的排列是按照顺时针方向的,你也知道如果我们把元素按照顺序代入到平面构造器中,我们就可以得到平面的法线,它将位于三角形的前方。这时候,如果你把照相机所在的位置看成一个点的话,那么你所需要做的所有工作就是进行点-平面测试。如果照相机的点在平面的前面,那么这个三角形就是可见的,因此它应该被绘制。 

 这里有一个最佳的判定方案。因为你已经知道了位于平面中的三个点(三角形的三个点),所以你只需要知道平面的法线就行了,而不需要整个平面等式。要进行back-face cull,只需要把照相机的位置减去三角形的一个点,并把得到的这个结果矢量同平面的法线进行点乘积。如果得到的结果大于0,那么就说明视点(照相机的位置)在三角形的前面。图5.16可以帮助解释这一点。

A3DwithDX9_5_16.gifwidth: 350 pxsize: -1 bytesdouble click to view all
图5.16 back-face dulling的一个可视化例子

    实际上,3D加速器本身就可以执行back-face culling,因此手工进行back-face dulling已经越来越少了。不过,这些信息对那些不打算使用Direct3D工具的自行开发的3D引擎来说,是很有用的。


Clipping Lines(剪裁直线?)

    你现在需要做的一件事情就是获得两个点(a 和 b),这两个点必须分居在平面的两边,形成一条线段,这样你就可以找到线段和平面的相交点。
    这一点很容易做到。想想这些参数,点a可以被认为是时间为0的时候的一个点,而点b则可以被认为是时间为1的时候的一个点 ,那么你可以发现,相交处的这个点一定是在这两者之间的某个地方。
    分别获得ab同平面法线的点乘积,然后使用它们和平面参数d的相反数,你就可以得到scale值(当直线同平面相交的时候,这个值在0到1之间,它是用来定义实际位置的参变量)。将这个值代入到直线参变量等式,你就可以得到相交点的位置。图5.17是以上文字的图形表示,表5.20是它的代码:

A3DwithDX9_5_17.gifwidth: 350 pxsize: -1 bytesdouble click to view all
图5.17 得到平面和直线的相交点

表5.20: plane3::Split
===========================================
    inline const point3 plane3::Split( const point3 &a, const point3 &b ) const
    {
        float aDot = (a * n);
        float bDot = (b * n);
    
        float scale = ( -d - aDot) / ( bDot - aDot );
    
        return a + (scale * (b - a));
    }
===========================================


Clipping Polygons(剪裁多边形?)

    既然你已经能够剪裁直线,那么现在你也能够剪裁多边形。用平面剪裁多边形是一个常用的操作。你需要准备一个多边形和一个平面,进行这个操作之后你将得到一个多边形,它是那个输入多边形的一部分,就是在平面前面的那部分。从概念上来说,你也可以理解成平面将在它后面的多边形切掉了,只留下在它前面的一部分。
    在剪切中主要应用的就是剪裁多边形。如果一个多边形位于某一个位置,即它的一部分在屏幕上,一部分不在屏幕上的时候的那个位置,你就需要对此多边形进行剪裁,以便你只需要绘制在屏幕上的那部分多边形就可以了。如果绘制不在屏幕上的那部分多边形可能会破坏程序。图5.18 显示了多边形的剪裁。

A3DwithDX9_5_18.gifwidth: 350 pxsize: -1 bytesdouble click to view all
图5.18 一个需要被剪裁的多边形

    为了实现多边形剪裁,我使用了Sutherland-Hodgeman多边形剪裁算法。关于该算法在书籍《Principles and Practice in C (2nd Ed.)》(由James Foley,等编写)的3.14.1计算机图像部分有讨论。 
 这个算法是相当简单易懂的。按照顺时针方向,将多边形的点排列好,然后考虑多边形每一对相邻的两点。如果第一个点在平面的前面(调用点和平面位置关系函数进行判定),你应该把这个点加到被切掉的多边形的末尾,然后重新按顺时针方向排列多边形的点,可以得到如图5.19所示的排列。如果第一个顶点和第二个顶点分处于平面的两边,那么你就要找到切割点并把它加入到列表中。虽然这样不是很直观,但是算法确实是这样做的。图5.19用图形的方式显示了算法工作的步骤。表5.21是这种操作的代码,如果剪裁多边形成功,则函数返回TURE。


             if( ( thisRes == ptBack && nextRes == ptFront ) ||
                 ( thisRes == ptFront && nextRes == ptBack ) )
             {
                   // Add the split point
                   out->pList[out->nElem++] = Split(
                           in.pList[thisInd],
                           in.pList[nextInd] );
             }
    
             thisInd = nextInd;
    
             thisRes = nextRes;
          }
          if( out->nElem >= 3 )
          {
              return true;
          }
          return false;
    }
[End]

    如果你已经有了把多边形在平面后面的部分剪裁掉的代码,那么创建一个函数把被剪裁掉的部分作为一个新的多边形保存也不难。这个操作将把那些有元素在平面两侧的多边形切割成截然不同的两部分,完全在平面前面的一部分和完全在平面后面的一部分。
    本章最后的BSP代码就是使用多边形剪裁。此算法直接从剪裁代码演变而来,代码非常相似。
表5.22:plane3::Split
[Begin]
    bool plane3::Split( polygon<point3> const &in, polygon<point3> *pFront,
                       polygon<point3> *pBack ) const
    {
    
         // Make sure our pointer to the out polygon is valid
         assert( pFront );
         // Make sure our pointer to the out polygon is valid 

   assert( pBack );
         // Make sure we're not passed a degenerate polygon
         assert( in.nElem>2);
    
// Start with curr as the last vertex and next as 0.
         pFront->nElem = 0;
         pBack->nElem = 0;
    
         int thisInd=in.nElem-1;
         int nextInd=0;
    
         ePointLoc thisRes = TestPoint( in.pList[thisInd] );
         ePointLoc nextRes;
    
         for( nextInd=0; nextInd<in.nElem; nextInd++) {
    
              nextRes = TestPoint( in.pList[nextInd] );
    
              if( thisRes == ptFront )
              {
                  // Add the point to the front
                  pFront->pList[pFront->nElem++] = in.pList[thisInd];
              }
    
              if( thisRes == ptBack )
              {
                  // Add the point to the back
                  pBack->pList[pBack->nElem++] = in.pList[thisInd];
              }
    
              if( thisRes == ptCoplanar )
              {
                  // Add the point to both
                  pFront->pList[pFront->nElem++] = in.pList[thisInd];
                  pBack->pList[pBack->nElem++] = in.pList[thisInd];
              }
    
              if( ( thisRes == ptBack && nextRes == ptFront ) ||
                  ( thisRes == ptFront && nextRes == ptBack ) )
              {
                  // Add the split point to both
                  point3 split = Split(
                     in.pList[thisInd],
                     in.pList[nextInd] );
                 pFront->pList[pFront->nElem++] = split;
                 pBack->pList[pBack->nElem++] = split;
              }
    
              thisInd = nextInd;
              thisRes = nextRes;
       }
       if( pFront->nElem > 2 && pBack->nElem > 2)
       {
           // Nothing ended up degenerate
           return true;
       }
       return false;
    }
[end]

原创粉丝点击