Skip to main content

Grid

Struct Grid 

Source
pub struct Grid {
Show 13 fields pub transform: GridTransform, pub chunks: HashMap<IVec3, Vxl>, pub render_sky: bool, pub mip_levels_override: Option<u32>, pub lod_thresholds: LodThresholds, pub billboards: Option<BillboardCache>, pub generator: Option<Arc<dyn ChunkGenerator>>, pub name: Option<String>, pub store: Option<Arc<dyn ChunkStore>>, pub stream_radius: StreamRadius, pub bake_lights: Vec<BakeLight>, pub pending_gen: HashSet<IVec3>, pub dda_brick_cache: BrickCache, /* private fields */
}
Expand description

One independent voxel grid in a scene. Holds its world placement and a sparse map of populated chunks. Empty chunk slots are implicit air and skipped during rendering / raycasts.

Each chunk is internally a Vxl with vsid = CHUNK_SIZE_XY — the existing per-chunk renderer (opticast + grouscan + sprites + lighting in roxlap-core) runs on each chunk unchanged. Vertical worlds are built by stacking chunks along grid-local +z.

Fields§

§transform: GridTransform

World placement (origin + rotation).

§chunks: HashMap<IVec3, Vxl>

Sparse chunk storage keyed by (chx, chy, chz) chunk coordinates. A missing entry means the chunk is fully air.

§render_sky: bool

Whether sky pixels rendered for this grid should be composited into the final framebuffer. true is the historical “grid owns its own sky” behaviour: ray misses inside this grid’s frustum paint sky_color into the temp buffer. Set false for grids that are a foreground object (e.g. a ship) — the sky is owned by a single “world” grid (the ground) and other grids should not contribute sky pixels, otherwise their grid-local-frame sky lookup rotates with the grid and visibly fights the world’s sky during compose. See crate::render::render_scene_composed for the masking implementation.

§mip_levels_override: Option<u32>

Override roxlap_core::opticast::OpticastSettings::mip_levels for this grid. None ⇒ use the caller’s value. Some(n) ⇒ cap at n (clamped to [1, settings.mip_levels]). Use to disable multi-mip on a per-grid basis — small grids (rotating ships, billboards) don’t benefit from deep mips and CAN trigger the [[project_axis_aligned_mip_beams]]-style cf-cancellation artifact when near-axis-aligned rays hit the rotated grid. Some(1) = mip-0 only, byte-stable to single-mip.

§lod_thresholds: LodThresholds

World-distance thresholds for per-grid LOD tier selection (S6.0). Defaults to LodThresholds::always_near, so a freshly-constructed grid always renders at full voxel (the S5-and-earlier byte-stable behaviour). S6.1 plugs Mid into the existing multi-mip path; S6.3 plugs Far into the billboard impostor cache. See crate::lod.

§billboards: Option<BillboardCache>

Lazy BillboardCache for the Lod::Far tier (S6.2). None until the first time S6.3’s render dispatch needs it; populated then via BillboardCache::build and cleared by edits (Self::set_voxel / Self::set_rect / Self::set_sphere) to force a rebuild on next Far use. Callers may also force-invalidate via direct assignment.

§generator: Option<Arc<dyn ChunkGenerator>>

Optional procedural generator (S7.0). When set, Self::ensure_chunk_generated uses it to materialise chunks that are still absent from Self::chunks.

Streaming layers (S7.1+) walk the active radius around the camera and call ensure_chunk_generated for missing chunks; later stages dispatch this onto a background rayon pool. The trait bound is Send + Sync (needed for S7.3 async dispatch) + Debug (needed so Grid keeps deriving Debug).

None is the default — a grid without a generator behaves exactly like the pre-S7 grids: absent chunks stay absent.

Arc (not Box) so S7.3’s async dispatch can clone the generator into background rayon tasks without moving it out of the grid. Trait bound Send + Sync (required at S7.0) already makes Arc<dyn ChunkGenerator> Send + Sync.

§name: Option<String>

QE.5b - optional host-assigned tag, carried through snapshots. Grid ids are runtime-opaque, so a save/load cycle gives hosts nothing to rebind their own per-grid data (generators, stores, gameplay state) against; a stable name closes that gap. Not interpreted by the engine.

§store: Option<Arc<dyn ChunkStore>>

QE.5a - optional persistence for edited streamed chunks: the eviction pass hands every chunk_version != 0 chunk to ChunkStore::store before dropping it, and stream-in asks ChunkStore::load before running the generator. None (the default) keeps the pre-QE.5 behaviour: evicting an edited chunk discards the edits.

§stream_radius: StreamRadius

Streaming activity / eviction radii used by Scene::pump_streaming_sync (S7.1). Defaults to StreamRadius::DISABLED so existing grids see no change in behaviour until the caller opts in.

§bake_lights: Vec<BakeLight>

EV.3 — baked point lights (BakeLight, grid-local voxel coords) consumed by BakeMode::PointLights: Grid::bake and Grid::bake_bbox write each light’s Lambertian pool into the brightness bytes, so incremental carve relights keep their glow. Authoring state only — editing this list does not rebake by itself (call Grid::bake after) and it is not carried through snapshots (the baked bytes are; re-set the list after a load if you keep editing). Ignored by the other bake modes and by the dynamic LightRig.

§pending_gen: HashSet<IVec3>

In-flight background generation tasks (S7.3).

Populated by Scene::pump_streaming when it dispatches a generator call onto the streaming rayon pool, drained when the corresponding ChunkResult is received and processed (either installed or discarded). The set is consulted to avoid re-dispatching the same chunk while a previous task is still running.

Stays empty when only the synchronous Scene::pump_streaming_sync is used — that path generates inline on the calling thread.

§dda_brick_cache: BrickCache

Cross-frame DDA brick-occupancy cache (Substage DDA.7 perf). Keyed by (chunk, mip) + the chunk’s edit version, so a static chunk’s brick map is built once and reused every frame. Skipped entirely on the voxlap render path. Not serialised.

Implementations§

Source§

impl Grid

Source

pub fn voxel_solid(&self, voxel: IVec3) -> bool

True if the grid-local integer voxel voxel is solid (inside a solid run of its chunk). An implicit-air or absent chunk reads as false. voxel is a grid-local voxel coordinate (pre-transform) — get one from a world point via crate::world_to_grid_local + crate::voxel_global. Useful for picking, collision, and world queries.

Examples found in repository?
examples/book_scene_graph.rs (line 115)
83fn main() {
84    // ANCHOR: scene_grids
85    let mut scene = Scene::new();
86    // A static "world" grid at the origin…
87    let ground_id = scene.add_grid(GridTransform::at(DVec3::ZERO));
88    // …and a second grid placed and rotated independently — its own
89    // f64 world position + quaternion, same chunked voxel payload.
90    let ship_id = scene.add_grid(GridTransform {
91        origin: DVec3::new(300.0, -80.0, -40.0),
92        rotation: DQuat::from_rotation_z(0.5),
93    });
94    // Object grids should not paint their own (grid-local, rotating)
95    // sky — leave the sky to the world grid.
96    scene.grid_mut(ship_id).expect("just added").render_sky = false;
97    // ANCHOR_END: scene_grids
98
99    // ANCHOR: edits
100    let ground = scene.grid_mut(ground_id).expect("just added");
101    // Solid slab: grid-local voxel coords, inclusive on both ends,
102    // decomposed across as many chunks as it spans (here 4×4).
103    ground.set_rect(
104        IVec3::new(-200, -200, 200),
105        IVec3::new(199, 199, 254),
106        Some(GRASS),
107    );
108    // `Some(colour)` inserts, `None` carves back to air.
109    ground.set_sphere(IVec3::new(0, 0, 195), 12, Some(STONE));
110    ground.set_rect(IVec3::new(-3, -200, 190), IVec3::new(3, 199, 196), None);
111    // ANCHOR_END: edits
112
113    // ANCHOR: queries
114    // Solid tests: inside the slab, then inside the carved tunnel.
115    assert!(ground.voxel_solid(IVec3::new(0, 0, 210)));
116    assert!(!ground.voxel_solid(IVec3::new(0, 100, 193)));
117    // Colour queries answer on surface voxels; deep interior cells
118    // are untextured in the slab format and read as `None`.
119    assert_eq!(ground.voxel_color(IVec3::new(50, 50, 200)), Some(GRASS));
120    // ANCHOR_END: queries
121
122    // ANCHOR: recolour
123    // GOTCHA: inserting over already-solid voxels does NOT repaint
124    // them — span insertion only fills air. Recolour = carve, then
125    // insert.
126    let (lo, hi) = (IVec3::new(20, 20, 200), IVec3::new(24, 24, 204));
127    ground.set_rect(lo, hi, Some(RED));
128    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(GRASS)); // unchanged!
129    ground.set_rect(lo, hi, None); // carve…
130    ground.set_rect(lo, hi, Some(RED)); // …then insert
131    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
132    // ANCHOR_END: recolour
133
134    // ANCHOR: colfunc
135    // A carve exposes fresh interior walls; the plain edits paint
136    // them colour 0 (black). The `_with_colfunc` variants ask a
137    // closure instead — it sees grid-local coordinates, so gradients
138    // stay continuous across chunk seams. Here: a crater whose walls
139    // darken with depth.
140    ground.set_sphere_with_colfunc(IVec3::new(60, 60, 200), 8, SpanOp::Carve, |_x, _y, z| {
141        let depth = (z - 192).clamp(0, 15) as u8;
142        VoxColor::rgb(0x6b, 0x40 + depth * 3, 0x2f)
143    });
144    // ANCHOR_END: colfunc
145
146    // ANCHOR: snapshot
147    // Name grids before saving: ids survive the round-trip, but the
148    // generator / store hooks are host code and cannot be serialised
149    // — the name is your key for rebinding them after a load.
150    scene.grid_mut(ground_id).expect("ground exists").name = Some("ground".into());
151    let bytes = scene.save_snapshot(); // versioned envelope
152    let restored = Scene::load_snapshot(&bytes).expect("self-authored snapshot");
153    assert_eq!(restored.grid_count(), 2);
154    let (_, ground2) = restored
155        .grids()
156        .find(|(_, g)| g.name.as_deref() == Some("ground"))
157        .expect("rebind by name");
158    assert_eq!(ground2.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
159    // ANCHOR_END: snapshot
160
161    // ANCHOR: streaming
162    // Streaming: attach a generator + radii to a grid, then pump once
163    // per frame with the camera's world position. Chunks within
164    // `r_active` stream in; chunks beyond `r_evict` drop out; the band
165    // between is hysteresis so a camera hovering on a boundary
166    // doesn't thrash.
167    let stream_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 4096.0, 0.0)));
168    let store = Arc::new(MemoryStore::default());
169    let grid = scene.grid_mut(stream_id).expect("just added");
170    grid.set_generator(Some(Arc::new(FloorGenerator)));
171    grid.set_chunk_store(Some(store));
172    grid.stream_radius = StreamRadius::new(256.0, 384.0);
173
174    let camera_near = DVec3::new(0.0, 4096.0, 100.0);
175    scene.pump_streaming_sync(camera_near); // real games: `pump_streaming`
176    let grid = scene.grid(stream_id).expect("still there");
177    assert!(grid.chunk_count() > 0); // floor streamed in around the camera
178    assert!(grid.voxel_solid(IVec3::new(5, 5, 240)));
179    // ANCHOR_END: streaming
180
181    // ANCHOR: streaming_edit
182    // Edit the streamed floor, walk far away, come back. The active
183    // set follows the camera: the old region evicts (edited chunks
184    // are handed to the ChunkStore first) while a fresh set streams
185    // in around the new position. On return the store wins over the
186    // generator, so the edit survives; without a store, eviction
187    // would silently revert the chunk to generator output.
188    scene
189        .grid_mut(stream_id)
190        .expect("still there")
191        .set_voxel(IVec3::new(5, 5, 240), None); // dig a hole
192    scene.pump_streaming_sync(camera_near + DVec3::new(0.0, 100_000.0, 0.0));
193    let grid = scene.grid(stream_id).expect("still there");
194    assert!(grid.chunk(IVec3::ZERO).is_none()); // home chunk evicted…
195    assert!(grid.chunk_count() > 0); // …but the far region streamed in
196    scene.pump_streaming_sync(camera_near);
197    let grid = scene.grid(stream_id).expect("re-streamed");
198    // The hole survived evict + re-stream.
199    assert!(!grid.voxel_solid(IVec3::new(5, 5, 240)));
200    // ANCHOR_END: streaming_edit
201
202    println!("book_scene_graph: all scene-graph assertions hold");
203}
Source

