Generic 3D BFS light propagation with two-phase removal for voxel engines
See the codeBFS flood-fill light propagation with two-phase removal and re-propagation
from surviving sources. Operates on any storage that implements a single trait
method. Returns updates as a Vec — never mutates caller data.
cargo add voxel-light
Implement the trait for your storage:
use voxel_light::{VoxelAccess, VoxelInfo};
impl VoxelAccess for MyWorld {
fn get_voxel(&self, x: i32, y: i32, z: i32) -> Option<VoxelInfo> {
let chunk = self.get_chunk(x.div_euclid(16), y.div_euclid(16), z.div_euclid(16))?;
let block = chunk.get_block(x.rem_euclid(16) as usize, /* ... */);
Some(VoxelInfo {
transparent: block.is_transparent(),
block_light: chunk.get_light(x, y, z),
emission: block.emission(),
})
}
}
Call the algorithm and apply results:
use voxel_light::LightEngine;
let mut engine = LightEngine::new();
// Place a torch (level 14)
let updates = engine.place_light(&world, [10, 64, 10], 14);
for u in &updates {
world.set_light(u.x, u.y, u.z, u.level);
}
// Remove it — two-phase removal zeroes dependents, then re-propagates
// from any neighboring sources that still exist
let updates = engine.remove_light(&world, [10, 64, 10]);
for u in &updates {
world.set_light(u.x, u.y, u.z, u.level);
}
| Method | When to call |
|---|---|
place_light(access, pos, level) | Light-emitting block placed |
remove_light(access, pos) | Light-emitting block broken |
block_placed(access, pos) | Opaque block placed (may cut light paths) |
block_removed(access, pos) | Opaque block broken (light may flow in) |
recalculate_area(access, center, radius) | Clear and re-propagate all emitters in a cubic radius |
Free functions (propagate, remove) are available if you don't need
LightEngine's buffer reuse.
| Feature | Effect |
|---|---|
std (default) | Uses ahash for the visited map (~2x faster than BTreeMap) |
sky | Column-first sky light propagation with horizontal BFS spread |
colored | Per-channel RGB light (3 channels, single BFS pass) |
Core is no_std compatible (requires alloc). Without std, the visited map
uses BTreeMap.
Open-air world (no obstructions, worst-case BFS expansion). AMD Ryzen 9 7900, 64 GB RAM, Windows 11:
| Operation | Level 7 | Level 10 | Level 14 |
|---|---|---|---|
| Propagation | 17 µs | 60 µs | 174 µs |
| Removal (single source) | 105 µs | — | 432 µs |
| Removal (neighboring source) | — | — | 463 µs |
| Full place + remove cycle | — | — | 585 µs |
A level-14 torch touches ~11,500 voxels. Benchmarks use criterion: cargo bench.
Propagation. BFS from source, decaying by 1 per block. Stops at opaque
voxels (transparent: false) and world boundaries (None from get_voxel).
Visited map prevents re-processing.
Removal phase 1. BFS from removed source. For each neighbor: if light < node's old level, it was dependent — zero it and enqueue. If light >= old level, it has independent light — save as boundary source.
Removal phase 2. Build an overlay that returns zeroed values for all positions cleared in phase 1 (without requiring the caller to apply them first). BFS re-propagate from all boundary sources against this overlay.
Output. Flat Vec<LightUpdate>. Zeroing updates appear first, then
re-propagation values. Apply in order.
The algorithm is standard (Seed of Andromeda documented it in 2015). These projects implement it by hand:
voxelalgorithms.cpp, ~500 LOC, C++No existing crate on crates.io provides this. block-mesh handles meshing.
building-blocks handles storage. tapestry is 2D. pathfinding solves
graph traversal without light-decay or removal semantics.
Not a spatial data structure. Queries your storage via the trait and returns updates. Does not store light values.
Visited map allocated per call. The AHashMap is not pooled across
calls. Typical cost: ~11k entries for level-14 propagation.
Colored removal over-zeroes. A voxel is considered dependent if any channel is less than the corresponding old level. Edge cases with overlapping colored sources of different ranges may zero more than necessary.
No sky light removal. The sky feature provides propagation only.
Placing a roof requires the caller to recalculate affected columns.
6 commits
Rust
100.0%
Generic 3D BFS light propagation with two-phase removal for voxel engines
See the codeBFS flood-fill light propagation with two-phase removal and re-propagation
from surviving sources. Operates on any storage that implements a single trait
method. Returns updates as a Vec — never mutates caller data.
cargo add voxel-light
Implement the trait for your storage:
use voxel_light::{VoxelAccess, VoxelInfo};
impl VoxelAccess for MyWorld {
fn get_voxel(&self, x: i32, y: i32, z: i32) -> Option<VoxelInfo> {
let chunk = self.get_chunk(x.div_euclid(16), y.div_euclid(16), z.div_euclid(16))?;
let block = chunk.get_block(x.rem_euclid(16) as usize, /* ... */);
Some(VoxelInfo {
transparent: block.is_transparent(),
block_light: chunk.get_light(x, y, z),
emission: block.emission(),
})
}
}
Call the algorithm and apply results:
use voxel_light::LightEngine;
let mut engine = LightEngine::new();
// Place a torch (level 14)
let updates = engine.place_light(&world, [10, 64, 10], 14);
for u in &updates {
world.set_light(u.x, u.y, u.z, u.level);
}
// Remove it — two-phase removal zeroes dependents, then re-propagates
// from any neighboring sources that still exist
let updates = engine.remove_light(&world, [10, 64, 10]);
for u in &updates {
world.set_light(u.x, u.y, u.z, u.level);
}
| Method | When to call |
|---|---|
place_light(access, pos, level) | Light-emitting block placed |
remove_light(access, pos) | Light-emitting block broken |
block_placed(access, pos) | Opaque block placed (may cut light paths) |
block_removed(access, pos) | Opaque block broken (light may flow in) |
recalculate_area(access, center, radius) | Clear and re-propagate all emitters in a cubic radius |
Free functions (propagate, remove) are available if you don't need
LightEngine's buffer reuse.
| Feature | Effect |
|---|---|
std (default) | Uses ahash for the visited map (~2x faster than BTreeMap) |
sky | Column-first sky light propagation with horizontal BFS spread |
colored | Per-channel RGB light (3 channels, single BFS pass) |
Core is no_std compatible (requires alloc). Without std, the visited map
uses BTreeMap.
Open-air world (no obstructions, worst-case BFS expansion). AMD Ryzen 9 7900, 64 GB RAM, Windows 11:
| Operation | Level 7 | Level 10 | Level 14 |
|---|---|---|---|
| Propagation | 17 µs | 60 µs | 174 µs |
| Removal (single source) | 105 µs | — | 432 µs |
| Removal (neighboring source) | — | — | 463 µs |
| Full place + remove cycle | — | — | 585 µs |
A level-14 torch touches ~11,500 voxels. Benchmarks use criterion: cargo bench.
Propagation. BFS from source, decaying by 1 per block. Stops at opaque
voxels (transparent: false) and world boundaries (None from get_voxel).
Visited map prevents re-processing.
Removal phase 1. BFS from removed source. For each neighbor: if light < node's old level, it was dependent — zero it and enqueue. If light >= old level, it has independent light — save as boundary source.
Removal phase 2. Build an overlay that returns zeroed values for all positions cleared in phase 1 (without requiring the caller to apply them first). BFS re-propagate from all boundary sources against this overlay.
Output. Flat Vec<LightUpdate>. Zeroing updates appear first, then
re-propagation values. Apply in order.
The algorithm is standard (Seed of Andromeda documented it in 2015). These projects implement it by hand:
voxelalgorithms.cpp, ~500 LOC, C++No existing crate on crates.io provides this. block-mesh handles meshing.
building-blocks handles storage. tapestry is 2D. pathfinding solves
graph traversal without light-decay or removal semantics.
Not a spatial data structure. Queries your storage via the trait and returns updates. Does not store light values.
Visited map allocated per call. The AHashMap is not pooled across
calls. Typical cost: ~11k entries for level-14 propagation.
Colored removal over-zeroes. A voxel is considered dependent if any channel is less than the corresponding old level. Edge cases with overlapping colored sources of different ranges may zero more than necessary.
No sky light removal. The sky feature provides propagation only.
Placing a roof requires the caller to recalculate affected columns.
6 commits
Rust
100.0%