flutter3d
Showcase Changelog 38 packages API reference

BVH and frustum culling

since 0.7.0 Scene and geometry

Submitting a mesh that cannot reach the image wastes CPU and GPU work. The render list tests world-space boxes against the view frustum and uses a BVH once a scene is large enough to benefit from the index.

Step 1: Keep a small group in view #

Sixteen cubes form the part of the scene the camera can see. They share one mesh and material, but remain separate nodes so each has its own world bounds.

final Scene scene = Scene()
  ..ambientColor = Vector3(0.44, 0.53, 0.7)
  ..ambientIntensity = 0.14;
for (var i = 0; i < visibleCount; i++) {
  final int x = i % 4;
  final int z = i ~/ 4;
  scene.add(
    MeshNode(cube, material, name: 'visible cube $i')
      ..setPosition((x - 1.5) * 1.35, 0.0, (z - 1.5) * 1.35),
  );
}

Step 2: Put a large branch outside the frustum #

Another 304 cubes sit under one parent far from the view. The parent's subtreeBounds encloses the whole warehouse, allowing a branch-level test to reject it before every child needs individual work.

_offscreenBranch = SceneNode(name: 'offscreen warehouse')
  ..setPosition(120.0, 0.0, 80.0);
for (var i = 0; i < hiddenCount; i++) {
  final int x = i % 19;
  final int z = i ~/ 19;
  _offscreenBranch.add(
    MeshNode(cube, material, name: 'offscreen cube $i')
      ..setPosition(x * 1.1, 0.0, z * 1.1),
  );
}
scene.add(_offscreenBranch);

Step 3: Light only what survives #

Lighting happens after visibility is decided. The directional light therefore reaches the visible cubes without turning the offscreen warehouse into draw calls.

scene.add(
  LightNode(name: 'sun', intensity: 3.1)
    ..setLocalForward(Vector3(-0.5, -0.8, -0.3)),
);

Step 4: Read the culling result #

FrameResult.culled reports how many scene meshes did not enter the colour pass. The page checks the full scene size, the branch bound, and that at least all 304 offscreen cubes were rejected.

final int total = visibleCount + hiddenCount;
if (scene.meshes.length != total ||
    _offscreenBranch.subtreeBounds == null ||
    frame.culled < hiddenCount ||
    frame.drawCalls < visibleCount) {
  throw StateError('the offscreen mesh field was not culled');
}