pub fn chunk_voxel_solid(vxl: &Vxl, in_chunk: UVec3) -> bool

AU.0 — the chunk-local half of Self::voxel_solid, for external DDA marches with strong chunk locality: split the voxel with crate::voxel_split, borrow the chunk once via Self::chunk, and test cells against the borrow — one HashMap probe per chunk instead of per voxel. in_chunk is the chunk-local coordinate voxel_split returned.

Source

pub fn voxel_color(&self, voxel: IVec3) -> Option<VoxColor>

Packed BGRA colour of the textured voxel at grid-local voxel, or None for air / untextured cells. Thin wrapper over roxlap_formats::vxl::Vxl::voxel_color after the chunk split — the colour-inspection companion to Self::voxel_solid. Use it to read back what a pick / raycast hit looks like.

Examples found in repository?
examples/book_scene_graph.rs (line 119)
83fn main() {
84    // ANCHOR: scene_grids
85    let mut scene = Scene::new();
86    // A static "world" grid at the origin…
87    let ground_id = scene.add_grid(GridTransform::at(DVec3::ZERO));
88    // …and a second grid placed and rotated independently — its own
89    // f64 world position + quaternion, same chunked voxel payload.
90    let ship_id = scene.add_grid(GridTransform {
91        origin: DVec3::new(300.0, -80.0, -40.0),
92        rotation: DQuat::from_rotation_z(0.5),
93    });
94    // Object grids should not paint their own (grid-local, rotating)
95    // sky — leave the sky to the world grid.
96    scene.grid_mut(ship_id).expect("just added").render_sky = false;
97    // ANCHOR_END: scene_grids
98
99    // ANCHOR: edits
100    let ground = scene.grid_mut(ground_id).expect("just added");
101    // Solid slab: grid-local voxel coords, inclusive on both ends,
102    // decomposed across as many chunks as it spans (here 4×4).
103    ground.set_rect(
104        IVec3::new(-200, -200, 200),
105        IVec3::new(199, 199, 254),
106        Some(GRASS),
107    );
108    // `Some(colour)` inserts, `None` carves back to air.
109    ground.set_sphere(IVec3::new(0, 0, 195), 12, Some(STONE));
110    ground.set_rect(IVec3::new(-3, -200, 190), IVec3::new(3, 199, 196), None);
111    // ANCHOR_END: edits
112
113    // ANCHOR: queries
114    // Solid tests: inside the slab, then inside the carved tunnel.
115    assert!(ground.voxel_solid(IVec3::new(0, 0, 210)));
116    assert!(!ground.voxel_solid(IVec3::new(0, 100, 193)));
117    // Colour queries answer on surface voxels; deep interior cells
118    // are untextured in the slab format and read as `None`.
119    assert_eq!(ground.voxel_color(IVec3::new(50, 50, 200)), Some(GRASS));
120    // ANCHOR_END: queries
121
122    // ANCHOR: recolour
123    // GOTCHA: inserting over already-solid voxels does NOT repaint
124    // them — span insertion only fills air. Recolour = carve, then
125    // insert.
126    let (lo, hi) = (IVec3::new(20, 20, 200), IVec3::new(24, 24, 204));
127    ground.set_rect(lo, hi, Some(RED));
128    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(GRASS)); // unchanged!
129    ground.set_rect(lo, hi, None); // carve…
130    ground.set_rect(lo, hi, Some(RED)); // …then insert
131    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
132    // ANCHOR_END: recolour
133
134    // ANCHOR: colfunc
135    // A carve exposes fresh interior walls; the plain edits paint
136    // them colour 0 (black). The `_with_colfunc` variants ask a
137    // closure instead — it sees grid-local coordinates, so gradients
138    // stay continuous across chunk seams. Here: a crater whose walls
139    // darken with depth.
140    ground.set_sphere_with_colfunc(IVec3::new(60, 60, 200), 8, SpanOp::Carve, |_x, _y, z| {
141        let depth = (z - 192).clamp(0, 15) as u8;
142        VoxColor::rgb(0x6b, 0x40 + depth * 3, 0x2f)
143    });
144    // ANCHOR_END: colfunc
145
146    // ANCHOR: snapshot
147    // Name grids before saving: ids survive the round-trip, but the
148    // generator / store hooks are host code and cannot be serialised
149    // — the name is your key for rebinding them after a load.
150    scene.grid_mut(ground_id).expect("ground exists").name = Some("ground".into());
151    let bytes = scene.save_snapshot(); // versioned envelope
152    let restored = Scene::load_snapshot(&bytes).expect("self-authored snapshot");
153    assert_eq!(restored.grid_count(), 2);
154    let (_, ground2) = restored
155        .grids()
156        .find(|(_, g)| g.name.as_deref() == Some("ground"))
157        .expect("rebind by name");
158    assert_eq!(ground2.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
159    // ANCHOR_END: snapshot
160
161    // ANCHOR: streaming
162    // Streaming: attach a generator + radii to a grid, then pump once
163    // per frame with the camera's world position. Chunks within
164    // `r_active` stream in; chunks beyond `r_evict` drop out; the band
165    // between is hysteresis so a camera hovering on a boundary
166    // doesn't thrash.
167    let stream_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 4096.0, 0.0)));
168    let store = Arc::new(MemoryStore::default());
169    let grid = scene.grid_mut(stream_id).expect("just added");
170    grid.set_generator(Some(Arc::new(FloorGenerator)));
171    grid.set_chunk_store(Some(store));
172    grid.stream_radius = StreamRadius::new(256.0, 384.0);
173
174    let camera_near = DVec3::new(0.0, 4096.0, 100.0);
175    scene.pump_streaming_sync(camera_near); // real games: `pump_streaming`
176    let grid = scene.grid(stream_id).expect("still there");
177    assert!(grid.chunk_count() > 0); // floor streamed in around the camera
178    assert!(grid.voxel_solid(IVec3::new(5, 5, 240)));
179    // ANCHOR_END: streaming
180
181    // ANCHOR: streaming_edit
182    // Edit the streamed floor, walk far away, come back. The active
183    // set follows the camera: the old region evicts (edited chunks
184    // are handed to the ChunkStore first) while a fresh set streams
185    // in around the new position. On return the store wins over the
186    // generator, so the edit survives; without a store, eviction
187    // would silently revert the chunk to generator output.
188    scene
189        .grid_mut(stream_id)
190        .expect("still there")
191        .set_voxel(IVec3::new(5, 5, 240), None); // dig a hole
192    scene.pump_streaming_sync(camera_near + DVec3::new(0.0, 100_000.0, 0.0));
193    let grid = scene.grid(stream_id).expect("still there");
194    assert!(grid.chunk(IVec3::ZERO).is_none()); // home chunk evicted…
195    assert!(grid.chunk_count() > 0); // …but the far region streamed in
196    scene.pump_streaming_sync(camera_near);
197    let grid = scene.grid(stream_id).expect("re-streamed");
198    // The hole survived evict + re-stream.
199    assert!(!grid.voxel_solid(IVec3::new(5, 5, 240)));
200    // ANCHOR_END: streaming_edit
201
202    println!("book_scene_graph: all scene-graph assertions hold");
203}
Source

pub fn chunk(&self, chunk_idx: IVec3) -> Option<&Vxl>

Borrow the chunk at chunk_idx if it has been materialised. None means the chunk is implicitly all-air.

