平面是一个无限扩展的区域。读者可以将其形象化为沿着上下两个方向无限扩展的平面墙。如果很难理解平面这个概念,那么试着考虑一下宇宙。想象一样,宇宙非常平而且沿着所有其他边不断无限扩展的情况。
虽然屏幕区域有限,但可以在屏幕上绘制平面,因为只要对有限数量的像素做阴影处理即可。然而,在Direct3D和OpenGL中并没有内置渲染平面的方法。所以,如果不能渲染平面,那就会出现问题,即平面到底对谁有好处?关于这个问题,答案有很多。最常见的一个答案就是几何图形选择和碰撞检测
有了几何图形选择,就可以创建一个平面或一系列平面,而这取决于几何图形位于平面的哪一侧,并可以快速确定是否需要绘制该对象。因为这可以很大程度地加速程序的渲染次数,所以这种方法很有用。使用选择法通过几个快速数学运算,就可以快速拒绝将要发送给渲染API的几何图形。如果场景中有数十、数百或是更多的浏览者不可见的对象,那么使用选择法就可以节省大量的时间和处理操作。另一方面,如果真正要渲染场景中的每个几何图形,而不考虑它是否可见,那就会注意到程序帧数显著下降。下面的公式是从数学上定义一个平面:
V*N + D = 0
方程中的D代表平面到原点的距离。变量V代表位于平面某个位置上的点。该点可以在平面的任意位置上。变量N是平面法线。平面法线是与平面正交的矢量,它朝向平面前面的方向。利用该信息可以确定平面的哪一侧是前面,哪一侧是后面。这样就可以测试一个点或其他几何图形是在平面前面、后面或是在平面扩展处。如果某个视角周围有6个平面,分别代表左、右、上、下、远、近,那么就可以描述对象可见、不可见或部分可见。此时,可以拒绝或渲染测试后的几何图形。对大场景而言,这样做可以节省大量的时间。
平面类中的成员变量并不多,所需的全部成员变量就是平面的法线(由x、y、z变成了a、b、c)和平面距离。这意味着屏幕结构主要由4个浮点变量a、b、c、d构成。
class CPlane
{
public:
CPlane();
CPlane(float A, float B, float C, float D);
void CreatePlaneFromTri(Vector3D &v1, Vector3D &v2,
Vector3D &v3);
bool Intersect(CPlane &p1, Vector3D *point);
bool Intersect(CAabb &aabb);
int ClassifyPolygon(CPolygon &pol);
int ClassifyPoint(Vector3D &v);
float GetDistance(Vector3D &v);
float a, b, c, d;
};
CPlane::CPlane()
{
a = 0;
b = 0;
c = 0;
d = 0;
}
CPlane::CPlane(float A, float B, float C, float D)
{
a = A;
b = B;
c = C;
d = D;
}
平面类中的下一个函数是CreatePlaneFromTri()。该函数利用三角形信息计算平面法线和距离。该函数以构成三角形的三个顶点为参数。CreatePlaneFromTri()函数内部计算三角形的法线,并将三角形的x、y、z成员变量设为平面的a、b、c成员变量。距离d通过计算平面法线和第一个顶点的点积,并对该点积求反得到。CreatePlaneFromTri()函数如程序
void CPlane::CreatePlaneFromTri(Vector3D &v1, Vector3D &v2, Vector3D &v3)
{
Vector3D normal, e1, e2;
e1 = v3 - v1;
e2 = v2 - v1;
e1.Normal();
e2.Normal();
normal.CrossProduct(e1, e2);
normal.Normal();
a = normal.x;
b = normal.y;
c = normal.z;
d = - (a * tril.x + b * tril.y + c * tril.z);
}
平面类中的下一个函数是第一个交叉点函数。Intersect()函数用于测试两个平面之间是否相交。如果这两个平面相交,函数返回true(真)及相交的交叉点。该函数的参数包括第二个平面对象,以及如果要和平面相交将要存储交叉的矢量对象上的点。测试两个平面是否相交的Intersect()函数的完整代码如程序清单
bool CPlane::Intersect(CPlane &pl, Vector3D *intersectPoint)
{
Vector3D cross;
Vector3D normal(a, b, c);
Vector3D plNormal(pl.a, pl.b, pl.c);
float length = 0;
cross.CrossProduct(normal, plNormal);
length = cross.DotProduct3(cross);
if(length < 1e-08f) return false;
if(intersectPoint)
{
float l0 = normal.DotProduct3(normal);
float l1 = normal.DotProduct3(plNormal);
float l2 = plNormal.DotProduct3(plNormal);
float det = l0 * l2 - l1 * l1;
float invDet = 0;
if(fabs(det) < 1e-08f) return false;
invDet = 1 / det;
float d0 = (l2 * d - l1 * pl.d) * invDet;
float d1 = (l0 * pl.d - l1 * d) * invDet;
(*intersectPoint) = normal * d0 + plNormal * d1;
}
return true;
}
平面类中的另一个交叉函数是重载的Intersect()函数。该函数测试平面是否和轴对称的边界框相交。这将在本书稍后的BSP树演示程序中派上用场。
bool CPlane::Intersect(CAabb &aabb)
{
Vector3D min, max;
Vector3D normal(a, b, c);
if (normal.x >= 0.0f)
{
min.x = aabb.m_min.x;
max.x = aabb.m_max.x;
}
else
{
min.x = aabb.m_max.x;
max.x = aabb.m_min.x;
}
if (normal.y >= 0.0f)
{
min.y = aabb.m_min.y;
max.y = aabb.m_max.y;
}
else
{
min.y = aabb.m_max.y;
max.y = aabb.m_min.y;
}
if (normal.z >= 0.0f)
{
min.z = aabb.m_min.z;
max.z = aabb.m_max.z;
}
else
{
min.z = aabb.m_max.z;
max.z = aabb.m_min.z;
}
if ((normal.DotProduct3(min)+d) > 0.0f) return false;
if ((normal.DotProduct3(max)+d) >= 0.0f) return true;
return false;
}
对平面而言,要知道点位于平面哪一侧。点可以在平面上,也可以在平面前面,还可以在平面后面。通常在不同的选择算法中会使用它,而且还可用它确定是否要绘制某些内容。同样可以在碰撞时使用它。如果旧位置位于平面前面,而新位置位于平面后面,那么因为位置穿过平面,所以可知发生了碰撞。确定点位于平面哪一侧的方法称为“分类点”。在平面类中有一个名为ClassifyPoint()的函数。ClassifyPoint()函数如程序清单8.25所示,它以3D位置为参数。该函数通过计算平面和位置的点积,并加上平面距离而返回点和平面的相对位置,点要么在平面前面、要么在平面后面,要么在平面上。如果距离小于0,表示点在平面后面;大于0,表示在平面前面;等于0,则在平面上。
int CPlane::ClassifyPoint(Vector3D &v)
{
float distance = a * v.x + b * v.y + c * v.z + d;
if(distance > 0.001) return UGP_FRONT;
if(distance < -0.001) return UGP_BACK;
return UGP_ON_PLANE;
}
了解了点分类的方法就可以确认三角形或多边形位于平面哪一侧。这通过将图元中的所有点分类加以确认。如果所有的点都在多边形的一侧,那么整个图元就在该多边形的那一侧。如果某些点在多边形一侧,某些点在另一侧,那么该图元穿过平面。程序清单8.26中的ClassifyPolygon()函数完成测试。它以一个多边形对象(稍后将会是更多的多边形对象)为参数,并返回多边形是在平面的前面、后面还是穿过该平面的结果。
int CPlane::ClassifyPolygon(CPolygon &pol)
{
int frontPolys = 0;
int backPolys = 0;
int planePolys = 0;
int type = 0;
for(int i = 0; i < 3; i++)
{
type = ClassifyPoint(pol.m_vertexList[i]);
switch(type)
{
case UGP_FRONT:
frontPolys++;
break;
case UGP_BACK:
backPolys++;
break;
default:
frontPolys++;
backPolys++;
planePolys++;
break;
}
}
if(planePolys == 3) return UGP_ON_PLANE;
else if(frontPolys == 3) return UGP_FRONT;
else if(backPolys == 3) return UGP_BACK;
return UGP_CLIPPED;
}
平面类的最后一个函数计算平面到一个3D矢量的距离。计算平面和3D矢量之间的点积可以得到该距离,返回结果为一浮点值。程序清单8.27给出了平面GetDistance()成员函数的实现代码。
float CPlane::GetDistance(Vector3D &v)
{
return a*v.x + b*v.y + c*v.z + d;
}
Direct3D平面
如同所有其他数学结构一样,Direct3D还包含了一套平面对象。Direct3D平面对象D3DXPLANE包含了计算平面的4D矢量点积的D3DXPlaneDot()函数、计算平面和3D矢量点积的D3DXPlaneDotCoord()函数、使用假定w为0计算平面和3D矢量点积的D3DXPlaneDotCoord()函数、由三角形计算平面的D3DXPlaneFromPoints()函数及由点和法线计算平面的D3DXPlaneFromPointNormal()函数。如果准备使用Direct3D平面,那么可以查看DirectX SDK文档获取完整的信息。D3DXPLANE结构如程序清单8.28所示。
typedef struct D3DXPLANE {
FLOAT a;
FLOAT b;
FLOAT c;
FLOAT d;
} D3DXPLANE;