Baked visibility
Open the live demo · Read the source · View on GitHub
A brush level is boxes, so whether one point can see another through the walls is a question the collision world already answers exactly. Baking that answer once, into a table keyed by cell, means a frame does not raycast to find out: it looks up which cell the camera is in and which other cells that cell can see.
Step 1: A corridor with a wall across it #
One long floor, and a wall crossing it a third of the way down with no gap in it.
// A floor the whole way along, and one solid wall crossing it a third of
// the way down: nothing on the near side of the wall can be seen from
// the far side.
final level = Level(
name: 'corridor',
brushes: <Brush>[
Brush(centre: Vector3(5, -2, 2), size: Vector3(14, 4, 4)),
Brush(centre: Vector3(5, 1, 2), size: Vector3(1.0, 8, 4)),
],
);
Step 2: Bake the table #
LevelVisibility.bake samples points inside each cell of a grid over the
level and traces rays between them through the level's own collision world.
final visibility = LevelVisibility.bake(level, cellSize: 1.0);
Step 3: Hide what the eye cannot see #
VisibilityCuller is the runtime half: given the table and a list of
batches with their bounds, it turns a batch's mesh node off when the eye's
own cell cannot see it.
final culler = VisibilityCuller(visibility, <VisibilityBatch>[
(
node: nearMarker,
bounds: Aabb3.minMax(Vector3(-1, 0, 1), Vector3(1, 2, 3)),
),
(
node: farMarker,
bounds: Aabb3.minMax(Vector3(9, 0, 1), Vector3(11, 2, 3)),
),
]);
final hiddenFromNearSide = culler.apply(Vector3(0, 0.2, 2));
Standing on the near side of the wall, the culler leaves the near marker showing and turns the far one off, because every straight line from one side to the other has to cross the wall.
Step 4: Walk through the wall #
The corridor on this page has four markers and a wall down the middle; the blue ball is the eye and it walks from end to end, or stands where the slider puts it. The culler is asked every frame, and the markers on the far side of the wall disappear the moment the eye is on the other side, then come back as it crosses over. The wall is drawn, but nothing in the culler knows about drawing: its answer comes from the table baked before the walk began.
// The same culler, over four markers, asked again from wherever the eye
// is each frame: a marker is on when the eye's own cell can see it.
const List<double> stands = <double>[0.0, 1.5, 8.5, 10.0];
final List<MeshNode> markers = <MeshNode>[
for (final double x in stands)
blockNode(
context,
'marker $x',
Vector3(1.4, 1.4, 1.4),
Vector4(0.85, 0.6, 0.35, 1.0),
at: Vector3(x, 0.7, 2.0),
),
];
_live = VisibilityCuller(visibility, <VisibilityBatch>[
for (final MeshNode marker in markers)
(
node: marker,
bounds: Aabb3.minMax(
marker.readPosition() - Vector3.all(0.7),
marker.readPosition() + Vector3.all(0.7),
),
),
]);