深度探索DxFramework 10-1


请尊重原作者的工作,转载时请务必注明转载自:www.xionggf.com

第10章 进入三维的世界 1

10.1.3D预备数学知识和相关代码的分析

10.2 向量,矩阵,四元数

10.3 三维几何工具集

10.3.1 与三维空间的平面相关算法

对于给定的3D点P0和法向量N,过点P0并且和N垂直的平面就可以定义为满足方程N.(P-P0) = 0的集合。(不明白这个定义怎么来的读者,可以回忆一下两向量点积的几何意义)。如下图,平面中任意点与P0的连线都与法线N是垂直的。

如果令法向量N为(A,B,C),点P0为(P0x,P0y,P0z)。则平面的方程为:

$ Ax + By + Cz + D = 0 $ ( 此时 $ Ax + By +Cz + D = \overset{\rightharpoonup}{N}\cdot P $ ,$ D = -\overset{\rightharpoonup}{N}\cdot P $ )

根据平面法线的指向,平面把它所处的空间划分成两个半空间,法线所指向的半空间称为正半空间,也可以称为正侧空间。另一半则称为负半空间,也称为负侧空间。判断某点和某平面的位置关系可以依照上面三个判断式,如下:

判断式 位置关系
Ax+By+Cz+D=0 点(x,y,z)在平面上
Ax+By+Cz+D>0 点(x,y,z)在正半空间
Ax+By+Cz+D<0 点(x,y,z)在负半空间

在此可以举一个例子来说明。如下图:

在图中,平面A由法线N(0,1,0)和点P0(0,5,0)定义。那么,定义式Ax+By+Cz+D=0的D为: D=-N.P0 = -(00+15+0*0) = -5

P3的坐标为(6,5,2),代入方程式Ax+By+Cz+D中,为: 60+51+0*2-5=0

所以P3在平面上。P1的坐标为(0,10,0),代入方程式Ax+By+Cz+D中,为: 00+101+0*0-5=5>0

所以P1在平面的正半空间。P2的坐标为(0,2,0),代入方程式Ax+By+Cz+D中,为: 00+21+0*0-5=-3<0

所以P1在平面的负半空间。tool3d.cpp文件中定义了点和平面的位置关系判断的两个函数,如下:

 1//检查某点p_Point是否在某平面p_Plane的正侧:
 2bool positiveSide(D3DXVECTOR3* p_Point, D3DXPLANE* p_Plane)
 3{
 4    return p_Plane->a * p_Point->x +  p_Plane->b * p_Point->y + 
 5             p_Plane->c * p_Point->z + p_Plane->d > 0;
 6}
 7
 8//检查某点p_Point和某平面p_Plane的位置关系,1为正侧, 
 9//-1为负侧,0为点在平面上
10int whichSide(D3DXVECTOR3* p_Point, D3DXPLANE* p_Plane) 
11{
12    float temp = p_Plane->a * p_Point->x + 
13                 p_Plane->b * p_Point->y + 
14                 p_Plane->c * p_Point->z + p_Plane->d;
15    
16    if (temp > epsilon) 
17    {
18        return 1;
19    }
20    else if (temp < (-epsilon))
21    {
22        return -1;
23    } 
24    else
25    {
26        return 0;
27    }
28}

10.3.2 计算三角形面积和凸多边形面积

计算三角形的面积的方法有多种,最常用的就是把两个全等的三角形,组合成一个平行四边形,然后求出平行四边形的面积再除以2就可以了。当给定一个三角形的三个顶点坐标的时候,利用向量叉积的几何性质,就可以按照上述方法来求出三角形的面积。我们知道,两向量P×Q,有|P×Q| = |P|.|Q|.sinA,A是两向量的夹角。从几何意义上来说,两向量的叉积的模,就等于以这两向量的模的长度为不等边,组成的平行四边形的面积,如下图:

DXFrameWork提供了计算三角形面积的函数,如下:

1XFLOAT triangleArea(D3DXVECTOR3* p0, D3DXVECTOR3* p1, 
2                    D3DXVECTOR3* p2)
3{
4    D3DXVECTOR3 v1 = (*p1) - (*p0);
5    D3DXVECTOR3 v2 = (*p2) - (*p0);
6    return 0.5 * D3DXVec3Length(D3DXVec3Cross(&v1, &v1, &v2));
7}