Examples found in repository?
examples/book_scene_graph.rs (line 194)
83fn main() {
84    // ANCHOR: scene_grids
85    let mut scene = Scene::new();
86    // A static "world" grid at the origin…
87    let ground_id = scene.add_grid(GridTransform::at(DVec3::ZERO));
88    // …and a second grid placed and rotated independently — its own
89    // f64 world position + quaternion, same chunked voxel payload.
90    let ship_id = scene.add_grid(GridTransform {
91        origin: DVec3::new(300.0, -80.0, -40.0),
92        rotation: DQuat::from_rotation_z(0.5),
93    });
94    // Object grids should not paint their own (grid-local, rotating)
95    // sky — leave the sky to the world grid.
96    scene.grid_mut(ship_id).expect("just added").render_sky = false;
97    // ANCHOR_END: scene_grids
98
99    // ANCHOR: edits
100    let ground = scene.grid_mut(ground_id).expect("just added");
101    // Solid slab: grid-local voxel coords, inclusive on both ends,
102    // decomposed across as many chunks as it spans (here 4×4).
103    ground.set_rect(
104        IVec3::new(-200, -200, 200),
105        IVec3::new(199, 199, 254),
106        Some(GRASS),
107    );
108    // `Some(colour)` inserts, `None` carves back to air.
109    ground.set_sphere(IVec3::new(0, 0, 195), 12, Some(STONE));
110    ground.set_rect(IVec3::new(-3, -200, 190), IVec3::new(3, 199, 196), None);
111    // ANCHOR_END: edits
112
113    // ANCHOR: queries
114    // Solid tests: inside the slab, then inside the carved tunnel.
115    assert!(ground.voxel_solid(IVec3::new(0, 0, 210)));
116    assert!(!ground.voxel_solid(IVec3::new(0, 100, 193)));
117    // Colour queries answer on surface voxels; deep interior cells
118    // are untextured in the slab format and read as `None`.
119    assert_eq!(ground.voxel_color(IVec3::new(50, 50, 200)), Some(GRASS));
120    // ANCHOR_END: queries
121
122    // ANCHOR: recolour
123    // GOTCHA: inserting over already-solid voxels does NOT repaint
124    // them — span insertion only fills air. Recolour = carve, then
125    // insert.
126    let (lo, hi) = (IVec3::new(20, 20, 200), IVec3::new(24, 24, 204));
127    ground.set_rect(lo, hi, Some(RED));
128    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(GRASS)); // unchanged!
129    ground.set_rect(lo, hi, None); // carve…
130    ground.set_rect(lo, hi, Some(RED)); // …then insert
131    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
132    // ANCHOR_END: recolour
133
134    // ANCHOR: colfunc
135    // A carve exposes fresh interior walls; the plain edits paint
136    // them colour 0 (black). The `_with_colfunc` variants ask a
137    // closure instead — it sees grid-local coordinates, so gradients
138    // stay continuous across chunk seams. Here: a crater whose walls
139    // darken with depth.
140    ground.set_sphere_with_colfunc(IVec3::new(60, 60, 200), 8, SpanOp::Carve, |_x, _y, z| {
141        let depth = (z - 192).clamp(0, 15) as u8;
142        VoxColor::rgb(0x6b, 0x40 + depth * 3, 0x2f)
143    });
144    // ANCHOR_END: colfunc
145
146    // ANCHOR: snapshot
147    // Name grids before saving: ids survive the round-trip, but the
148    // generator / store hooks are host code and cannot be serialised
149    // — the name is your key for rebinding them after a load.
150    scene.grid_mut(ground_id).expect("ground exists").name = Some("ground".into());
151    let bytes = scene.save_snapshot(); // versioned envelope
152    let restored = Scene::load_snapshot(&bytes).expect("self-authored snapshot");
153    assert_eq!(restored.grid_count(), 2);
154    let (_, ground2) = restored
155        .grids()
156        .find(|(_, g)| g.name.as_deref() == Some("ground"))
157        .expect("rebind by name");
158    assert_eq!(ground2.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
159    // ANCHOR_END: snapshot
160
161    // ANCHOR: streaming
162    // Streaming: attach a generator + radii to a grid, then pump once
163    // per frame with the camera's world position. Chunks within
164    // `r_active` stream in; chunks beyond `r_evict` drop out; the band
165    // between is hysteresis so a camera hovering on a boundary
166    // doesn't thrash.
167    let stream_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 4096.0, 0.0)));
168    let store = Arc::new(MemoryStore::default());
169    let grid = scene.grid_mut(stream_id).expect("just added");
170    grid.set_generator(Some(Arc::new(FloorGenerator)));
171    grid.set_chunk_store(Some(store));
172    grid.stream_radius = StreamRadius::new(256.0, 384.0);
173
174    let camera_near = DVec3::new(0.0, 4096.0, 100.0);
175    scene.pump_streaming_sync(camera_near); // real games: `pump_streaming`
176    let grid = scene.grid(stream_id).expect("still there");
177    assert!(grid.chunk_count() > 0); // floor streamed in around the camera
178    assert!(grid.voxel_solid(IVec3::new(5, 5, 240)));
179    // ANCHOR_END: streaming
180
181    // ANCHOR: streaming_edit
182    // Edit the streamed floor, walk far away, come back. The active
183    // set follows the camera: the old region evicts (edited chunks
184    // are handed to the ChunkStore first) while a fresh set streams
185    // in around the new position. On return the store wins over the
186    // generator, so the edit survives; without a store, eviction
187    // would silently revert the chunk to generator output.
188    scene
189        .grid_mut(stream_id)
190        .expect("still there")
191        .set_voxel(IVec3::new(5, 5, 240), None); // dig a hole
192    scene.pump_streaming_sync(camera_near + DVec3::new(0.0, 100_000.0, 0.0));
193    let grid = scene.grid(stream_id).expect("still there");
194    assert!(grid.chunk(IVec3::ZERO).is_none()); // home chunk evicted…
195    assert!(grid.chunk_count() > 0); // …but the far region streamed in
196    scene.pump_streaming_sync(camera_near);
197    let grid = scene.grid(stream_id).expect("re-streamed");
198    // The hole survived evict + re-stream.
199    assert!(!grid.voxel_solid(IVec3::new(5, 5, 240)));
200    // ANCHOR_END: streaming_edit
201
202    println!("book_scene_graph: all scene-graph assertions hold");
203}
Source

pub fn bake(&mut self, mode: BakeMode)

Bake per-voxel lighting (voxlap updatevxl/estnorm shading) into every materialised chunk’s brightness bytes, in place. Both the CPU rasteriser and the GPU marcher read these pre-baked brightness bytes, so call this once after building a grid and again over edited chunks after a carve (then bump their versions so the GPU re-uploads — edits already do, via Grid::set_voxel &c.). For small runtime edits prefer Self::bake_bbox — it re-bakes only the touched columns.

Each chunk is baked neighbour-aware in all three axes: estnorm’s (and AO’s) ±2-voxel padding that crosses a chunk face reads the actual neighbour chunk (when populated) — XY neighbours and, for stacked grids, the chunks above/below on chz±1 — so brightness / occlusion is continuous across every seam. Under BakeMode::PointLights the grid’s Grid::bake_lights are applied (EV.3); the other modes ignore them. No-op for an empty grid.

Source

pub fn bake_lightmode(&mut self, lightmode: u32)

👎Deprecated since 0.23.0:

use bake(BakeMode::..)

QE-B6 — magic u32 lightmode; use Self::bake with a typed BakeMode instead (1BakeMode::Directional, 3BakeMode::AmbientOcclusion(AoParams::default())).

Source

pub fn bake_lightmode_with_ao(&mut self, lightmode: u32, ao: AoParams)

👎Deprecated since 0.23.0:

use bake(BakeMode::AmbientOcclusion(ao))

QE-B6 — use Self::bake with BakeMode::AmbientOcclusion(ao) instead.

Source

pub fn bake_bbox(&mut self, lo: IVec3, hi: IVec3, mode: BakeMode)

PF.11 — re-bake lighting over just the grid-local voxel bbox [lo, hi] (inclusive), neighbour-aware across chunk seams in all three axes: the write region is padded by ±ESTNORMRAD internally (an edit changes the estnorm of nearby voxels too — pass only the geometric edit extent, mirroring update_lighting’s convention), and any chunk the padded region touches gets its strip re-baked. Touched chunks get their versions bumped so the GPU re-uploads.

This is the runtime-edit primitive the full-grid Self::bake_lightmode is far too heavy for: a bullet-hole rebake touches a few hundred columns instead of a whole chunk (the cave demo measured ~0.04 ms vs 4–7 ms). Mip regeneration is NOT performed — near-field renders read mip 0; callers streaming distant edited chunks should remip as they already do for edits.

Source

pub fn bake_lightmode_bbox(&mut self, lo: IVec3, hi: IVec3, lightmode: u32)

👎Deprecated since 0.23.0:

use bake_bbox(lo, hi, BakeMode::..)

QE-B6 — magic u32 lightmode, and this variant silently baked AO with default params. Use Self::bake_bbox with a typed BakeMode (which honours AmbientOcclusion’s params).

Source

pub fn chunk_mut(&mut self, chunk_idx: IVec3) -> Option<&mut Vxl>

Mutably borrow a materialised chunk. Returns None for implicit-air chunks; use Grid::ensure_chunk when you need a &mut Vxl for an edit that may write voxels.

Source

pub fn ensure_chunk(&mut self, chunk_idx: IVec3) -> &mut Vxl

Borrow chunk_idx’s Vxl, creating an empty all-air chunk first if it doesn’t exist yet. The returned &mut is valid for editing via roxlap_formats::edit — the new chunk has Vxl::reserve_edit_capacity already applied.

Source

pub fn chunk_count(&self) -> usize

Number of materialised chunks. Implicit-air chunks don’t count.

