解决方案
我发现了一个对于我的BSP树目的非常有效的启发式算法,同时也非常有趣。在下面的代码中,我首先尝试使用AABB树来询问“这条线段与哪些线段相交”。但即使如此,速度仍然太慢,因此最终我选择了一种昂贵的初始O(N^2)
比较算法,随着BSP树的构建,它会快速加速,利用了一些巧妙的观察!
- 让每个BSP节点跟踪其子空间中仍留在其中的线段(以及必须从中选择下一个分割器的线段)。
- 让每个线段有四个与之关联的值:
posCount
、negCount
、introduced
和saved
。如果该线段被分割,则还应具有对另一个线段的partner
引用(否则为null
)。
- 使用以下
O(N^2)
算法初始化根节点(即所有节点)的分割器:
算法calcRelationCounts(splitters S)
:O(N^2)
for all splitters s in S
s.posCount = s.negCount = s.introduced = s.saved = 0
for all splitters vs in S
if (s == vs) continue
if vs is fully on the positive side of the plane of s
s.posCount++
else if vs is fully on the negative side of the plane of s
s.negCount++
else if vs intersects the plane of s
s.negCount++, s.posCount++, s.introduced++
else if vs is coplanar with s
s.saved++
- 对于仍有分裂器的每个节点,选择最大化以下内容的节点:
算法evaluate(...)
,其中treeDepth = floor(log2(splitterCountAtThisNode))
:O(1)
evaluate(posCount, negCount, saved, introduced, treeDepth) {
float f;
if (treeDepth >= EVALUATE_X2) {
f = EVALUATE_V2;
} else if (treeDepth >= EVALUATE_X1) {
float r = treeDepth - EVALUATE_X1;
float w = EVALUATE_X2 - EVALUATE_X1;
f = ((w-r) * EVALUATE_V1 + r * EVALUATE_V2) / w;
} else {
f = EVALUATE_V1;
}
float balanceScore = -f * BALANCE_WEIGHT * abs(posCount - negCount);
float freedomScore = (1.0f-f) * (SAVED_WEIGHT * saved - INTRO_WEIGHT * introduced);
return freedomScore + balanceScore;
}
以下是我的优化算法使用的魔数:
#define BALANCE_WEIGHT 437
#define INTRO_WEIGHT 750
#define SAVED_WEIGHT 562
#define EVALUATE_X1 3
#define EVALUATE_X2 31
#define EVALUATE_V1 0.0351639f
#define EVALUATE_V2 0.187508f
- 使用此分裂器作为此节点的分裂器,将其称为SEL。然后,将此节点中的所有分裂器分成三组:
positives
、negatives
和 remnants
。
算法 distributeSplitters()
:
for all splitters s at this node
s.partner = null
if s == SEL then add s to "remnants"
else
if s is fully on the positive side of SEL
add s to "positives"
else if s is fully on the negative side of SEL
add s to "negatives
else if s intersects SEL
split s into two appropriate segments sp and sn
sp.partner = sn, sn.partner = sp
add sn to "negatives", sp to "positives" and s to "remnants"
else if s coplanar with SEL
add s to "remnants"
// the clever bit
if (positives.size() > negatives.size())
calcRelationCounts(negatives)
updateRelationCounts(positives, negatives, remnants)
else
calcRelationCounts(positives)
updateRelationCounts(negatives, positives, remnants)
add positives and negatives to appropriate child nodes for further processing
我意识到的巧妙之处是,通常,尤其是以上启发式算法的前几个拆分,会产生非常不平衡的拆分(但非常自由)。问题在于,当n很小的时候,您得到的是"
O(N^2) + O((N-n)^2)" + ...
,这是可怕的!相反,我意识到我们可以重新计算需要
O(n^2)
的最小拆分长度,然后只需迭代每个位拆分分隔器,并从较小的拆分部分中减去计数,这只需要
O(Nn)
,比
O(N^2)
好得多!以下是
updateRelationCounts()
的代码:
算法
updateRelationCounts()
:
updateRelationCounts(toUpdate, removed, remnants) {
for all splitters s in toUpdate
for all splitters vs in removed, then remnants
if vs has a partner
if the partner intersects s
s.posCount++, s.negCount++, s.introduced++
else if the partner is fully on the positive side of s
s.posCount++
else if the partner is fully on the negative side of s
s.negCount++
else if the partner is coplanar with s
s.saved++
else
if vs intersects s
s.posCount--, s.negCount--, s.introduced--
else if vs is fully on the positive side of s
s.posCount--
else if vs is fully on the negative side of s
s.negCount--
else if vs is coplanar with s
s.saved--
我现在已经仔细测试过了,似乎逻辑是正确的,更新操作正确地修改了
posCount
等变量,使它们与重新硬计算后的结果相同!