The Annotated ATI SDK BSP Tree Source
Part III:Implementation
丁欧南
Keyword:[Triangle Split][Collision Detection][Bounding Sphere]
此系列文章介绍包含在ATI SDK(March 2006)中的BSP Tree源代码,它实现了这些主要功能:BSP Tree 的离线编译,对第一人称视角的碰撞检测(实际上是Bounding Sphere Collision Detection).
这篇文章将接触BSP.cpp中所实现的算法.
1.初始化
void BTri::setup(){
//因为逆时针旋向为正面,所以如下计算法线
normal = normalize(cross(v[1] - v[0], v[2] - v[0]));
//利用Part I 4.1节所述技术
offset = -dot(v[0], normal);
//由叉积右手定则,法线是垂直于边且指向三角形里
edgeNormals[0] = cross(normal, v[0] - v[2]);
edgeNormals[1] = cross(normal, v[1] - v[0]);
edgeNormals[2] = cross(normal, v[2] - v[1]);
//注意:未在点积前加负号,偏移量其实是反向的
edgeOffsets[0] = dot(edgeNormals[0], v[0]);
edgeOffsets[1] = dot(edgeNormals[1], v[1]);
edgeOffsets[2] = dot(edgeNormals[2], v[2]);
}
2.线段与平面交点
vec3 planeHit(const vec3 &v0, const vec3 &v1, const vec3 &normal, const float offset){
//使用Part I 4.2所述的技术.v0对应于S
//dir对应于V
//d/(dot(normal,dir))对应于t
vec3 dir = v1 - v0;
float d = dot(v0, normal) + offset;
vec3 pos = v0 - (d / dot(normal, dir)) * dir;
return pos;
}
3.三角形分割
void BTri::split(BTri *dest, int &nPos, int &nNeg, const vec3 &normal, const float offset, const float epsilon) const {
float d[3];
//分别取得顶点距分割面的带符号距离
//使用了Part I 5的技术
for (int i = 0; i < 3; i++){
d[i] = dot(v[i], normal) + offset;
}
//first,second分别指示当前三角形的起讫点
//并用以逆时针遍历三个边
int first = 2;
int second = 0;
//找寻第一组分列于分割面两侧的顶点
while (!(d[second] > epsilon && d[first] <= epsilon)){
first = second;
second++;
}
// 处理正面的三角形分割
nPos = 0;
//first,second分别记录了被分割边的相应顶点
//h是第一次相交的分割点
vec3 h = planeHit(v[first], v[second], normal, offset);
//以h为源点,进行一次三角形扇形状的分割
//直到遇到第二个分割点,表明正面分割完毕
do {
first = second;
second++;
if (second >= 3) second = 0;
dest->v[0] = h;
dest->v[1] = v[first];
if (d[second] > epsilon){
dest->v[2] = v[second];
} else {
//如果first,second分列于分割面两侧
//则用分割点作为最后一个正面三角形的顶点.
dest->v[2] = h = planeHit(v[first], v[second], normal, offset);
}
dest->data = data;
dest->setup();
dest++;
nPos++;
} while (d[second] > epsilon);
// 重新初始化first,second,使其位于反面三角形的第一条边
if (fabsf(d[second]) <= epsilon){
first = second;
second++;
if (second >= 3) second = 0;
}
//处理反面的三角形分割
nNeg = 0;
//h在处理正面多边形是被记录,
//是第二次与分割面相交的点
//以h为源点,进行一次三角形扇形状的分割
//直到遇到下一个分割点,表明反面分割完毕
do {
first = second;
second++;
if (second >= 3) second = 0;
dest->v[0] = h;
dest->v[1] = v[first];
if (d[second] < -epsilon){
dest->v[2] = v[second];
} else {
dest->v[2] = planeHit(v[first], v[second], normal, offset);
}
dest->data = data;
dest->setup();
dest++;
nNeg++;
} while (d[second] < -epsilon);
}
4.三角形相交测试
bool BTri::intersects(const vec3 &v0, const vec3 &v1) const {
//v0,v1分别是起讫点,计算出的dir其实是反的.
vec3 dir = v0 - v1;
//参见 Part I 4.3 所述技术
float k = (dot(normal, v0) + offset) / dot(normal, dir);
//k<0或k>1时都是v0,v1的延长线与三角相交,故return false;
if (k < 0 || k > 1) return false;
vec3 pos = v0 - k * dir;
//检测交点是否位于三角形里
for (unsigned int i = 0; i < 3; i++){
if (dot(edgeNormals[i], pos) < edgeOffsets[i]){
return false;
}
}
return true;
}
5.判断一点是否正投影于三角形
bool BTri::isAbove(const vec3 &pos) const {
//使用暴力编码以提高效率
/* for (unsigned int i = 0; i < 3; i++){
if (dot(edgeNormals[i], pos) < edgeOffsets[i]){
return false;
}
}
return true;
*/ //注意>=后的edgeOffsets无符号,这样移项之后,edgeOffsets的方向便被正了过来
//回忆我们在setup里说的edgeOffsets反向问题
//以下编码技术介绍在Part I 5
return (edgeNormals[0].x * pos.x + edgeNormals[0].y * pos.y + edgeNormals[0].z * pos.z >= edgeOffsets[0] &&
edgeNormals[1].x * pos.x + edgeNormals[1].y * pos.y + edgeNormals[1].z * pos.z >= edgeOffsets[1] &&
edgeNormals[2].x * pos.x + edgeNormals[2].y * pos.y + edgeNormals[2].z * pos.z >= edgeOffsets[2]);
}
6.BSP Tree节点判断相交位置
bool BNode::intersects(const vec3 &v0, const vec3 &v1, const vec3 &dir, vec3 *point, const BTri **triangle) const {
//分别计算v0,v1到平面的距离
float d0 = dot(v0, tri.normal) + tri.offset;
float d1 = dot(v1, tri.normal) + tri.offset;
//记录找到的交点,用于赋给point
vec3 pos;
if (d0 > 0){
if (d1 <= 0){
//如果v0,v1分列平面两侧,求交点
//但注意交点未必位于三角形上
pos = v0 - (d0 / dot(tri.normal, dir)) * dir;
}
//对于pos没有位于三角形上的情况
//递归进入下一层节点找寻更确切的交点,见图
if (front != NULL && front->intersects(v0, (d1 <= 0)? pos : v1, dir, point, triangle)) return true;
if (d1 <= 0){
if (tri.isAbove(pos)){
//如果pos确实在当前三角形上,则成功.
if (point) *point = pos;
if (triangle) *triangle = &tri;
return true;
}
//如果交点被判断可能在反面,则递归进入反面查找
if (back != NULL && back->intersects(pos, v1, dir, point, triangle)) return true;
}
} else {
if (d1 > 0){
pos = v0 - (d0 / dot(tri.normal, dir)) * dir;
}
if (back != NULL && back->intersects(v0, (d1 > 0)? pos : v1, dir, point, triangle)) return true;
if (d1 > 0){
if (tri.isAbove(pos)){
if (point) *point = pos;
if (triangle) *triangle = &tri;
return true;
}
if (front != NULL && front->intersects(pos, v1, dir, point, triangle)) return true;
}
}
return false;
}
7.Bounding Sphere Collision Detection
对于这个ATI SDK版本的碰撞检测,我要说,它是有Bug的.它少了对于一个顶点与Sphere相交时的判断,并且,对于夹角小于pi/2的两个平面将导致碰撞检测失效.对于这个论题,如果你想知道更多,请在Google Groups上的comp.graphics.algorithm搜索Bounding Sphere in ATI SDK查看我与David H.Eberly的讨论.
bool BNode::pushSphere(vec3 &pos, const float radius) const {
//取得pos到当前节点的距离
float d = dot(pos, tri.normal) + tri.offset;
bool pushed = false;
if (fabsf(d) < radius){
if (tri.isAbove(pos)){
//如果pos的正投影在三角形上,并且与三角形距离小于半径
//则将pos沿面法线方向向外推
// Right above the triangles
pos += (radius - d) * tri.normal;
pushed = true;
} else {
//如果pos并非正投影在三角形上,但距离却小于半径时
//则Sphere有可能与三角形的边侧交
// Near any of the edges?
vec3 v1 = tri.v[2];
for (int i = 0; i < 3; i++){
vec3 v0 = v1;
v1 = tri.v[i];
vec3 diff = v1 - v0;
float t = dot(diff, pos - v0);
if (t > 0){
float f = dot(diff, diff);
if (t < f){
//t<f,说明t/f<1,v的延长线未超过diff本身,
//则Sphere与一条边侧交肯定
vec3 v = v0 + (t / f) * diff;
vec3 dir = pos - v;
//沿pos在边上的垂直投影线反向推开Sphere
if (dot(dir, dir) < radius * radius){
pos = v + radius * normalize(dir);
break;
}
}
}
}
}
}
//递归进入其它边检测,以防止以上的操作将Sphere推入其它的三角形体内
//其实防不住的,见本节首叙述
if (front != NULL && d > -radius) pushed |= front->pushSphere(pos, radius);
if (back != NULL && d < radius) pushed |= back ->pushSphere(pos, radius);
return pushed;
}
8.Build the BSP Tree
void BNode::build(Array <BTri> &tris, const int cutWeight, const int unbalanceWeight){
float epsilon = 0.001f ;
//指引最佳分割面
unsigned int index = 0;
//最佳分割面的最小分值
int minScore = 0x7FFFFFFF;
for (unsigned int i = 0; i < tris.getCount(); i++){
//当前分割面的分值
int score = 0;
//左右子树的层数差
int diff = 0;
for (unsigned int k = 0; k < tris.getCount(); k++){
//分别记录正面,反面三角形个数
unsigned int neg = 0, pos = 0;
for (unsigned int j = 0; j < 3; j++){
float dist = dot(tris[k].v[j], tris[i].normal) + tris[i].offset;
if (dist < -epsilon) neg++; else
if (dist > epsilon) pos++;
}
if (pos){
//如果正反面顶点都存在,则必定要分解平面
if (neg) score += cutWeight; else diff++;
} else {
//如果正反面顶点俱无,则与当前平面共面,算作正面
if (neg) diff--; else diff++;
}
}
score += unbalanceWeight * abs(diff);
if (score < minScore){
//取罚分最少的三角形作为分割平面
minScore = score;
index = i;
}
}
//把分割平面提出,并从三角形集中删除
tri = tris[index];
tris.fastRemove(index);
Array <BTri> backTris;
Array <BTri> frontTris;
for (unsigned int i = 0; i < tris.getCount(); i++){
unsigned int neg = 0, pos = 0;
for (unsigned int j = 0; j < 3; j++){
float dist = dot(tris[i].v[j], tri.normal) + tri.offset;
if (dist < -epsilon) neg++; else
if (dist > epsilon) pos++;
}
if (neg){
if (pos){
BTri newTris[3];
int nPos, nNeg;
tris[i].split(newTris, nPos, nNeg, tri.normal, tri.offset, epsilon);
//dest[0,nPos)是正面三角形
for (int i = 0; i < nPos; i++){
frontTris.add(newTris[i]);
}
//dest[nPos,nNeg)是反面三角形
for (int i = 0; i < nNeg; i++){
backTris.add(newTris[nPos + i]);
}
} else {
backTris.add(tris[i]);
}
} else {
frontTris.add(tris[i]);
}
}
清空三角形集
tris.reset();
//递归进入下一节点进行构造,终结条件是正/反三角形集为空
if (backTris.getCount() > 0){
back = new BNode;
back->build(backTris, cutWeight, unbalanceWeight);
} else back = NULL;
if (frontTris.getCount() > 0){
front = new BNode;
front->build(frontTris, cutWeight, unbalanceWeight);
} else front = NULL;
}