知道了三角形的面积如何计算,接下来凸多边形的面积也好计算了。无非就是把凸多边形分解为若干个三角形,计算每个三角形的面积后累加即可。

 1//第一个参数是凸多边形的顶点列表,第二个参数numPoints是凸多边形的顶点数
 2XFLOAT convexPolygonArea(D3DXVECTOR3* points, int numPoints) 
 3{
 4    int i;
 5    XFLOAT sum = 0.0;
 6    //注意points[0]是该凸多边形分解而成的三角形的共用顶点
 7    for (i = 2; i < numPoints; i++)
 8    {
 9        sum += triangleArea(&points[0],&points[i - 1],&points[i]);
10    }
11    return sum;
12}

计算凸多边形的面积的示意图如下:

10.3.3 线段和面(face)相交检测

检查某线段是否和某面(face,注意不是平面)相交的代码如下:

 1bool checkEdgeIntersectsFace( D3DXVECTOR3 p_Edge[2], D3DXVECTOR3* p_Face, int numVertices,  D3DXPLANE* p_Plane, D3DXVECTOR3* p_PointOnPlane)  {
 2    //要进行相交判断的face,只能是三角形或者是四边形。
 3    if(numVertices > MAX_VERTICES_FOR_EDGE_INTERSECT_CHECKING) 
 4    {
 5        throw Error(_T("checkEdgeIntersectsFace : Number of
 6                        vertices used in checking an edge 
 7                      intersects with a face is too many"));
 8        return false;
 9    }
10
11    // 通过计算面积,判断要检查的face是不是一个一维的线段。如果是就返回false
12    XFLOAT area = convexPolygonArea(p_Face, numVertices);
13    if ((area < 0.001) && (area > -0.001))
14        return false; 
15
16    //如果线段的两个端点都在face所处平面的同一侧,则显然这个线段是不会和该face相交的
17    bool temp1, temp2;
18    temp1 = positiveSide(&p_Edge[0], p_Plane);
19    temp2 = positiveSide(&p_Edge[1], p_Plane);
20
21    if (temp1 == temp2)
22    {
23        return false;
24    }
25
26    //再检查线段和face所在的平面是否相交,如果不相交,则线段肯定不和face相交
27    if (D3DXPlaneIntersectLine(p_PointOnPlane, p_Plane, &p_Edge[0], &p_Edge[1]) == NULL) 
28    {
29        return false;
30    }

如果计算到线段和face所在的平面相交,那么接下来就是要判断相交点是落在face的内侧还是外测了。在计算机图形学中,往往把三维对象通过降维处理,可以降低问题的复杂度。降维的方法是:通过去掉face的法线N的绝对值最大的分量所对应的坐标,就可以把问题降到二维空间来考虑。如下图所示,降维过程相当于把face投影到xy,xz或者是yz平面上。在此先假设被去掉的是z坐标,即投影到xy平面上。如下图投影多边形到XZ,YZ,XY平面上

判断某点在不在多边形内,可以把问题转化为判断点在不在某三角形内。如上图,把多边形划分为若干个三角形。然后判断点是否在这些三角形内,只要判断到点在某一三角形内,就肯定这点在此多边形内。先考虑如何判断点是否在三角形内。上面已经把三角形投影到一个二维平面来进行考虑。假定投影在了xz平面上。对三角形的每条边进行下面的二维差运算,有:

1E = P1-P0
2F=P2-P0
3G=P-P0

从本质上说,这样做的意义上移动坐标系,使得顶点P0位于原点位置。或者可以认为是以P0为原点。如下图所示。

在上面的式子中,向量E所代表的边有内侧和外测之分,将边向量旋转90°(顺时针或者逆时针旋转都可以),便可以构造出该边的法向量,而又因为点F一定是在边的正侧,所以点积Ne.F的符号就是边OE的任一内侧点的符号。因此,如果P点位于该边的内侧的话,那么就必须要满足:

1(Ne.F)(Ne.G)=(Ne.(P2-P0))(Ne.(P-P0))≥0

对于任何的点,如果它都位于三角形的三条边的内侧,那么它就在三角形内。从上面的图可以看出,线段P2-P0和法线Ne的点积是大于0(观察其夹角便可知道,其夹角绝对值小于90°),那么线段P-P0和法线Ne的点积也应大于0,即Ne.(P-P0)>0。假定是投影到XY平面上,而Ne又等于P1-P0乘以一个绕Z轴90°的旋转矩阵。令Ne为(NeX,NeY),P0为(x0,y0),P1为(x1,y1),P2为(x2,y2),P为(x,y)则有:

1NeX = (x1-x0)*cos90°+(y1-y0)*sin90°
2NeY = -(x1-x0)*sin90°+(y1-y0)*cos90°

所以NeX=(y1-y0),NeY=-(x1-x0),求P2在P0P1的那一侧,就求P0P2与法线的夹角值就可以了:当:

1Ne.(P2-P0)=NeX*(x2-x1)+NeY*(y2-y1)=(y1-y0)*(x2-x1)+(-(x1-x0)*(y2-y1))>0

就表示p2是在边e的正侧。 而Ne.(P2-P0)=NeX*(x2-x1)+NeY*(y2-y1)经过整理后,实质上就是下面行列式的值,如下:

1|x0 y0 1|
2|x1 y1 1|
3|x2 y2 1|

所以判断点P是否在边e的内侧,就是把上面行列式中点P2的分量代替为点P的分量,只需要计算行列式的值是否大于0便可。大于0,在e的正侧,小于0在e的负侧。下面接着的代码就是根据这个原理实现。

 1    //接上段代码,    checkEdgeIntersectsFace函数,tools3d.cpp
 2    float absa = (float)fabs(p_Plane->a); 
 3    float absb = (float)fabs(p_Plane->b); 
 4    float absc = (float)fabs(p_Plane->c);
 5    
 6    static D3DXVECTOR2 
 7        face2D[MAX_VERTICES_FOR_EDGE_INTERSECT_CHECKING];
 8    
 9    D3DXVECTOR2 point2D;
10    int i;
11
12    //下面的几个if-else判断式,就是根据法线的分量大小,把三角形投影到适当的平面上去。
13    if ((absa >= absb) && (absa >= absc)) 
14    {
15        for (i = 0; i < numVertices; i++)
16        {
17            face2D[i].x = p_Face[i].y;
18            face2D[i].y = p_Face[i].z;
19        }
20        point2D.x = p_PointOnPlane->y;
21        point2D.y = p_PointOnPlane->z;
22    }
23    else if ((absb >= absa) && (absb >= absc)) 
24    {
25        for (i = 0; i < numVertices; i++)
26        {
27            face2D[i].x = p_Face[i].x;
28            face2D[i].y = p_Face[i].z;
29        }
30        point2D.x = p_PointOnPlane->x;
31        point2D.y = p_PointOnPlane->z;
32    } 
33    else 
34    {
35        for (i = 0; i < numVertices; i++)
36        {
37            face2D[i].x = p_Face[i].x;
38            face2D[i].y = p_Face[i].y;
39        }
40        point2D.x = p_PointOnPlane->x;
41        point2D.y = p_PointOnPlane->y;
42    }
43
44    //p0,p1是多边形的线段的两端点,p2是想要检查的点。
45
46    float val;
47    //这里就是检查(Ne.F)(Ne.G)的结果值    
48    val=(face2D[0].x * face2D[1].y + face2D[0].y * point2D.x 
49         + face2D[1].x * point2D.y  ) -(point2D.x * 
50         face2D[1].y + point2D.y * face2D[0].x + 
51         face2D[1].x * face2D[0].y);
52    bool cc = val > 0;
53    bool cctemp;
54    if (fabs(val) < 0.0001f)
55     return true; //在边上
56    
57    //逐个边检查。
58    for (i = 1; i < numVertices; i++)
59    {
60         val = (face2D[i].x * face2D[(i + 1) % numVertices].y 
61                + face2D[i].y * point2D.x + face2D[(i + 1) % 
62                numVertices].x * point2D.y) -
63              (point2D.x * face2D[(i + 1) % numVertices].y + 
64              point2D.y * face2D[i].x + face2D[(i + 1) %
65              numVertices].x * face2D[i].y);
66
67           if (fabs(val) < 0.0001f) return true; //在边上
68         cctemp = val > 0;
69         if ((cctemp != cc))
70         {
71             return false;
72         }
73    }
74    return true; }

10.3.4 分割三角形

 1void splitTriangle(D3DXPLANE* p_Plane, D3DXVECTOR3* p0, 
 2                           D3DXVECTOR3* p1, D3DXVECTOR3* p2, 
 3                           unsigned short* positiveIndices, 
 4                           DWORD* p_NumPositiveIndices, 
 5                           unsigned short* negativeIndices, 
 6                          DWORD* p_NumNegativeIndices)
 7{
 8    //检查三角形的三个角顶点在分割平面的哪一则?正?负?面上?
 9    int sides[3];  //存储顶点与分割平面的位置信息
10    int numzero = 0, i;//三角形角顶点在分割平面上的个数
11    sides[0] = whichSide(p0, p_Plane);
12    sides[1] = whichSide(p1, p_Plane);
13    sides[2] = whichSide(p2, p_Plane);
14
15    if (sides[0] == 0) numzero++;
16    if (sides[1] == 0) numzero++;
17    if (sides[2] == 0) numzero++;
18    
19    //如果没有角顶点在分割平面上
20    if (numzero == 0) 
21    {
22        //检查有多少个角顶点在分割平面的正侧
23        int numPositive = 0;
24        for (i = 0; i < 3; i++) {
25            if (sides[i] == 1) {
26                numPositive++;
27            }
28        }
29
30        //根据角顶点在分割平面的正侧的个数,分四种情况考虑。
31        //情况1:没有一个角顶点在分割平面上,
32        if (numPositive == 0) 
33        {
34              //没有一个角顶点在分割平面的正侧,那么所有的角顶点都在
35              //分割平面的负侧
36              //在分割平面的正侧的角顶点数为0
37            *p_NumPositiveIndices = 0;
38             //在分割平面的负侧的角顶点数为3
39            *p_NumNegativeIndices = 3;
40
41            //记录下在分割平面负侧的角顶点编号
42            negativeIndices[0] = 0;
43            negativeIndices[1] = 1;
44            negativeIndices[2] = 2;
45        }

 1        //有一个顶点在分割平面的正侧 
 2else if (numPositive == 1)
 3{
 4            // 找到哪个角顶点在分割平面的正侧
 5            int index;
 6            for (i = 0; i < 3; i++) {
 7                if (sides[i] == 1) {
 8                    index = i;
 9                    break;
10                }
11            }
12            
13            //原三角形在正侧部分被分割成1一个三角形,所以顶点数为3
14            //在负侧部分被分割成2个三角形,所以顶点数为6
15            *p_NumPositiveIndices = 3;
16            *p_NumNegativeIndices = 6;
17
18            positiveIndices[0] = 3 + (index + 2) % 3;
19            positiveIndices[1] = index;
20            positiveIndices[2] = 3 + index;
21
22            negativeIndices[0] = 3 + index;
23            negativeIndices[1] = (index + 1) % 3;
24            negativeIndices[2] = 3 + (index + 2) % 3;
25
26            negativeIndices[3] = (index + 1) % 3;
27            negativeIndices[4] = (index + 2) % 3;
28            negativeIndices[5] = 3 + (index + 2) % 3;
29
30        }

 1  else if (numPositive == 2) {
 2            // 1 point is on negative side
 3            // Find which one is it
 4            int index;
 5            for (i = 0; i < 3; i++) {
 6                if (sides[i] == -1) {
 7                    index = i;
 8                    break;
 9                }
10            }
11            // Two triangles on positive side
12            // One triangles on negative side
13            *p_NumPositiveIndices = 6;
14            *p_NumNegativeIndices = 3;
15
16            positiveIndices[0] = 3 + index;
17            positiveIndices[1] = (index + 1) % 3;
18            positiveIndices[2] = 3 + (index + 2) % 3;
19
20            positiveIndices[3] = (index + 1) % 3;
21            positiveIndices[4] = (index + 2) % 3;
22            positiveIndices[5] = 3 + (index + 2) % 3;
23
24            negativeIndices[0] = 3 + (index + 2) % 3;
25            negativeIndices[1] = index;
26            negativeIndices[2] = 3 + index;
27
28        } else if (numPositive == 3) {
29            // Alll points are on positive side
30            *p_NumPositiveIndices = 3;
31            *p_NumNegativeIndices = 0;
32
33            positiveIndices[0] = 0;
34            positiveIndices[1] = 1;
35            positiveIndices[2] = 2;
36        } 
37    }

 1     //如果有一个角顶点在分割平面上
 2else if (numzero == 1) {
 3        //One point is on the plane
 4        //Can either be 1. On one side of the plane 
 5        //                2. On two side of the plane
 6        int sum = sides[0] + sides[1] + sides[2];
 7        //-1+1+0=0,一个顶点在分割面正侧,另一个在负侧,一个在面上
 8        if (sum == 0) {
 9            //On two side of the plane
10            int index;
11
12            // Find which one is on the plane
13            for (i = 0; i < 3; i++) {
14                if (sides[i] == 0) {
15                    index = i;
16                    break;
17                }
18            }
19            
20            //分别找到在正侧和在负测的顶点的索引
21            if (sides[(index + 1) % 3] == 1) {
22                // Positive
23                *p_NumPositiveIndices = 3;
24                *p_NumNegativeIndices = 3;
25
26                positiveIndices[0] = index;
27                positiveIndices[1] = (index + 1) % 3;
28                positiveIndices[2] = 3 + (index + 1) % 3;
29
30                negativeIndices[0] = 3 + (index + 1) % 3;
31                negativeIndices[1] = (index + 2) % 3;
32                negativeIndices[2] = index;
33            } else {
34                // Negative
35                *p_NumPositiveIndices = 3;
36                *p_NumNegativeIndices = 3;
37
38                positiveIndices[0] = 3 + (index + 1) % 3;
39                positiveIndices[1] = (index + 2) % 3;
40                positiveIndices[2] = index;
41
42                negativeIndices[0] = index;
43                negativeIndices[1] = (index + 1) % 3;
44                negativeIndices[2] = 3 + (index + 1) % 3;
45            }
46        } 

 1else {
 2            //On one side of the plane
 3            for (i = 0; i < 3; i++)    {
 4                if (sides[i] != 0) {
 5                    break;
 6                }
 7            }
 8            if (sides[i] == 1) {
 9                //The triangle is on positive sides
10                *p_NumPositiveIndices = 3;
11                *p_NumNegativeIndices = 0;
12
13                positiveIndices[0] = 0;
14                positiveIndices[1] = 1;
15                positiveIndices[2] = 2;
16            } else {
17                //The triangle is on negative side
18                *p_NumPositiveIndices = 0;
19                *p_NumNegativeIndices = 3;
20
21                negativeIndices[0] = 0;
22                negativeIndices[1] = 1;
23                negativeIndices[2] = 2;
24            }
25        }
26    }

 1else if (numzero == 2) {
 2        //Two points are on the plane
 3        // See which one is not 
 4        for (i = 0; i < 3; i++)    {
 5            if (sides[i] != 0) {
 6                break;
 7            }
 8        }
 9
10        if (sides[i] == 1) {
11            //The triangle is on positive side
12            *p_NumPositiveIndices = 3;
13            *p_NumNegativeIndices = 0;
14
15            positiveIndices[0] = 0;
16            positiveIndices[1] = 1;
17            positiveIndices[2] = 2;
18
19        } else {
20            //The triangle is on negative side
21            *p_NumPositiveIndices = 0;
22            *p_NumNegativeIndices = 3;
23
24            negativeIndices[0] = 0;
25            negativeIndices[1] = 1;
26            negativeIndices[2] = 2;
27        }
28    } 

最后一种情况,当被分割的三角形的三个角顶点都在分割平面上,就要检查三角形的法线和分割平面的法线是否同向。同向则表示三角形被分割成一个,在分割平面的正侧;反向则表示三角形也被分割成一个,在分割平面的负侧。

 1else     
 2{
 3        D3DXVECTOR3 v01, v02, normal;
 4
 5        //利用向量叉积的几何性质,根据三角形的两条边求此三角形的法线。要注意方向
 6        v01 = (*p1) - (*p0);
 7        v02 = (*p2) - (*p0);
 8        D3DXVec3Cross(&normal, &v01, &v02);
 9        D3DXVec3Normalize(&normal, &normal);
10
11        //用D3DX库函数计算到平面的的法线。
12        D3DXPLANE plane = *p_Plane;
13        D3DXPlaneNormalize(&plane, &plane);
14        D3DXVECTOR3 planeNormal =  D3DXVECTOR3(plane.a, plane.b, plane.c);
15
16        //观察两法线的差
17        D3DXVECTOR3 temp = planeNormal - normal;
18
19        //如果两法线的差的模很小很小,接近于0,就可以认为这三角形的
20        //法线和分割平面的法线是同向。三角形被分在分割平面的正侧,否则
21        //就分在的负测
22        if (D3DXVec3Length(&temp) < 0.01f) {
23            // 在正侧
24            *p_NumPositiveIndices = 3;
25            *p_NumNegativeIndices = 0;
26
27            positiveIndices[0] = 0;
28            positiveIndices[1] = 1;
29            positiveIndices[2] = 2;
30            
31        } else {
32            // 在负测
33            *p_NumPositiveIndices = 0;
34            *p_NumNegativeIndices = 3;
35
36            negativeIndices[0] = 0;
37            negativeIndices[1] = 1;
38            negativeIndices[2] = 2;
39        }
40    }
41}