In his note Havran advises the use of double linked lists, i think that's overkill if careful. Then, like Wald, he proposes to work with boundaries (events in Wald terminology) as the basic unit; again i think that's not wise because such events/boundaries are always paired and we'd be losing that connection.
That leads us to a structure like...
- Code: Select all
struct lbox_t {
struct boundary_t {
float pos;
ptr_t next;
} dim[3][2]; // 3 axis, 2 sides.
union flags_t { ... } flags;
lbox_t *clone;
uint_t triangle_id;
char padding[4];
};
... which happens to be 64 bytes (2 cache lines) wide on 32 bit platforms.
We have a triangle id to allow for 'perfect clips', a bit field (flags) for transient tagging and some padding - let's put aside the clone pointer for now -.
Now we have to decide how we're going to handle pointers from one boundary to the other, keeping in mind that our goal is to keep the whole lbox_t structure in sight. As our structure is now 64 bytes wide, if aligned we could safely deduce from, say, &dim[1][1].next the underlying lbox_t structure address; sadly that wouldn't work on 64 bit platforms unless we pad the whole thing to 128 bytes. Unacceptable.
Instead we're going to encode sideness into pointers, within the lowest bit, because boundaries are uniquely identified as the triplet lbox_t, axis, side; so lbox_t::boundary_t::ptr_t really is a 'lbox_t *' pointer and a side bit. Extracting either the side or a proper pointer only requires a logical and.
Fast scoring.
- Code: Select all
for each axis
open = 0; close = 0;
for each linked boundaries
open += local_open; local_open = 0;
close += local_close; local_close = 0;
num_planars = 0;
split = boundary.pos;
// basically, scan ahead for all boundaries with a matching position.
while (true) {
(lbox, is_left) = boundary;
if (lbox.is_planar(axis, split))
++num_planars;
else {
local_open += is_left ? 1 : 0;
local_close += is_left ? 0 : 1;
}
if (boundary.next.pos != split)
break;
boundary = boundary.next;
split = boundary.pos;
}
// completly on the left side.
num_left = close+local_close;
// crossing, local_open(s) end up on the right side.
num_cross = open - num_left;
// completly on the right side.
num_right = total_count - (num_left + num_cross + num_planars);
<assign planars to either side>
<update score>
end
end
That's the gist of it, in bogus pseudo-code: we recognize 3 zones (< split, == split, > split) and try to minimize the number of score evaluations; storing the point where we transition from '== split' to '> split' allows for some shortcuts when building the left/right node further down the code (but also leads to ugly spaghetti code).
Split clipping & building the left/right lists.
Suppose we have a winning split, we now have to categorize our triangles and build left/right lists for 3 axis.
A naive approach would walk all those triangles 4 times (1 to categorize, 3 to build left/right sub-lists) or worse.
The fact is, if we're careful, we can categorize and build the left/right sub-lists for the winning axis at once (with no merging whatsoever); we just need another property for our boundary lists: a left side should always come before a right side for a given lbox_t. That require an adjusted sort phase and to maintain that order thereafter.
- Code: Select all
// k = winning axis, ku/kv = other axis.
for each boundaries on the winning axis
(is_left, lbox) = boundary;
if (is_left) {
// categorize this lbox_t for the given axis as: left/right cross/planar
// if crossing, both cross and left tags are set.
// update its flags accordingly.
categorize(lbox, axis, winning_split);
if (lbox.tag_cross) {
lboxr = allocate();
lbox.clone = lboxr; // store a reference, we'll come back to it.
<rewire lboxr as following lbox on axis ku and kv>
lbox.dim[k][side_right].pos = split;
lboxr.dim[k][side_left].pos = split;
sublist_left.append(lbox, k, side_left);
sublist_right.append(lboxr, k, side_left);
}
else if (lbox_tag_left)
sublist_left.append(lbox, k, side_left);
else
sublist_right.append(lbox, k, side_left);
}
else {
if (lbox.tag_cross) {
park_left_end.append(lbox, k, side_right);
sublist_right.append(lbox->clone, k, side_right);
}
else if (lbox_tag_left)
sublist_left.append(lbox, k, side_right);
else
sublist_right.append(lbox, k, side_right);
}
end
park_left_end.close();
sublist_left.append(park_left_end);
sublist_right.close();
A bit hairy, but no merging and we categorize only once.
It's then simpler for axis ku & kv, we just build sub-lists as we go according to tags set previously.
You'll notice i'm not updating/clipping those lbox proxy against voxels, first because i'm still tinkering with that code and then it doesn't introduce any new boundaries; it may only update values, in a known order (more precisely increasing a left boundary and decreasing a right one).
Voilà, i hope it's clear enough.

