subtreeBounds property
What this node and everything under it occupies, or null when the subtree draws nothing.
A branch rejected whole — gfx-66n. A node holding a thousand meshes
was a thousand frustum tests, because the only bound in the engine was a
mesh's own. This is the union over the subtree, cached on
changeEpoch like every other derived quantity here, so a scene that
nobody touched computes it once and a scene that moved computes it once
more.
Recursive rather than iterative on purpose: the recursion reads
subtreeBounds on each child, so every node on the way down caches its
own answer and the whole tree costs one pass rather than one per node. A
hierarchy deep enough to overflow a stack here is one whose transforms
would have overflowed it first.
The box is in world space and it is grown by whatever MeshNode.frustumCulled refuses: a subtree holding a node that opted out of culling answers subtreeAlwaysDrawn, and a caller that rejects on this box has to honour that or a sky dome disappears.
Implementation
Aabb3? get subtreeBounds {
if (_subtreeEpoch == _dirtyEpoch) return _subtreeBounds;
_subtreeEpoch = _dirtyEpoch;
_subtreeAlwaysDrawn = false;
Aabb3? box = ownBounds;
if (box != null) {
// A fresh box rather than the node's own: the union below writes into it,
// and a `MeshNode`'s world bounds are the cache the whole engine reads.
box = Aabb3.copy(box);
_subtreeAlwaysDrawn = !ownBoundsAreCullable;
}
for (var i = 0; i < _children.length; i++) {
final child = _children[i];
final childBox = child.subtreeBounds;
if (child._subtreeAlwaysDrawn) _subtreeAlwaysDrawn = true;
if (childBox == null) continue;
if (box == null) {
box = Aabb3.copy(childBox);
} else {
box.hull(childBox);
}
}
return _subtreeBounds = box;
}