subtreeBounds property

Aabb3? get subtreeBounds

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;
}