Examples found in repository?
examples/book_scene_graph.rs (line 177)
83fn main() {
84    // ANCHOR: scene_grids
85    let mut scene = Scene::new();
86    // A static "world" grid at the origin…
87    let ground_id = scene.add_grid(GridTransform::at(DVec3::ZERO));
88    // …and a second grid placed and rotated independently — its own
89    // f64 world position + quaternion, same chunked voxel payload.
90    let ship_id = scene.add_grid(GridTransform {
91        origin: DVec3::new(300.0, -80.0, -40.0),
92        rotation: DQuat::from_rotation_z(0.5),
93    });
94    // Object grids should not paint their own (grid-local, rotating)
95    // sky — leave the sky to the world grid.
96    scene.grid_mut(ship_id).expect("just added").render_sky = false;
97    // ANCHOR_END: scene_grids
98
99    // ANCHOR: edits
100    let ground = scene.grid_mut(ground_id).expect("just added");
101    // Solid slab: grid-local voxel coords, inclusive on both ends,
102    // decomposed across as many chunks as it spans (here 4×4).
103    ground.set_rect(
104        IVec3::new(-200, -200, 200),
105        IVec3::new(199, 199, 254),
106        Some(GRASS),
107    );
108    // `Some(colour)` inserts, `None` carves back to air.
109    ground.set_sphere(IVec3::new(0, 0, 195), 12, Some(STONE));
110    ground.set_rect(IVec3::new(-3, -200, 190), IVec3::new(3, 199, 196), None);
111    // ANCHOR_END: edits
112
113    // ANCHOR: queries
114    // Solid tests: inside the slab, then inside the carved tunnel.
115    assert!(ground.voxel_solid(IVec3::new(0, 0, 210)));
116    assert!(!ground.voxel_solid(IVec3::new(0, 100, 193)));
117    // Colour queries answer on surface voxels; deep interior cells
118    // are untextured in the slab format and read as `None`.
119    assert_eq!(ground.voxel_color(IVec3::new(50, 50, 200)), Some(GRASS));
120    // ANCHOR_END: queries
121
122    // ANCHOR: recolour
123    // GOTCHA: inserting over already-solid voxels does NOT repaint
124    // them — span insertion only fills air. Recolour = carve, then
125    // insert.
126    let (lo, hi) = (IVec3::new(20, 20, 200), IVec3::new(24, 24, 204));
127    ground.set_rect(lo, hi, Some(RED));
128    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(GRASS)); // unchanged!
129    ground.set_rect(lo, hi, None); // carve…
130    ground.set_rect(lo, hi, Some(RED)); // …then insert
131    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
132    // ANCHOR_END: recolour
133
134    // ANCHOR: colfunc
135    // A carve exposes fresh interior walls; the plain edits paint
136    // them colour 0 (black). The `_with_colfunc` variants ask a
137    // closure instead — it sees grid-local coordinates, so gradients
138    // stay continuous across chunk seams. Here: a crater whose walls
139    // darken with depth.
140    ground.set_sphere_with_colfunc(IVec3::new(60, 60, 200), 8, SpanOp::Carve, |_x, _y, z| {
141        let depth = (z - 192).clamp(0, 15) as u8;
142        VoxColor::rgb(0x6b, 0x40 + depth * 3, 0x2f)
143    });
144    // ANCHOR_END: colfunc
145
146    // ANCHOR: snapshot
147    // Name grids before saving: ids survive the round-trip, but the
148    // generator / store hooks are host code and cannot be serialised
149    // — the name is your key for rebinding them after a load.
150    scene.grid_mut(ground_id).expect("ground exists").name = Some("ground".into());
151    let bytes = scene.save_snapshot(); // versioned envelope
152    let restored = Scene::load_snapshot(&bytes).expect("self-authored snapshot");
153    assert_eq!(restored.grid_count(), 2);
154    let (_, ground2) = restored
155        .grids()
156        .find(|(_, g)| g.name.as_deref() == Some("ground"))
157        .expect("rebind by name");
158    assert_eq!(ground2.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
159    // ANCHOR_END: snapshot
160
161    // ANCHOR: streaming
162    // Streaming: attach a generator + radii to a grid, then pump once
163    // per frame with the camera's world position. Chunks within
164    // `r_active` stream in; chunks beyond `r_evict` drop out; the band
165    // between is hysteresis so a camera hovering on a boundary
166    // doesn't thrash.
167    let stream_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 4096.0, 0.0)));
168    let store = Arc::new(MemoryStore::default());
169    let grid = scene.grid_mut(stream_id).expect("just added");
170    grid.set_generator(Some(Arc::new(FloorGenerator)));
171    grid.set_chunk_store(Some(store));
172    grid.stream_radius = StreamRadius::new(256.0, 384.0);
173
174    let camera_near = DVec3::new(0.0, 4096.0, 100.0);
175    scene.pump_streaming_sync(camera_near); // real games: `pump_streaming`
176    let grid = scene.grid(stream_id).expect("still there");
177    assert!(grid.chunk_count() > 0); // floor streamed in around the camera
178    assert!(grid.voxel_solid(IVec3::new(5, 5, 240)));
179    // ANCHOR_END: streaming
180
181    // ANCHOR: streaming_edit
182    // Edit the streamed floor, walk far away, come back. The active
183    // set follows the camera: the old region evicts (edited chunks
184    // are handed to the ChunkStore first) while a fresh set streams
185    // in around the new position. On return the store wins over the
186    // generator, so the edit survives; without a store, eviction
187    // would silently revert the chunk to generator output.
188    scene
189        .grid_mut(stream_id)
190        .expect("still there")
191        .set_voxel(IVec3::new(5, 5, 240), None); // dig a hole
192    scene.pump_streaming_sync(camera_near + DVec3::new(0.0, 100_000.0, 0.0));
193    let grid = scene.grid(stream_id).expect("still there");
194    assert!(grid.chunk(IVec3::ZERO).is_none()); // home chunk evicted…
195    assert!(grid.chunk_count() > 0); // …but the far region streamed in
196    scene.pump_streaming_sync(camera_near);
197    let grid = scene.grid(stream_id).expect("re-streamed");
198    // The hole survived evict + re-stream.
199    assert!(!grid.voxel_solid(IVec3::new(5, 5, 240)));
200    // ANCHOR_END: streaming_edit
201
202    println!("book_scene_graph: all scene-graph assertions hold");
203}
Source

pub fn chunk_xy_backing(&self) -> Option<ChunkXyBacking<'_>>

S4B.2.c.3: build a per-chunk roxlap_core::GridView table over this grid’s XY chunk footprint at chz = 0.

Returns None if no chz=0 chunk is populated (the entire grid would render as implicit air anyway).

Iterates chunks once to find the chx/chy bounding box, then a second time to fill the row-major Vec<Option<GridView<'_>>>. Empty XY slots (implicit-air chunks inside the box) get None.

Pair with roxlap_core::ChunkGrid + [roxlap_core:: GridView::from_chunk_grid] to drive the Approach B render path:

let backing = grid.chunk_xy_backing().unwrap();
let cg = roxlap_core::ChunkGrid {
    chunks: &backing.chunks,
    origin_chunk_xy: backing.origin_chunk_xy,
    chunks_x: backing.chunks_x,
    chunks_y: backing.chunks_y,
};
let view = roxlap_core::GridView::from_chunk_grid(
    &cg, crate::CHUNK_SIZE_XY,
);

Only chz=0 chunks contribute (multi-z handoff lands in S4B.3); higher-chz chunks in Self::chunks are ignored here.

Source

pub fn chunk_xyz_backing(&self) -> Option<ChunkXyBacking<'_>>

S4B.6.a: 3D-aware version of Self::chunk_xy_backing. Enumerates ALL chunks across the chx/chy/chz bounding box (not just chz=0) so a stacked grid can be rendered once S4B.6.c switches the rasterizer to a chunk-z-aware column walker.

Iterates chunks once for the XYZ bbox, then a second time to fill the row-major-per-z Vec<Option<GridView<'_>>>. Index layout: [(dz * chunks_y + dy) * chunks_x + dx] — matches roxlap_core::ChunkGrid’s indexing exactly.

Returns None for empty grids.

Source§

impl Grid

Source

pub fn set_voxel(&mut self, voxel: IVec3, color: Option<VoxColor>)

Set or carve a single voxel at grid-local coordinate voxel. color = Some(c) inserts a solid voxel of colour c; color = None carves to air.

Inserting in an implicit-air chunk materialises that chunk (allocates a fresh Vxl); carving from a missing chunk is a no-op.

Examples found in repository?
examples/book_scene_graph.rs (line 191)
83fn main() {
84    // ANCHOR: scene_grids
85    let mut scene = Scene::new();
86    // A static "world" grid at the origin…
87    let ground_id = scene.add_grid(GridTransform::at(DVec3::ZERO));
88    // …and a second grid placed and rotated independently — its own
89    // f64 world position + quaternion, same chunked voxel payload.
90    let ship_id = scene.add_grid(GridTransform {
91        origin: DVec3::new(300.0, -80.0, -40.0),
92        rotation: DQuat::from_rotation_z(0.5),
93    });
94    // Object grids should not paint their own (grid-local, rotating)
95    // sky — leave the sky to the world grid.
96    scene.grid_mut(ship_id).expect("just added").render_sky = false;
97    // ANCHOR_END: scene_grids
98
99    // ANCHOR: edits
100    let ground = scene.grid_mut(ground_id).expect("just added");
101    // Solid slab: grid-local voxel coords, inclusive on both ends,
102    // decomposed across as many chunks as it spans (here 4×4).
103    ground.set_rect(
104        IVec3::new(-200, -200, 200),
105        IVec3::new(199, 199, 254),
106        Some(GRASS),
107    );
108    // `Some(colour)` inserts, `None` carves back to air.
109    ground.set_sphere(IVec3::new(0, 0, 195), 12, Some(STONE));
110    ground.set_rect(IVec3::new(-3, -200, 190), IVec3::new(3, 199, 196), None);
111    // ANCHOR_END: edits
112
113    // ANCHOR: queries
114    // Solid tests: inside the slab, then inside the carved tunnel.
115    assert!(ground.voxel_solid(IVec3::new(0, 0, 210)));
116    assert!(!ground.voxel_solid(IVec3::new(0, 100, 193)));
117    // Colour queries answer on surface voxels; deep interior cells
118    // are untextured in the slab format and read as `None`.
119    assert_eq!(ground.voxel_color(IVec3::new(50, 50, 200)), Some(GRASS));
120    // ANCHOR_END: queries
121
122    // ANCHOR: recolour
123    // GOTCHA: inserting over already-solid voxels does NOT repaint
124    // them — span insertion only fills air. Recolour = carve, then
125    // insert.
126    let (lo, hi) = (IVec3::new(20, 20, 200), IVec3::new(24, 24, 204));
127    ground.set_rect(lo, hi, Some(RED));
128    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(GRASS)); // unchanged!
129    ground.set_rect(lo, hi, None); // carve…
130    ground.set_rect(lo, hi, Some(RED)); // …then insert
131    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
132    // ANCHOR_END: recolour
133
134    // ANCHOR: colfunc
135    // A carve exposes fresh interior walls; the plain edits paint
136    // them colour 0 (black). The `_with_colfunc` variants ask a
137    // closure instead — it sees grid-local coordinates, so gradients
138    // stay continuous across chunk seams. Here: a crater whose walls
139    // darken with depth.
140    ground.set_sphere_with_colfunc(IVec3::new(60, 60, 200), 8, SpanOp::Carve, |_x, _y, z| {
141        let depth = (z - 192).clamp(0, 15) as u8;
142        VoxColor::rgb(0x6b, 0x40 + depth * 3, 0x2f)
143    });
144    // ANCHOR_END: colfunc
145
146    // ANCHOR: snapshot
147    // Name grids before saving: ids survive the round-trip, but the
148    // generator / store hooks are host code and cannot be serialised
149    // — the name is your key for rebinding them after a load.
150    scene.grid_mut(ground_id).expect("ground exists").name = Some("ground".into());
151    let bytes = scene.save_snapshot(); // versioned envelope
152    let restored = Scene::load_snapshot(&bytes).expect("self-authored snapshot");
153    assert_eq!(restored.grid_count(), 2);
154    let (_, ground2) = restored
155        .grids()
156        .find(|(_, g)| g.name.as_deref() == Some("ground"))
157        .expect("rebind by name");
158    assert_eq!(ground2.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
159    // ANCHOR_END: snapshot
160
161    // ANCHOR: streaming
162    // Streaming: attach a generator + radii to a grid, then pump once
163    // per frame with the camera's world position. Chunks within
164    // `r_active` stream in; chunks beyond `r_evict` drop out; the band
165    // between is hysteresis so a camera hovering on a boundary
166    // doesn't thrash.
167    let stream_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 4096.0, 0.0)));
168    let store = Arc::new(MemoryStore::default());
169    let grid = scene.grid_mut(stream_id).expect("just added");
170    grid.set_generator(Some(Arc::new(FloorGenerator)));
171    grid.set_chunk_store(Some(store));
172    grid.stream_radius = StreamRadius::new(256.0, 384.0);
173
174    let camera_near = DVec3::new(0.0, 4096.0, 100.0);
175    scene.pump_streaming_sync(camera_near); // real games: `pump_streaming`
176    let grid = scene.grid(stream_id).expect("still there");
177    assert!(grid.chunk_count() > 0); // floor streamed in around the camera
178    assert!(grid.voxel_solid(IVec3::new(5, 5, 240)));
179    // ANCHOR_END: streaming
180
181    // ANCHOR: streaming_edit
182    // Edit the streamed floor, walk far away, come back. The active
183    // set follows the camera: the old region evicts (edited chunks
184    // are handed to the ChunkStore first) while a fresh set streams
185    // in around the new position. On return the store wins over the
186    // generator, so the edit survives; without a store, eviction
187    // would silently revert the chunk to generator output.
188    scene
189        .grid_mut(stream_id)
190        .expect("still there")
191        .set_voxel(IVec3::new(5, 5, 240), None); // dig a hole
192    scene.pump_streaming_sync(camera_near + DVec3::new(0.0, 100_000.0, 0.0));
193    let grid = scene.grid(stream_id).expect("still there");
194    assert!(grid.chunk(IVec3::ZERO).is_none()); // home chunk evicted…
195    assert!(grid.chunk_count() > 0); // …but the far region streamed in
196    scene.pump_streaming_sync(camera_near);
197    let grid = scene.grid(stream_id).expect("re-streamed");
198    // The hole survived evict + re-stream.
199    assert!(!grid.voxel_solid(IVec3::new(5, 5, 240)));
200    // ANCHOR_END: streaming_edit
201
202    println!("book_scene_graph: all scene-graph assertions hold");
203}
Source

