深度探索DxFramework 10-1
Table of Contents
请尊重原作者的工作,转载时请务必注明转载自: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文件中定义了点和平面的位置关系判断的两个函数,如下:
//检查某点p_Point是否在某平面p_Plane的正侧:
bool positiveSide(D3DXVECTOR3* p_Point, D3DXPLANE* p_Plane)
{
return p_Plane->a * p_Point->x + p_Plane->b * p_Point->y +
p_Plane->c * p_Point->z + p_Plane->d > 0;
}
//检查某点p_Point和某平面p_Plane的位置关系,1为正侧,
//-1为负侧,0为点在平面上
int whichSide(D3DXVECTOR3* p_Point, D3DXPLANE* p_Plane)
{
float temp = p_Plane->a * p_Point->x +
p_Plane->b * p_Point->y +
p_Plane->c * p_Point->z + p_Plane->d;
if (temp > epsilon)
{
return 1;
}
else if (temp < (-epsilon))
{
return -1;
}
else
{
return 0;
}
}
10.3.2 计算三角形面积和凸多边形面积
计算三角形的面积的方法有多种,最常用的就是把两个全等的三角形,组合成一个平行四边形,然后求出平行四边形的面积再除以2就可以了。当给定一个三角形的三个顶点坐标的时候,利用向量叉积的几何性质,就可以按照上述方法来求出三角形的面积。我们知道,两向量P×Q,有|P×Q| = |P|.|Q|.sinA,A是两向量的夹角。从几何意义上来说,两向量的叉积的模,就等于以这两向量的模的长度为不等边,组成的平行四边形的面积,如下图:
DXFrameWork提供了计算三角形面积的函数,如下:
XFLOAT triangleArea(D3DXVECTOR3* p0, D3DXVECTOR3* p1,
D3DXVECTOR3* p2)
{
D3DXVECTOR3 v1 = (*p1) - (*p0);
D3DXVECTOR3 v2 = (*p2) - (*p0);
return 0.5 * D3DXVec3Length(D3DXVec3Cross(&v1, &v1, &v2));
}
知道了三角形的面积如何计算,接下来凸多边形的面积也好计算了。无非就是把凸多边形分解为若干个三角形,计算每个三角形的面积后累加即可。
//第一个参数是凸多边形的顶点列表,第二个参数numPoints是凸多边形的顶点数
XFLOAT convexPolygonArea(D3DXVECTOR3* points, int numPoints)
{
int i;
XFLOAT sum = 0.0;
//注意points[0]是该凸多边形分解而成的三角形的共用顶点
for (i = 2; i < numPoints; i++)
{
sum += triangleArea(&points[0],&points[i - 1],&points[i]);
}
return sum;
}
计算凸多边形的面积的示意图如下:
10.3.3 线段和面(face)相交检测
检查某线段是否和某面(face,注意不是平面)相交的代码如下:
bool checkEdgeIntersectsFace( D3DXVECTOR3 p_Edge[2], D3DXVECTOR3* p_Face, int numVertices, D3DXPLANE* p_Plane, D3DXVECTOR3* p_PointOnPlane) {
//要进行相交判断的face,只能是三角形或者是四边形。
if(numVertices > MAX_VERTICES_FOR_EDGE_INTERSECT_CHECKING)
{
throw Error(_T("checkEdgeIntersectsFace : Number of
vertices used in checking an edge
intersects with a face is too many"));
return false;
}
// 通过计算面积,判断要检查的face是不是一个一维的线段。如果是就返回false
XFLOAT area = convexPolygonArea(p_Face, numVertices);
if ((area < 0.001) && (area > -0.001))
return false;
//如果线段的两个端点都在face所处平面的同一侧,则显然这个线段是不会和该face相交的
bool temp1, temp2;
temp1 = positiveSide(&p_Edge[0], p_Plane);
temp2 = positiveSide(&p_Edge[1], p_Plane);
if (temp1 == temp2)
{
return false;
}
//再检查线段和face所在的平面是否相交,如果不相交,则线段肯定不和face相交
if (D3DXPlaneIntersectLine(p_PointOnPlane, p_Plane, &p_Edge[0], &p_Edge[1]) == NULL)
{
return false;
}
如果计算到线段和face所在的平面相交,那么接下来就是要判断相交点是落在face的内侧还是外测了。在计算机图形学中,往往把三维对象通过降维处理,可以降低问题的复杂度。降维的方法是:通过去掉face的法线N的绝对值最大的分量所对应的坐标,就可以把问题降到二维空间来考虑。如下图所示,降维过程相当于把face投影到xy,xz或者是yz平面上。在此先假设被去掉的是z坐标,即投影到xy平面上。如下图投影多边形到XZ,YZ,XY平面上
判断某点在不在多边形内,可以把问题转化为判断点在不在某三角形内。如上图,把多边形划分为若干个三角形。然后判断点是否在这些三角形内,只要判断到点在某一三角形内,就肯定这点在此多边形内。先考虑如何判断点是否在三角形内。上面已经把三角形投影到一个二维平面来进行考虑。假定投影在了xz平面上。对三角形的每条边进行下面的二维差运算,有:
E = P1-P0
F=P2-P0
G=P-P0
从本质上说,这样做的意义上移动坐标系,使得顶点P0位于原点位置。或者可以认为是以P0为原点。如下图所示。
在上面的式子中,向量E所代表的边有内侧和外测之分,将边向量旋转90°(顺时针或者逆时针旋转都可以),便可以构造出该边的法向量,而又因为点F一定是在边的正侧,所以点积Ne.F的符号就是边OE的任一内侧点的符号。因此,如果P点位于该边的内侧的话,那么就必须要满足:
(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)则有:
NeX = (x1-x0)*cos90°+(y1-y0)*sin90°
NeY = -(x1-x0)*sin90°+(y1-y0)*cos90°
所以NeX=(y1-y0),NeY=-(x1-x0),求P2在P0P1的那一侧,就求P0P2与法线的夹角值就可以了:当:
Ne.(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)经过整理后,实质上就是下面行列式的值,如下:
|x0 y0 1|
|x1 y1 1|
|x2 y2 1|
所以判断点P是否在边e的内侧,就是把上面行列式中点P2的分量代替为点P的分量,只需要计算行列式的值是否大于0便可。大于0,在e的正侧,小于0在e的负侧。下面接着的代码就是根据这个原理实现。
//接上段代码, checkEdgeIntersectsFace函数,tools3d.cpp
float absa = (float)fabs(p_Plane->a);
float absb = (float)fabs(p_Plane->b);
float absc = (float)fabs(p_Plane->c);
static D3DXVECTOR2
face2D[MAX_VERTICES_FOR_EDGE_INTERSECT_CHECKING];
D3DXVECTOR2 point2D;
int i;
//下面的几个if-else判断式,就是根据法线的分量大小,把三角形投影到适当的平面上去。
if ((absa >= absb) && (absa >= absc))
{
for (i = 0; i < numVertices; i++)
{
face2D[i].x = p_Face[i].y;
face2D[i].y = p_Face[i].z;
}
point2D.x = p_PointOnPlane->y;
point2D.y = p_PointOnPlane->z;
}
else if ((absb >= absa) && (absb >= absc))
{
for (i = 0; i < numVertices; i++)
{
face2D[i].x = p_Face[i].x;
face2D[i].y = p_Face[i].z;
}
point2D.x = p_PointOnPlane->x;
point2D.y = p_PointOnPlane->z;
}
else
{
for (i = 0; i < numVertices; i++)
{
face2D[i].x = p_Face[i].x;
face2D[i].y = p_Face[i].y;
}
point2D.x = p_PointOnPlane->x;
point2D.y = p_PointOnPlane->y;
}
//p0,p1是多边形的线段的两端点,p2是想要检查的点。
float val;
//这里就是检查(Ne.F)(Ne.G)的结果值
val=(face2D[0].x * face2D[1].y + face2D[0].y * point2D.x
+ face2D[1].x * point2D.y ) -(point2D.x *
face2D[1].y + point2D.y * face2D[0].x +
face2D[1].x * face2D[0].y);
bool cc = val > 0;
bool cctemp;
if (fabs(val) < 0.0001f)
return true; //在边上
//逐个边检查。
for (i = 1; i < numVertices; i++)
{
val = (face2D[i].x * face2D[(i + 1) % numVertices].y
+ face2D[i].y * point2D.x + face2D[(i + 1) %
numVertices].x * point2D.y) -
(point2D.x * face2D[(i + 1) % numVertices].y +
point2D.y * face2D[i].x + face2D[(i + 1) %
numVertices].x * face2D[i].y);
if (fabs(val) < 0.0001f) return true; //在边上
cctemp = val > 0;
if ((cctemp != cc))
{
return false;
}
}
return true; }
10.3.4 分割三角形
void splitTriangle(D3DXPLANE* p_Plane, D3DXVECTOR3* p0,
D3DXVECTOR3* p1, D3DXVECTOR3* p2,
unsigned short* positiveIndices,
DWORD* p_NumPositiveIndices,
unsigned short* negativeIndices,
DWORD* p_NumNegativeIndices)
{
//检查三角形的三个角顶点在分割平面的哪一则?正?负?面上?
int sides[3]; //存储顶点与分割平面的位置信息
int numzero = 0, i;//三角形角顶点在分割平面上的个数
sides[0] = whichSide(p0, p_Plane);
sides[1] = whichSide(p1, p_Plane);
sides[2] = whichSide(p2, p_Plane);
if (sides[0] == 0) numzero++;
if (sides[1] == 0) numzero++;
if (sides[2] == 0) numzero++;
//如果没有角顶点在分割平面上
if (numzero == 0)
{
//检查有多少个角顶点在分割平面的正侧
int numPositive = 0;
for (i = 0; i < 3; i++) {
if (sides[i] == 1) {
numPositive++;
}
}
//根据角顶点在分割平面的正侧的个数,分四种情况考虑。
//情况1:没有一个角顶点在分割平面上,
if (numPositive == 0)
{
//没有一个角顶点在分割平面的正侧,那么所有的角顶点都在
//分割平面的负侧
//在分割平面的正侧的角顶点数为0
*p_NumPositiveIndices = 0;
//在分割平面的负侧的角顶点数为3
*p_NumNegativeIndices = 3;
//记录下在分割平面负侧的角顶点编号
negativeIndices[0] = 0;
negativeIndices[1] = 1;
negativeIndices[2] = 2;
}
//有一个顶点在分割平面的正侧
else if (numPositive == 1)
{
// 找到哪个角顶点在分割平面的正侧
int index;
for (i = 0; i < 3; i++) {
if (sides[i] == 1) {
index = i;
break;
}
}
//原三角形在正侧部分被分割成1一个三角形,所以顶点数为3
//在负侧部分被分割成2个三角形,所以顶点数为6
*p_NumPositiveIndices = 3;
*p_NumNegativeIndices = 6;
positiveIndices[0] = 3 + (index + 2) % 3;
positiveIndices[1] = index;
positiveIndices[2] = 3 + index;
negativeIndices[0] = 3 + index;
negativeIndices[1] = (index + 1) % 3;
negativeIndices[2] = 3 + (index + 2) % 3;
negativeIndices[3] = (index + 1) % 3;
negativeIndices[4] = (index + 2) % 3;
negativeIndices[5] = 3 + (index + 2) % 3;
}
else if (numPositive == 2) {
// 1 point is on negative side
// Find which one is it
int index;
for (i = 0; i < 3; i++) {
if (sides[i] == -1) {
index = i;
break;
}
}
// Two triangles on positive side
// One triangles on negative side
*p_NumPositiveIndices = 6;
*p_NumNegativeIndices = 3;
positiveIndices[0] = 3 + index;
positiveIndices[1] = (index + 1) % 3;
positiveIndices[2] = 3 + (index + 2) % 3;
positiveIndices[3] = (index + 1) % 3;
positiveIndices[4] = (index + 2) % 3;
positiveIndices[5] = 3 + (index + 2) % 3;
negativeIndices[0] = 3 + (index + 2) % 3;
negativeIndices[1] = index;
negativeIndices[2] = 3 + index;
} else if (numPositive == 3) {
// Alll points are on positive side
*p_NumPositiveIndices = 3;
*p_NumNegativeIndices = 0;
positiveIndices[0] = 0;
positiveIndices[1] = 1;
positiveIndices[2] = 2;
}
}
//如果有一个角顶点在分割平面上
else if (numzero == 1) {
//One point is on the plane
//Can either be 1. On one side of the plane
// 2. On two side of the plane
int sum = sides[0] + sides[1] + sides[2];
//-1+1+0=0,一个顶点在分割面正侧,另一个在负侧,一个在面上
if (sum == 0) {
//On two side of the plane
int index;
// Find which one is on the plane
for (i = 0; i < 3; i++) {
if (sides[i] == 0) {
index = i;
break;
}
}
//分别找到在正侧和在负测的顶点的索引
if (sides[(index + 1) % 3] == 1) {
// Positive
*p_NumPositiveIndices = 3;
*p_NumNegativeIndices = 3;
positiveIndices[0] = index;
positiveIndices[1] = (index + 1) % 3;
positiveIndices[2] = 3 + (index + 1) % 3;
negativeIndices[0] = 3 + (index + 1) % 3;
negativeIndices[1] = (index + 2) % 3;
negativeIndices[2] = index;
} else {
// Negative
*p_NumPositiveIndices = 3;
*p_NumNegativeIndices = 3;
positiveIndices[0] = 3 + (index + 1) % 3;
positiveIndices[1] = (index + 2) % 3;
positiveIndices[2] = index;
negativeIndices[0] = index;
negativeIndices[1] = (index + 1) % 3;
negativeIndices[2] = 3 + (index + 1) % 3;
}
}
else {
//On one side of the plane
for (i = 0; i < 3; i++) {
if (sides[i] != 0) {
break;
}
}
if (sides[i] == 1) {
//The triangle is on positive sides
*p_NumPositiveIndices = 3;
*p_NumNegativeIndices = 0;
positiveIndices[0] = 0;
positiveIndices[1] = 1;
positiveIndices[2] = 2;
} else {
//The triangle is on negative side
*p_NumPositiveIndices = 0;
*p_NumNegativeIndices = 3;
negativeIndices[0] = 0;
negativeIndices[1] = 1;
negativeIndices[2] = 2;
}
}
}
else if (numzero == 2) {
//Two points are on the plane
// See which one is not
for (i = 0; i < 3; i++) {
if (sides[i] != 0) {
break;
}
}
if (sides[i] == 1) {
//The triangle is on positive side
*p_NumPositiveIndices = 3;
*p_NumNegativeIndices = 0;
positiveIndices[0] = 0;
positiveIndices[1] = 1;
positiveIndices[2] = 2;
} else {
//The triangle is on negative side
*p_NumPositiveIndices = 0;
*p_NumNegativeIndices = 3;
negativeIndices[0] = 0;
negativeIndices[1] = 1;
negativeIndices[2] = 2;
}
}
最后一种情况,当被分割的三角形的三个角顶点都在分割平面上,就要检查三角形的法线和分割平面的法线是否同向。同向则表示三角形被分割成一个,在分割平面的正侧;反向则表示三角形也被分割成一个,在分割平面的负侧。
else
{
D3DXVECTOR3 v01, v02, normal;
//利用向量叉积的几何性质,根据三角形的两条边求此三角形的法线。要注意方向
v01 = (*p1) - (*p0);
v02 = (*p2) - (*p0);
D3DXVec3Cross(&normal, &v01, &v02);
D3DXVec3Normalize(&normal, &normal);
//用D3DX库函数计算到平面的的法线。
D3DXPLANE plane = *p_Plane;
D3DXPlaneNormalize(&plane, &plane);
D3DXVECTOR3 planeNormal = D3DXVECTOR3(plane.a, plane.b, plane.c);
//观察两法线的差
D3DXVECTOR3 temp = planeNormal - normal;
//如果两法线的差的模很小很小,接近于0,就可以认为这三角形的
//法线和分割平面的法线是同向。三角形被分在分割平面的正侧,否则
//就分在的负测
if (D3DXVec3Length(&temp) < 0.01f) {
// 在正侧
*p_NumPositiveIndices = 3;
*p_NumNegativeIndices = 0;
positiveIndices[0] = 0;
positiveIndices[1] = 1;
positiveIndices[2] = 2;
} else {
// 在负测
*p_NumPositiveIndices = 0;
*p_NumNegativeIndices = 3;
negativeIndices[0] = 0;
negativeIndices[1] = 1;
negativeIndices[2] = 2;
}
}
}