Trait-generic 3D BFS light propagation for voxel engines
BFS 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.
Install
cargo add voxel-light
Usage
Implement the trait for your storage:
use ;
Call the algorithm and apply results:
use LightEngine;
let mut engine = new;
// Place a torch (level 14)
let updates = engine.place_light;
for u in &updates
// Remove it — two-phase removal zeroes dependents, then re-propagates
// from any neighboring sources that still exist
let updates = engine.remove_light;
for u in &updates
API
| 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.
Features
| 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.
Performance
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.
Algorithm
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.
Prior art
The algorithm is standard (Seed of Andromeda documented it in 2015). These projects implement it by hand:
- Luanti/Minetest
—
voxelalgorithms.cpp, ~500 LOC, C++ - Seed of Andromeda — three BFS passes per RGB channel, C++
- PocketMine-MP — standalone specification document for the algorithm
- Rust voxel engines (Rezcraft, Aern-do/voxel, Technici4n/voxel-rs) — each contains a bespoke implementation
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.
Known limitations
-
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
AHashMapis 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
skyfeature provides propagation only. Placing a roof requires the caller to recalculate affected columns.