pub fn set_rect(&mut self, lo: IVec3, hi: IVec3, color: Option<VoxColor>)

Set or carve an axis-aligned box [lo, hi] in grid-local voxel coordinates. Inclusive on both ends, like roxlap_formats::edit::set_rect.

The box is decomposed per chunk: each touched chunk receives a set_rect call with the box clipped to its footprint and translated to chunk-local. Inserts materialise missing chunks; carves skip them.

lo and hi may be in any order on each axis — the decomposition normalises them.

Examples found in repository?
examples/book_controller.rs (lines 35-39)
29fn build_world() -> Scene {
30    let mut scene = Scene::new();
31    let id = scene.add_grid(GridTransform::identity());
32    let g = scene.grid_mut(id).expect("grid");
33    // Ground slab: surface plane at z = 100 (+z is down, so the
34    // slab fills downward).
35    g.set_rect(
36        IVec3::new(20, 20, 100),
37        IVec3::new(140, 140, 120),
38        Some(VoxColor::rgb(0x4d, 0x8a, 0x3a)),
39    );
40    // A 1-voxel ledge to step onto, and a wall to slide along.
41    g.set_rect(
42        IVec3::new(90, 20, 99),
43        IVec3::new(140, 140, 99),
44        Some(VoxColor::rgb(0x77, 0x66, 0x55)),
45    );
46    g.set_rect(
47        IVec3::new(60, 70, 60),
48        IVec3::new(62, 140, 99),
49        Some(VoxColor::rgb(0x88, 0x44, 0x44)),
50    );
51    // A water curtain the veto lets the body wade through. Two
52    // format facts (both pinned as engine tests): pass-through
53    // geometry works up to 2 voxels thick — thicker walls grow a
54    // colourless UnexposedSolid core no veto can classify — and
55    // must sit ≥ 1 voxel inside the chunk (edge voxels lose their
56    // side colours).
57    g.set_rect(IVec3::new(40, 40, 80), IVec3::new(41, 60, 99), Some(WATER));
58    scene
59}
More examples
Hide additional examples
examples/book_scene_graph.rs (lines 103-107)
83fn main() {
84    // ANCHOR: scene_grids
85    let mut scene = Scene::new();
86    // A static "world" grid at the origin…
87    let ground_id = scene.add_grid(GridTransform::at(DVec3::ZERO));
88    // …and a second grid placed and rotated independently — its own
89    // f64 world position + quaternion, same chunked voxel payload.
90    let ship_id = scene.add_grid(GridTransform {
91        origin: DVec3::new(300.0, -80.0, -40.0),
92        rotation: DQuat::from_rotation_z(0.5),
93    });
94    // Object grids should not paint their own (grid-local, rotating)
95    // sky — leave the sky to the world grid.
96    scene.grid_mut(ship_id).expect("just added").render_sky = false;
97    // ANCHOR_END: scene_grids
98
99    // ANCHOR: edits
100    let ground = scene.grid_mut(ground_id).expect("just added");
101    // Solid slab: grid-local voxel coords, inclusive on both ends,
102    // decomposed across as many chunks as it spans (here 4×4).
103    ground.set_rect(
104        IVec3::new(-200, -200, 200),
105        IVec3::new(199, 199, 254),
106        Some(GRASS),
107    );
108    // `Some(colour)` inserts, `None` carves back to air.
109    ground.set_sphere(IVec3::new(0, 0, 195), 12, Some(STONE));
110    ground.set_rect(IVec3::new(-3, -200, 190), IVec3::new(3, 199, 196), None);
111    // ANCHOR_END: edits
112
113    // ANCHOR: queries
114    // Solid tests: inside the slab, then inside the carved tunnel.
115    assert!(ground.voxel_solid(IVec3::new(0, 0, 210)));
116    assert!(!ground.voxel_solid(IVec3::new(0, 100, 193)));
117    // Colour queries answer on surface voxels; deep interior cells
118    // are untextured in the slab format and read as `None`.
119    assert_eq!(ground.voxel_color(IVec3::new(50, 50, 200)), Some(GRASS));
120    // ANCHOR_END: queries
121
122    // ANCHOR: recolour
123    // GOTCHA: inserting over already-solid voxels does NOT repaint
124    // them — span insertion only fills air. Recolour = carve, then
125    // insert.
126    let (lo, hi) = (IVec3::new(20, 20, 200), IVec3::new(24, 24, 204));
127    ground.set_rect(lo, hi, Some(RED));
128    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(GRASS)); // unchanged!
129    ground.set_rect(lo, hi, None); // carve…
130    ground.set_rect(lo, hi, Some(RED)); // …then insert
131    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
132    // ANCHOR_END: recolour
133
134    // ANCHOR: colfunc
135    // A carve exposes fresh interior walls; the plain edits paint
136    // them colour 0 (black). The `_with_colfunc` variants ask a
137    // closure instead — it sees grid-local coordinates, so gradients
138    // stay continuous across chunk seams. Here: a crater whose walls
139    // darken with depth.
140    ground.set_sphere_with_colfunc(IVec3::new(60, 60, 200), 8, SpanOp::Carve, |_x, _y, z| {
141        let depth = (z - 192).clamp(0, 15) as u8;
142        VoxColor::rgb(0x6b, 0x40 + depth * 3, 0x2f)
143    });
144    // ANCHOR_END: colfunc
145
146    // ANCHOR: snapshot
147    // Name grids before saving: ids survive the round-trip, but the
148    // generator / store hooks are host code and cannot be serialised
149    // — the name is your key for rebinding them after a load.
150    scene.grid_mut(ground_id).expect("ground exists").name = Some("ground".into());
151    let bytes = scene.save_snapshot(); // versioned envelope
152    let restored = Scene::load_snapshot(&bytes).expect("self-authored snapshot");
153    assert_eq!(restored.grid_count(), 2);
154    let (_, ground2) = restored
155        .grids()
156        .find(|(_, g)| g.name.as_deref() == Some("ground"))
157        .expect("rebind by name");
158    assert_eq!(ground2.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
159    // ANCHOR_END: snapshot
160
161    // ANCHOR: streaming
162    // Streaming: attach a generator + radii to a grid, then pump once
163    // per frame with the camera's world position. Chunks within
164    // `r_active` stream in; chunks beyond `r_evict` drop out; the band
165    // between is hysteresis so a camera hovering on a boundary
166    // doesn't thrash.
167    let stream_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 4096.0, 0.0)));
168    let store = Arc::new(MemoryStore::default());
169    let grid = scene.grid_mut(stream_id).expect("just added");
170    grid.set_generator(Some(Arc::new(FloorGenerator)));
171    grid.set_chunk_store(Some(store));
172    grid.stream_radius = StreamRadius::new(256.0, 384.0);
173
174    let camera_near = DVec3::new(0.0, 4096.0, 100.0);
175    scene.pump_streaming_sync(camera_near); // real games: `pump_streaming`
176    let grid = scene.grid(stream_id).expect("still there");
177    assert!(grid.chunk_count() > 0); // floor streamed in around the camera
178    assert!(grid.voxel_solid(IVec3::new(5, 5, 240)));
179    // ANCHOR_END: streaming
180
181    // ANCHOR: streaming_edit
182    // Edit the streamed floor, walk far away, come back. The active
183    // set follows the camera: the old region evicts (edited chunks
184    // are handed to the ChunkStore first) while a fresh set streams
185    // in around the new position. On return the store wins over the
186    // generator, so the edit survives; without a store, eviction
187    // would silently revert the chunk to generator output.
188    scene
189        .grid_mut(stream_id)
190        .expect("still there")
191        .set_voxel(IVec3::new(5, 5, 240), None); // dig a hole
192    scene.pump_streaming_sync(camera_near + DVec3::new(0.0, 100_000.0, 0.0));
193    let grid = scene.grid(stream_id).expect("still there");
194    assert!(grid.chunk(IVec3::ZERO).is_none()); // home chunk evicted…
195    assert!(grid.chunk_count() > 0); // …but the far region streamed in
196    scene.pump_streaming_sync(camera_near);
197    let grid = scene.grid(stream_id).expect("re-streamed");
198    // The hole survived evict + re-stream.
199    assert!(!grid.voxel_solid(IVec3::new(5, 5, 240)));
200    // ANCHOR_END: streaming_edit
201
202    println!("book_scene_graph: all scene-graph assertions hold");
203}
Source

pub fn set_sphere( &mut self, centre: IVec3, radius: u32, color: Option<VoxColor>, )

Set or carve a sphere of voxels at grid-local centre centre with the given radius. Euclidean distance, like roxlap_formats::edit::set_sphere.

The bounding box of the sphere is enumerated chunk by chunk; each touched chunk receives a set_sphere call with the centre re-expressed in chunk-local coords (the per-chunk call clips the sphere to the chunk’s footprint internally). Chunks the sphere doesn’t actually reach get materialised only if color.is_some() and they fall within the AABB — the per-chunk set_sphere is a no-op for non-overlapping chunks but the materialisation cost remains. A subsequent pre-pass that filters chunks against radius² could avoid this; out of scope for v1.

Examples found in repository?
examples/book_scene_graph.rs (line 109)
83fn main() {
84    // ANCHOR: scene_grids
85    let mut scene = Scene::new();
86    // A static "world" grid at the origin…
87    let ground_id = scene.add_grid(GridTransform::at(DVec3::ZERO));
88    // …and a second grid placed and rotated independently — its own
89    // f64 world position + quaternion, same chunked voxel payload.
90    let ship_id = scene.add_grid(GridTransform {
91        origin: DVec3::new(300.0, -80.0, -40.0),
92        rotation: DQuat::from_rotation_z(0.5),
93    });
94    // Object grids should not paint their own (grid-local, rotating)
95    // sky — leave the sky to the world grid.
96    scene.grid_mut(ship_id).expect("just added").render_sky = false;
97    // ANCHOR_END: scene_grids
98
99    // ANCHOR: edits
100    let ground = scene.grid_mut(ground_id).expect("just added");
101    // Solid slab: grid-local voxel coords, inclusive on both ends,
102    // decomposed across as many chunks as it spans (here 4×4).
103    ground.set_rect(
104        IVec3::new(-200, -200, 200),
105        IVec3::new(199, 199, 254),
106        Some(GRASS),
107    );
108    // `Some(colour)` inserts, `None` carves back to air.
109    ground.set_sphere(IVec3::new(0, 0, 195), 12, Some(STONE));
110    ground.set_rect(IVec3::new(-3, -200, 190), IVec3::new(3, 199, 196), None);
111    // ANCHOR_END: edits
112
113    // ANCHOR: queries
114    // Solid tests: inside the slab, then inside the carved tunnel.
115    assert!(ground.voxel_solid(IVec3::new(0, 0, 210)));
116    assert!(!ground.voxel_solid(IVec3::new(0, 100, 193)));
117    // Colour queries answer on surface voxels; deep interior cells
118    // are untextured in the slab format and read as `None`.
119    assert_eq!(ground.voxel_color(IVec3::new(50, 50, 200)), Some(GRASS));
120    // ANCHOR_END: queries
121
122    // ANCHOR: recolour
123    // GOTCHA: inserting over already-solid voxels does NOT repaint
124    // them — span insertion only fills air. Recolour = carve, then
125    // insert.
126    let (lo, hi) = (IVec3::new(20, 20, 200), IVec3::new(24, 24, 204));
127    ground.set_rect(lo, hi, Some(RED));
128    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(GRASS)); // unchanged!
129    ground.set_rect(lo, hi, None); // carve…
130    ground.set_rect(lo, hi, Some(RED)); // …then insert
131    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
132    // ANCHOR_END: recolour
133
134    // ANCHOR: colfunc
135    // A carve exposes fresh interior walls; the plain edits paint
136    // them colour 0 (black). The `_with_colfunc` variants ask a
137    // closure instead — it sees grid-local coordinates, so gradients
138    // stay continuous across chunk seams. Here: a crater whose walls
139    // darken with depth.
140    ground.set_sphere_with_colfunc(IVec3::new(60, 60, 200), 8, SpanOp::Carve, |_x, _y, z| {
141        let depth = (z - 192).clamp(0, 15) as u8;
142        VoxColor::rgb(0x6b, 0x40 + depth * 3, 0x2f)
143    });
144    // ANCHOR_END: colfunc
145
146    // ANCHOR: snapshot
147    // Name grids before saving: ids survive the round-trip, but the
148    // generator / store hooks are host code and cannot be serialised
149    // — the name is your key for rebinding them after a load.
150    scene.grid_mut(ground_id).expect("ground exists").name = Some("ground".into());
151    let bytes = scene.save_snapshot(); // versioned envelope
152    let restored = Scene::load_snapshot(&bytes).expect("self-authored snapshot");
153    assert_eq!(restored.grid_count(), 2);
154    let (_, ground2) = restored
155        .grids()
156        .find(|(_, g)| g.name.as_deref() == Some("ground"))
157        .expect("rebind by name");
158    assert_eq!(ground2.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
159    // ANCHOR_END: snapshot
160
161    // ANCHOR: streaming
162    // Streaming: attach a generator + radii to a grid, then pump once
163    // per frame with the camera's world position. Chunks within
164    // `r_active` stream in; chunks beyond `r_evict` drop out; the band
165    // between is hysteresis so a camera hovering on a boundary
166    // doesn't thrash.
167    let stream_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 4096.0, 0.0)));
168    let store = Arc::new(MemoryStore::default());
169    let grid = scene.grid_mut(stream_id).expect("just added");
170    grid.set_generator(Some(Arc::new(FloorGenerator)));
171    grid.set_chunk_store(Some(store));
172    grid.stream_radius = StreamRadius::new(256.0, 384.0);
173
174    let camera_near = DVec3::new(0.0, 4096.0, 100.0);
175    scene.pump_streaming_sync(camera_near); // real games: `pump_streaming`
176    let grid = scene.grid(stream_id).expect("still there");
177    assert!(grid.chunk_count() > 0); // floor streamed in around the camera
178    assert!(grid.voxel_solid(IVec3::new(5, 5, 240)));
179    // ANCHOR_END: streaming
180
181    // ANCHOR: streaming_edit
182    // Edit the streamed floor, walk far away, come back. The active
183    // set follows the camera: the old region evicts (edited chunks
184    // are handed to the ChunkStore first) while a fresh set streams
185    // in around the new position. On return the store wins over the
186    // generator, so the edit survives; without a store, eviction
187    // would silently revert the chunk to generator output.
188    scene
189        .grid_mut(stream_id)
190        .expect("still there")
191        .set_voxel(IVec3::new(5, 5, 240), None); // dig a hole
192    scene.pump_streaming_sync(camera_near + DVec3::new(0.0, 100_000.0, 0.0));
193    let grid = scene.grid(stream_id).expect("still there");
194    assert!(grid.chunk(IVec3::ZERO).is_none()); // home chunk evicted…
195    assert!(grid.chunk_count() > 0); // …but the far region streamed in
196    scene.pump_streaming_sync(camera_near);
197    let grid = scene.grid(stream_id).expect("re-streamed");
198    // The hole survived evict + re-stream.
199    assert!(!grid.voxel_solid(IVec3::new(5, 5, 240)));
200    // ANCHOR_END: streaming_edit
201
202    println!("book_scene_graph: all scene-graph assertions hold");
203}
Source

pub fn set_sphere_with_colfunc<F>( &mut self, centre: IVec3, radius: u32, op: SpanOp, colfunc: F, )
where F: FnMut(i32, i32, i32) -> VoxColor,

Carve or insert a sphere with a per-voxel colour callback — the colfunc counterpart of Grid::set_sphere, forwarding to roxlap_formats::edit::set_sphere_with_colfunc.

Use this (with SpanOp::Carve) to control the colour of the interior surface a carve newly exposes: a plain set_sphere carve paints those walls colour 0 (black), whereas this lets the closure return a crater colour, a depth gradient, jitter, or a texture lookup. With SpanOp::Insert the closure colours the inserted voxels.

colfunc(x, y, z) receives grid-local voxel coordinates (not chunk-local) and returns a voxlap-packed BGRA colour as i32 — the per-chunk decomposition translates coordinates back to grid-local before invoking the closure, so a position- or depth-dependent colour stays continuous across chunk seams.

Like Grid::set_sphere: SpanOp::Insert materialises missing chunks; SpanOp::Carve skips chunks that don’t yet exist (carving implicit air is a no-op).

Examples found in repository?
examples/book_scene_graph.rs (lines 140-143)
83fn main() {
84    // ANCHOR: scene_grids
85    let mut scene = Scene::new();
86    // A static "world" grid at the origin…
87    let ground_id = scene.add_grid(GridTransform::at(DVec3::ZERO));
88    // …and a second grid placed and rotated independently — its own
89    // f64 world position + quaternion, same chunked voxel payload.
90    let ship_id = scene.add_grid(GridTransform {
91        origin: DVec3::new(300.0, -80.0, -40.0),
92        rotation: DQuat::from_rotation_z(0.5),
93    });
94    // Object grids should not paint their own (grid-local, rotating)
95    // sky — leave the sky to the world grid.
96    scene.grid_mut(ship_id).expect("just added").render_sky = false;
97    // ANCHOR_END: scene_grids
98
99    // ANCHOR: edits
100    let ground = scene.grid_mut(ground_id).expect("just added");
101    // Solid slab: grid-local voxel coords, inclusive on both ends,
102    // decomposed across as many chunks as it spans (here 4×4).
103    ground.set_rect(
104        IVec3::new(-200, -200, 200),
105        IVec3::new(199, 199, 254),
106        Some(GRASS),
107    );
108    // `Some(colour)` inserts, `None` carves back to air.
109    ground.set_sphere(IVec3::new(0, 0, 195), 12, Some(STONE));
110    ground.set_rect(IVec3::new(-3, -200, 190), IVec3::new(3, 199, 196), None);
111    // ANCHOR_END: edits
112
113    // ANCHOR: queries
114    // Solid tests: inside the slab, then inside the carved tunnel.
115    assert!(ground.voxel_solid(IVec3::new(0, 0, 210)));
116    assert!(!ground.voxel_solid(IVec3::new(0, 100, 193)));
117    // Colour queries answer on surface voxels; deep interior cells
118    // are untextured in the slab format and read as `None`.
119    assert_eq!(ground.voxel_color(IVec3::new(50, 50, 200)), Some(GRASS));
120    // ANCHOR_END: queries
121
122    // ANCHOR: recolour
123    // GOTCHA: inserting over already-solid voxels does NOT repaint
124    // them — span insertion only fills air. Recolour = carve, then
125    // insert.
126    let (lo, hi) = (IVec3::new(20, 20, 200), IVec3::new(24, 24, 204));
127    ground.set_rect(lo, hi, Some(RED));
128    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(GRASS)); // unchanged!
129    ground.set_rect(lo, hi, None); // carve…
130    ground.set_rect(lo, hi, Some(RED)); // …then insert
131    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
132    // ANCHOR_END: recolour
133
134    // ANCHOR: colfunc
135    // A carve exposes fresh interior walls; the plain edits paint
136    // them colour 0 (black). The `_with_colfunc` variants ask a
137    // closure instead — it sees grid-local coordinates, so gradients
138    // stay continuous across chunk seams. Here: a crater whose walls
139    // darken with depth.
140    ground.set_sphere_with_colfunc(IVec3::new(60, 60, 200), 8, SpanOp::Carve, |_x, _y, z| {
141        let depth = (z - 192).clamp(0, 15) as u8;
142        VoxColor::rgb(0x6b, 0x40 + depth * 3, 0x2f)
143    });
144    // ANCHOR_END: colfunc
145
146    // ANCHOR: snapshot
147    // Name grids before saving: ids survive the round-trip, but the
148    // generator / store hooks are host code and cannot be serialised
149    // — the name is your key for rebinding them after a load.
150    scene.grid_mut(ground_id).expect("ground exists").name = Some("ground".into());
151    let bytes = scene.save_snapshot(); // versioned envelope
152    let restored = Scene::load_snapshot(&bytes).expect("self-authored snapshot");
153    assert_eq!(restored.grid_count(), 2);
154    let (_, ground2) = restored
155        .grids()
156        .find(|(_, g)| g.name.as_deref() == Some("ground"))
157        .expect("rebind by name");
158    assert_eq!(ground2.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
159    // ANCHOR_END: snapshot
160
161    // ANCHOR: streaming
162    // Streaming: attach a generator + radii to a grid, then pump once
163    // per frame with the camera's world position. Chunks within
164    // `r_active` stream in; chunks beyond `r_evict` drop out; the band
165    // between is hysteresis so a camera hovering on a boundary
166    // doesn't thrash.
167    let stream_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 4096.0, 0.0)));
168    let store = Arc::new(MemoryStore::default());
169    let grid = scene.grid_mut(stream_id).expect("just added");
170    grid.set_generator(Some(Arc::new(FloorGenerator)));
171    grid.set_chunk_store(Some(store));
172    grid.stream_radius = StreamRadius::new(256.0, 384.0);
173
174    let camera_near = DVec3::new(0.0, 4096.0, 100.0);
175    scene.pump_streaming_sync(camera_near); // real games: `pump_streaming`
176    let grid = scene.grid(stream_id).expect("still there");
177    assert!(grid.chunk_count() > 0); // floor streamed in around the camera
178    assert!(grid.voxel_solid(IVec3::new(5, 5, 240)));
179    // ANCHOR_END: streaming
180
181    // ANCHOR: streaming_edit
182    // Edit the streamed floor, walk far away, come back. The active
183    // set follows the camera: the old region evicts (edited chunks
184    // are handed to the ChunkStore first) while a fresh set streams
185    // in around the new position. On return the store wins over the
186    // generator, so the edit survives; without a store, eviction
187    // would silently revert the chunk to generator output.
188    scene
189        .grid_mut(stream_id)
190        .expect("still there")
191        .set_voxel(IVec3::new(5, 5, 240), None); // dig a hole
192    scene.pump_streaming_sync(camera_near + DVec3::new(0.0, 100_000.0, 0.0));
193    let grid = scene.grid(stream_id).expect("still there");
194    assert!(grid.chunk(IVec3::ZERO).is_none()); // home chunk evicted…
195    assert!(grid.chunk_count() > 0); // …but the far region streamed in
196    scene.pump_streaming_sync(camera_near);
197    let grid = scene.grid(stream_id).expect("re-streamed");
198    // The hole survived evict + re-stream.
199    assert!(!grid.voxel_solid(IVec3::new(5, 5, 240)));
200    // ANCHOR_END: streaming_edit
201
202    println!("book_scene_graph: all scene-graph assertions hold");
203}
Source

pub fn set_rect_with_colfunc<F>( &mut self, lo: IVec3, hi: IVec3, op: SpanOp, colfunc: F, )
where F: FnMut(i32, i32, i32) -> VoxColor,

Carve or insert an axis-aligned box [lo, hi] (inclusive) with a per-voxel colour callback — the colfunc counterpart of Grid::set_rect, forwarding to roxlap_formats::edit::set_rect_with_colfunc. See Grid::set_sphere_with_colfunc for the coordinate and chunk-materialisation contract; colfunc likewise receives grid-local coordinates.

Source§

impl Grid

Source

pub fn new(transform: GridTransform) -> Self

New empty grid at the given transform — no chunks populated, render_sky = true, LOD thresholds default to LodThresholds::always_near, no billboard cache.

Source

pub fn ensure_dda_bricks(&mut self, requested_mip: u32) -> u32

Ensure the DDA brick cache holds current mip-requested_mip occupancy maps for every populated chunk, rebuilding only chunks whose edit version changed (Substage DDA.7). Clamps the mip to a level every chunk has built (so coarse rendering never holes) and returns that effective mip. Evicts cache entries for chunks no longer present. Call once per frame before the DDA render.

Source

pub fn chunk_version(&self, chunk_idx: IVec3) -> u64

Current per-chunk edit version (S7.2). Returns 0 for any chunk that hasn’t been edited yet (including absent chunks and chunks materialised only via Self::ensure_chunk_generated).

Used by S7.3’s async generation dispatch to detect “edit happened while we were generating” — the dispatcher snapshots this value, the background task carries it, and the result is discarded on install if the live counter has since moved.

Source

pub fn bump_chunk_version(&mut self, chunk_idx: IVec3)

Bump the edit version of chunk_idx (S7.2). Saturating add at u64::MAX — a chunk would need 10^11 edits per second for ~5 years to wrap, so saturation is a defensive cap, not a realistic concern.

Called by the edit API (Self::set_voxel / Self::set_rect / Self::set_sphere) after a chunk has actually been written to. Pure no-op edit paths (carving from an air chunk that doesn’t exist yet) skip the bump.

Exposed as pub (vs the historical pub(crate)) so hosts that mutate grid.chunks directly — e.g. roxlap-scene-demo’s StreamingBakeTracker writing lightmode-1 alphas via apply_lighting_with_cache — can signal “this chunk’s slab changed” to downstream consumers like the GPU dirty-chunk poller.

Source

pub fn mutation_counter(&self) -> u64

PF.13 (H9) — the grid’s monotonic mutation counter: bumped by every chunk edit, install, and eviction. Per-frame consumers snapshot it to skip their whole-grid scans on quiet frames.

Source

pub fn bump_chunk_version_bbox( &mut self, chunk_idx: IVec3, lo: IVec3, hi: IVec3, )

PF.12 — Self::bump_chunk_version with the edit’s CHUNK-LOCAL voxel extent (inclusive), so partial-refresh consumers (the GPU facade) and incremental re-mip know how little actually changed. Extents accumulate (bbox union) until a consumer Self::take_chunk_dirtys them; an extent-less bump upgrades the entry to DirtyExtent::Full.

Source

pub fn take_chunk_dirty(&mut self, chunk_idx: IVec3) -> Option<DirtyExtent>

PF.12 — take (and clear) the extent accumulated for chunk_idx since the last take. None ⇒ no recorded change (a consumer that still observed a version bump should treat that as DirtyExtent::Full — e.g. a change recorded before the consumer first synced the chunk).

Source

pub fn chunk_versions(&self) -> &HashMap<IVec3, u64>

The full per-chunk edit-version map (QE.3b — the read half of the previously pub field). Consumers seeding a sync tracker iterate this; per-chunk reads go through Self::chunk_version.

Source

pub fn set_generator(&mut self, generator: Option<Arc<dyn ChunkGenerator>>)

Attach (or detach) the procedural generator used by Self::ensure_chunk_generated (S7.0).

Pass Some(Arc::new(generator)) to enable on-demand chunk generation; pass None to revert to the “absent stays absent” behaviour. Replacing an existing generator drops the previous Arc clone without touching already-materialised chunks. Any background tasks dispatched by a prior Scene::pump_streaming hold their own clones of the old generator and finish naturally.

Examples found in repository?
examples/book_scene_graph.rs (line 170)
83fn main() {
84    // ANCHOR: scene_grids
85    let mut scene = Scene::new();
86    // A static "world" grid at the origin…
87    let ground_id = scene.add_grid(GridTransform::at(DVec3::ZERO));
88    // …and a second grid placed and rotated independently — its own
89    // f64 world position + quaternion, same chunked voxel payload.
90    let ship_id = scene.add_grid(GridTransform {
91        origin: DVec3::new(300.0, -80.0, -40.0),
92        rotation: DQuat::from_rotation_z(0.5),
93    });
94    // Object grids should not paint their own (grid-local, rotating)
95    // sky — leave the sky to the world grid.
96    scene.grid_mut(ship_id).expect("just added").render_sky = false;
97    // ANCHOR_END: scene_grids
98
99    // ANCHOR: edits
100    let ground = scene.grid_mut(ground_id).expect("just added");
101    // Solid slab: grid-local voxel coords, inclusive on both ends,
102    // decomposed across as many chunks as it spans (here 4×4).
103    ground.set_rect(
104        IVec3::new(-200, -200, 200),
105        IVec3::new(199, 199, 254),
106        Some(GRASS),
107    );
108    // `Some(colour)` inserts, `None` carves back to air.
109    ground.set_sphere(IVec3::new(0, 0, 195), 12, Some(STONE));
110    ground.set_rect(IVec3::new(-3, -200, 190), IVec3::new(3, 199, 196), None);
111    // ANCHOR_END: edits
112
113    // ANCHOR: queries
114    // Solid tests: inside the slab, then inside the carved tunnel.
115    assert!(ground.voxel_solid(IVec3::new(0, 0, 210)));
116    assert!(!ground.voxel_solid(IVec3::new(0, 100, 193)));
117    // Colour queries answer on surface voxels; deep interior cells
118    // are untextured in the slab format and read as `None`.
119    assert_eq!(ground.voxel_color(IVec3::new(50, 50, 200)), Some(GRASS));
120    // ANCHOR_END: queries
121
122    // ANCHOR: recolour
123    // GOTCHA: inserting over already-solid voxels does NOT repaint
124    // them — span insertion only fills air. Recolour = carve, then
125    // insert.
126    let (lo, hi) = (IVec3::new(20, 20, 200), IVec3::new(24, 24, 204));
127    ground.set_rect(lo, hi, Some(RED));
128    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(GRASS)); // unchanged!
129    ground.set_rect(lo, hi, None); // carve…
130    ground.set_rect(lo, hi, Some(RED)); // …then insert
131    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
132    // ANCHOR_END: recolour
133
134    // ANCHOR: colfunc
135    // A carve exposes fresh interior walls; the plain edits paint
136    // them colour 0 (black). The `_with_colfunc` variants ask a
137    // closure instead — it sees grid-local coordinates, so gradients
138    // stay continuous across chunk seams. Here: a crater whose walls
139    // darken with depth.
140    ground.set_sphere_with_colfunc(IVec3::new(60, 60, 200), 8, SpanOp::Carve, |_x, _y, z| {
141        let depth = (z - 192).clamp(0, 15) as u8;
142        VoxColor::rgb(0x6b, 0x40 + depth * 3, 0x2f)
143    });
144    // ANCHOR_END: colfunc
145
146    // ANCHOR: snapshot
147    // Name grids before saving: ids survive the round-trip, but the
148    // generator / store hooks are host code and cannot be serialised
149    // — the name is your key for rebinding them after a load.
150    scene.grid_mut(ground_id).expect("ground exists").name = Some("ground".into());
151    let bytes = scene.save_snapshot(); // versioned envelope
152    let restored = Scene::load_snapshot(&bytes).expect("self-authored snapshot");
153    assert_eq!(restored.grid_count(), 2);
154    let (_, ground2) = restored
155        .grids()
156        .find(|(_, g)| g.name.as_deref() == Some("ground"))
157        .expect("rebind by name");
158    assert_eq!(ground2.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
159    // ANCHOR_END: snapshot
160
161    // ANCHOR: streaming
162    // Streaming: attach a generator + radii to a grid, then pump once
163    // per frame with the camera's world position. Chunks within
164    // `r_active` stream in; chunks beyond `r_evict` drop out; the band
165    // between is hysteresis so a camera hovering on a boundary
166    // doesn't thrash.
167    let stream_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 4096.0, 0.0)));
168    let store = Arc::new(MemoryStore::default());
169    let grid = scene.grid_mut(stream_id).expect("just added");
170    grid.set_generator(Some(Arc::new(FloorGenerator)));
171    grid.set_chunk_store(Some(store));
172    grid.stream_radius = StreamRadius::new(256.0, 384.0);
173
174    let camera_near = DVec3::new(0.0, 4096.0, 100.0);
175    scene.pump_streaming_sync(camera_near); // real games: `pump_streaming`
176    let grid = scene.grid(stream_id).expect("still there");
177    assert!(grid.chunk_count() > 0); // floor streamed in around the camera
178    assert!(grid.voxel_solid(IVec3::new(5, 5, 240)));
179    // ANCHOR_END: streaming
180
181    // ANCHOR: streaming_edit
182    // Edit the streamed floor, walk far away, come back. The active
183    // set follows the camera: the old region evicts (edited chunks
184    // are handed to the ChunkStore first) while a fresh set streams
185    // in around the new position. On return the store wins over the
186    // generator, so the edit survives; without a store, eviction
187    // would silently revert the chunk to generator output.
188    scene
189        .grid_mut(stream_id)
190        .expect("still there")
191        .set_voxel(IVec3::new(5, 5, 240), None); // dig a hole
192    scene.pump_streaming_sync(camera_near + DVec3::new(0.0, 100_000.0, 0.0));
193    let grid = scene.grid(stream_id).expect("still there");
194    assert!(grid.chunk(IVec3::ZERO).is_none()); // home chunk evicted…
195    assert!(grid.chunk_count() > 0); // …but the far region streamed in
196    scene.pump_streaming_sync(camera_near);
197    let grid = scene.grid(stream_id).expect("re-streamed");
198    // The hole survived evict + re-stream.
199    assert!(!grid.voxel_solid(IVec3::new(5, 5, 240)));
200    // ANCHOR_END: streaming_edit
201
202    println!("book_scene_graph: all scene-graph assertions hold");
203}
Source

pub fn set_chunk_store(&mut self, store: Option<Arc<dyn ChunkStore>>)

Attach (or detach) the persistence store for edited streamed chunks (QE.5a; see ChunkStore). Without one, evicting an edited chunk discards the edits — the pre-QE.5 default.

Examples found in repository?
examples/book_scene_graph.rs (line 171)
83fn main() {
84    // ANCHOR: scene_grids
85    let mut scene = Scene::new();
86    // A static "world" grid at the origin…
87    let ground_id = scene.add_grid(GridTransform::at(DVec3::ZERO));
88    // …and a second grid placed and rotated independently — its own
89    // f64 world position + quaternion, same chunked voxel payload.
90    let ship_id = scene.add_grid(GridTransform {
91        origin: DVec3::new(300.0, -80.0, -40.0),
92        rotation: DQuat::from_rotation_z(0.5),
93    });
94    // Object grids should not paint their own (grid-local, rotating)
95    // sky — leave the sky to the world grid.
96    scene.grid_mut(ship_id).expect("just added").render_sky = false;
97    // ANCHOR_END: scene_grids
98
99    // ANCHOR: edits
100    let ground = scene.grid_mut(ground_id).expect("just added");
101    // Solid slab: grid-local voxel coords, inclusive on both ends,
102    // decomposed across as many chunks as it spans (here 4×4).
103    ground.set_rect(
104        IVec3::new(-200, -200, 200),
105        IVec3::new(199, 199, 254),
106        Some(GRASS),
107    );
108    // `Some(colour)` inserts, `None` carves back to air.
109    ground.set_sphere(IVec3::new(0, 0, 195), 12, Some(STONE));
110    ground.set_rect(IVec3::new(-3, -200, 190), IVec3::new(3, 199, 196), None);
111    // ANCHOR_END: edits
112
113    // ANCHOR: queries
114    // Solid tests: inside the slab, then inside the carved tunnel.
115    assert!(ground.voxel_solid(IVec3::new(0, 0, 210)));
116    assert!(!ground.voxel_solid(IVec3::new(0, 100, 193)));
117    // Colour queries answer on surface voxels; deep interior cells
118    // are untextured in the slab format and read as `None`.
119    assert_eq!(ground.voxel_color(IVec3::new(50, 50, 200)), Some(GRASS));
120    // ANCHOR_END: queries
121
122    // ANCHOR: recolour
123    // GOTCHA: inserting over already-solid voxels does NOT repaint
124    // them — span insertion only fills air. Recolour = carve, then
125    // insert.
126    let (lo, hi) = (IVec3::new(20, 20, 200), IVec3::new(24, 24, 204));
127    ground.set_rect(lo, hi, Some(RED));
128    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(GRASS)); // unchanged!
129    ground.set_rect(lo, hi, None); // carve…
130    ground.set_rect(lo, hi, Some(RED)); // …then insert
131    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
132    // ANCHOR_END: recolour
133
134    // ANCHOR: colfunc
135    // A carve exposes fresh interior walls; the plain edits paint
136    // them colour 0 (black). The `_with_colfunc` variants ask a
137    // closure instead — it sees grid-local coordinates, so gradients
138    // stay continuous across chunk seams. Here: a crater whose walls
139    // darken with depth.
140    ground.set_sphere_with_colfunc(IVec3::new(60, 60, 200), 8, SpanOp::Carve, |_x, _y, z| {
141        let depth = (z - 192).clamp(0, 15) as u8;
142        VoxColor::rgb(0x6b, 0x40 + depth * 3, 0x2f)
143    });
144    // ANCHOR_END: colfunc
145
146    // ANCHOR: snapshot
147    // Name grids before saving: ids survive the round-trip, but the
148    // generator / store hooks are host code and cannot be serialised
149    // — the name is your key for rebinding them after a load.
150    scene.grid_mut(ground_id).expect("ground exists").name = Some("ground".into());
151    let bytes = scene.save_snapshot(); // versioned envelope
152    let restored = Scene::load_snapshot(&bytes).expect("self-authored snapshot");
153    assert_eq!(restored.grid_count(), 2);
154    let (_, ground2) = restored
155        .grids()
156        .find(|(_, g)| g.name.as_deref() == Some("ground"))
157        .expect("rebind by name");
158    assert_eq!(ground2.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
159    // ANCHOR_END: snapshot
160
161    // ANCHOR: streaming
162    // Streaming: attach a generator + radii to a grid, then pump once
163    // per frame with the camera's world position. Chunks within
164    // `r_active` stream in; chunks beyond `r_evict` drop out; the band
165    // between is hysteresis so a camera hovering on a boundary
166    // doesn't thrash.
167    let stream_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 4096.0, 0.0)));
168    let store = Arc::new(MemoryStore::default());
169    let grid = scene.grid_mut(stream_id).expect("just added");
170    grid.set_generator(Some(Arc::new(FloorGenerator)));
171    grid.set_chunk_store(Some(store));
172    grid.stream_radius = StreamRadius::new(256.0, 384.0);
173
174    let camera_near = DVec3::new(0.0, 4096.0, 100.0);
175    scene.pump_streaming_sync(camera_near); // real games: `pump_streaming`
176    let grid = scene.grid(stream_id).expect("still there");
177    assert!(grid.chunk_count() > 0); // floor streamed in around the camera
178    assert!(grid.voxel_solid(IVec3::new(5, 5, 240)));
179    // ANCHOR_END: streaming
180
181    // ANCHOR: streaming_edit
182    // Edit the streamed floor, walk far away, come back. The active
183    // set follows the camera: the old region evicts (edited chunks
184    // are handed to the ChunkStore first) while a fresh set streams
185    // in around the new position. On return the store wins over the
186    // generator, so the edit survives; without a store, eviction
187    // would silently revert the chunk to generator output.
188    scene
189        .grid_mut(stream_id)
190        .expect("still there")
191        .set_voxel(IVec3::new(5, 5, 240), None); // dig a hole
192    scene.pump_streaming_sync(camera_near + DVec3::new(0.0, 100_000.0, 0.0));
193    let grid = scene.grid(stream_id).expect("still there");
194    assert!(grid.chunk(IVec3::ZERO).is_none()); // home chunk evicted…
195    assert!(grid.chunk_count() > 0); // …but the far region streamed in
196    scene.pump_streaming_sync(camera_near);
197    let grid = scene.grid(stream_id).expect("re-streamed");
198    // The hole survived evict + re-stream.
199    assert!(!grid.voxel_solid(IVec3::new(5, 5, 240)));
200    // ANCHOR_END: streaming_edit
201
202    println!("book_scene_graph: all scene-graph assertions hold");
203}
Source

pub fn ensure_chunk_generated(&mut self, chunk_idx: IVec3) -> bool

Materialise the chunk at chunk_idx by running Self::generator if (a) the chunk is not already present and (b) a generator is attached. Returns true iff a chunk was newly generated.

No-ops in all other cases:

  • chunk already present (caller edits / a previous ensure_chunk_generated call already populated it),
  • no generator attached (the chunk stays implicit-air per the existing convention — does NOT fall through to Self::ensure_chunk’s empty-chunk constructor).

This is the synchronous S7.0 path; Scene::pump_streaming is the async counterpart (generation + store loads on a dedicated rayon pool).

QE.5a: with a ChunkStore attached, a stored chunk is installed (with its persisted edit version) before the generator is consulted — including for indices the generator’s ChunkGenerator::should_generate declines, since edits can materialise chunks the generator never would.

Source

pub fn bounding_radius(&self) -> f64

Bounding-sphere radius of the populated chunk set in grid-local space.

Walks the sparse chunk map once, computes the chunk-index AABB, converts to voxel-space half-extent, returns its Euclidean length. Empty grid → 0.0.

Conservative — bounds the full chunk volume, not just its populated voxels (a chunk containing one voxel still contributes CHUNK_SIZE_XY × CHUNK_SIZE_XY × CHUNK_SIZE_Z to the bbox). For LOD picking that’s fine: an over-bound sphere errs on the side of Near.

Cost: O(chunks.len()); recomputed on every call. Callers who need this every frame should memoize at the Scene-level cache (added when S6.2 needs it).

Source

pub fn select_lod(&self, camera_world_pos: DVec3) -> Lod

Pick this grid’s LOD tier for the given world-space camera position. Convenience wrapper around crate::select_lod that pulls Self::lod_thresholds from the grid.

Trait Implementations§

Source§

impl Debug for Grid

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

§

impl !RefUnwindSafe for Grid

§

impl !UnwindSafe for Grid

§

impl Freeze for Grid

§

impl Send for Grid

§

impl Sync for Grid

§

impl Unpin for Grid

§

impl UnsafeUnpin for Grid

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.