Skip to main content

Grid

Struct Grid 

Source
pub struct Grid {
Show 14 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 water_volumes: Vec<WaterVolume>, 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.

§water_volumes: Vec<WaterVolume>

WT.0 — the grid’s water volumes (WaterVolume, grid-local voxel AABBs; surface = each volume’s lo.z, z-down). The PHYSICS representation of water — Scene::water_depth_at and the swim state read these; the visual water is ordinary (typically volumetric-material) voxels the host fills separately. Authoring state like Self::bake_lights, but — unlike them — persisted in snapshots (wire v3+; older saves load with no water).

§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 116)
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        voxel_world_size: 1.0,
94    });
95    // Object grids should not paint their own (grid-local, rotating)
96    // sky — leave the sky to the world grid.
97    scene.grid_mut(ship_id).expect("just added").render_sky = false;
98    // ANCHOR_END: scene_grids
99
100    // ANCHOR: edits
101    let ground = scene.grid_mut(ground_id).expect("just added");
102    // Solid slab: grid-local voxel coords, inclusive on both ends,
103    // decomposed across as many chunks as it spans (here 4×4).
104    ground.set_rect(
105        IVec3::new(-200, -200, 200),
106        IVec3::new(199, 199, 254),
107        Some(GRASS),
108    );
109    // `Some(colour)` inserts, `None` carves back to air.
110    ground.set_sphere(IVec3::new(0, 0, 195), 12, Some(STONE));
111    ground.set_rect(IVec3::new(-3, -200, 190), IVec3::new(3, 199, 196), None);
112    // ANCHOR_END: edits
113
114    // ANCHOR: queries
115    // Solid tests: inside the slab, then inside the carved tunnel.
116    assert!(ground.voxel_solid(IVec3::new(0, 0, 210)));
117    assert!(!ground.voxel_solid(IVec3::new(0, 100, 193)));
118    // Colour queries answer on surface voxels; deep interior cells
119    // are untextured in the slab format and read as `None`.
120    assert_eq!(ground.voxel_color(IVec3::new(50, 50, 200)), Some(GRASS));
121    // ANCHOR_END: queries
122
123    // ANCHOR: recolour
124    // GOTCHA: inserting over already-solid voxels does NOT repaint
125    // them — span insertion only fills air. Recolour = carve, then
126    // insert.
127    let (lo, hi) = (IVec3::new(20, 20, 200), IVec3::new(24, 24, 204));
128    ground.set_rect(lo, hi, Some(RED));
129    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(GRASS)); // unchanged!
130    ground.set_rect(lo, hi, None); // carve…
131    ground.set_rect(lo, hi, Some(RED)); // …then insert
132    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
133    // ANCHOR_END: recolour
134
135    // ANCHOR: colfunc
136    // A carve exposes fresh interior walls; the plain edits paint
137    // them colour 0 (black). The `_with_colfunc` variants ask a
138    // closure instead — it sees grid-local coordinates, so gradients
139    // stay continuous across chunk seams. Here: a crater whose walls
140    // darken with depth.
141    ground.set_sphere_with_colfunc(IVec3::new(60, 60, 200), 8, SpanOp::Carve, |_x, _y, z| {
142        let depth = (z - 192).clamp(0, 15) as u8;
143        VoxColor::rgb(0x6b, 0x40 + depth * 3, 0x2f)
144    });
145    // ANCHOR_END: colfunc
146
147    // ANCHOR: snapshot
148    // Name grids before saving: ids survive the round-trip, but the
149    // generator / store hooks are host code and cannot be serialised
150    // — the name is your key for rebinding them after a load.
151    scene.grid_mut(ground_id).expect("ground exists").name = Some("ground".into());
152    let bytes = scene.save_snapshot(); // versioned envelope
153    let restored = Scene::load_snapshot(&bytes).expect("self-authored snapshot");
154    assert_eq!(restored.grid_count(), 2);
155    let (_, ground2) = restored
156        .grids()
157        .find(|(_, g)| g.name.as_deref() == Some("ground"))
158        .expect("rebind by name");
159    assert_eq!(ground2.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
160    // ANCHOR_END: snapshot
161
162    // ANCHOR: streaming
163    // Streaming: attach a generator + radii to a grid, then pump once
164    // per frame with the camera's world position. Chunks within
165    // `r_active` stream in; chunks beyond `r_evict` drop out; the band
166    // between is hysteresis so a camera hovering on a boundary
167    // doesn't thrash.
168    let stream_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 4096.0, 0.0)));
169    let store = Arc::new(MemoryStore::default());
170    let grid = scene.grid_mut(stream_id).expect("just added");
171    grid.set_generator(Some(Arc::new(FloorGenerator)));
172    grid.set_chunk_store(Some(store));
173    grid.stream_radius = StreamRadius::new(256.0, 384.0);
174
175    let camera_near = DVec3::new(0.0, 4096.0, 100.0);
176    scene.pump_streaming_sync(camera_near); // real games: `pump_streaming`
177    let grid = scene.grid(stream_id).expect("still there");
178    assert!(grid.chunk_count() > 0); // floor streamed in around the camera
179    assert!(grid.voxel_solid(IVec3::new(5, 5, 240)));
180    // ANCHOR_END: streaming
181
182    // ANCHOR: streaming_edit
183    // Edit the streamed floor, walk far away, come back. The active
184    // set follows the camera: the old region evicts (edited chunks
185    // are handed to the ChunkStore first) while a fresh set streams
186    // in around the new position. On return the store wins over the
187    // generator, so the edit survives; without a store, eviction
188    // would silently revert the chunk to generator output.
189    scene
190        .grid_mut(stream_id)
191        .expect("still there")
192        .set_voxel(IVec3::new(5, 5, 240), None); // dig a hole
193    scene.pump_streaming_sync(camera_near + DVec3::new(0.0, 100_000.0, 0.0));
194    let grid = scene.grid(stream_id).expect("still there");
195    assert!(grid.chunk(IVec3::ZERO).is_none()); // home chunk evicted…
196    assert!(grid.chunk_count() > 0); // …but the far region streamed in
197    scene.pump_streaming_sync(camera_near);
198    let grid = scene.grid(stream_id).expect("re-streamed");
199    // The hole survived evict + re-stream.
200    assert!(!grid.voxel_solid(IVec3::new(5, 5, 240)));
201    // ANCHOR_END: streaming_edit
202
203    // ANCHOR: grid_scale
204    // Per-grid scale: `voxel_world_size` is world units per voxel. A grid
205    // built with `at_scale(origin, 2.0)` has voxels twice world size — a
206    // coarse "planet" — while `0.25` gives a fine detail grid; both coexist
207    // at their true relative sizes. Only the world↔grid boundary scales:
208    // edits + queries stay in voxels, unchanged.
209    let mut scaled = Scene::new();
210    let planet = scaled.add_grid(GridTransform::at_scale(DVec3::ZERO, 2.0));
211    scaled
212        .grid_mut(planet)
213        .expect("just added")
214        // Same voxel edit API — grid-local coordinates.
215        .set_voxel(IVec3::new(5, 5, 10), Some(STONE));
216    // Grid-local voxel z=10 sits at WORLD z = 20 (10 voxels × 2.0). A world
217    // raycast down that column reports the grid-local voxel it hit AND a
218    // WORLD-space `t` — so hits across grids of different scale compare
219    // correctly (the raycaster marches voxels but returns world distance).
220    let hit = scaled
221        .raycast(DVec3::new(11.0, 11.0, 0.0), DVec3::new(0.0, 0.0, 1.0), 64.0)
222        .expect("ray hits the scaled voxel");
223    assert_eq!(hit.voxel, IVec3::new(5, 5, 10)); // grid-local, unscaled
224    assert!((hit.t - 20.0).abs() < 1e-4); // WORLD distance: 10 voxels × 2.0
225                                          // ANCHOR_END: grid_scale
226
227    println!("book_scene_graph: all scene-graph assertions hold");
228}
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 120)
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        voxel_world_size: 1.0,
94    });
95    // Object grids should not paint their own (grid-local, rotating)
96    // sky — leave the sky to the world grid.
97    scene.grid_mut(ship_id).expect("just added").render_sky = false;
98    // ANCHOR_END: scene_grids
99
100    // ANCHOR: edits
101    let ground = scene.grid_mut(ground_id).expect("just added");
102    // Solid slab: grid-local voxel coords, inclusive on both ends,
103    // decomposed across as many chunks as it spans (here 4×4).
104    ground.set_rect(
105        IVec3::new(-200, -200, 200),
106        IVec3::new(199, 199, 254),
107        Some(GRASS),
108    );
109    // `Some(colour)` inserts, `None` carves back to air.
110    ground.set_sphere(IVec3::new(0, 0, 195), 12, Some(STONE));
111    ground.set_rect(IVec3::new(-3, -200, 190), IVec3::new(3, 199, 196), None);
112    // ANCHOR_END: edits
113
114    // ANCHOR: queries
115    // Solid tests: inside the slab, then inside the carved tunnel.
116    assert!(ground.voxel_solid(IVec3::new(0, 0, 210)));
117    assert!(!ground.voxel_solid(IVec3::new(0, 100, 193)));
118    // Colour queries answer on surface voxels; deep interior cells
119    // are untextured in the slab format and read as `None`.
120    assert_eq!(ground.voxel_color(IVec3::new(50, 50, 200)), Some(GRASS));
121    // ANCHOR_END: queries
122
123    // ANCHOR: recolour
124    // GOTCHA: inserting over already-solid voxels does NOT repaint
125    // them — span insertion only fills air. Recolour = carve, then
126    // insert.
127    let (lo, hi) = (IVec3::new(20, 20, 200), IVec3::new(24, 24, 204));
128    ground.set_rect(lo, hi, Some(RED));
129    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(GRASS)); // unchanged!
130    ground.set_rect(lo, hi, None); // carve…
131    ground.set_rect(lo, hi, Some(RED)); // …then insert
132    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
133    // ANCHOR_END: recolour
134
135    // ANCHOR: colfunc
136    // A carve exposes fresh interior walls; the plain edits paint
137    // them colour 0 (black). The `_with_colfunc` variants ask a
138    // closure instead — it sees grid-local coordinates, so gradients
139    // stay continuous across chunk seams. Here: a crater whose walls
140    // darken with depth.
141    ground.set_sphere_with_colfunc(IVec3::new(60, 60, 200), 8, SpanOp::Carve, |_x, _y, z| {
142        let depth = (z - 192).clamp(0, 15) as u8;
143        VoxColor::rgb(0x6b, 0x40 + depth * 3, 0x2f)
144    });
145    // ANCHOR_END: colfunc
146
147    // ANCHOR: snapshot
148    // Name grids before saving: ids survive the round-trip, but the
149    // generator / store hooks are host code and cannot be serialised
150    // — the name is your key for rebinding them after a load.
151    scene.grid_mut(ground_id).expect("ground exists").name = Some("ground".into());
152    let bytes = scene.save_snapshot(); // versioned envelope
153    let restored = Scene::load_snapshot(&bytes).expect("self-authored snapshot");
154    assert_eq!(restored.grid_count(), 2);
155    let (_, ground2) = restored
156        .grids()
157        .find(|(_, g)| g.name.as_deref() == Some("ground"))
158        .expect("rebind by name");
159    assert_eq!(ground2.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
160    // ANCHOR_END: snapshot
161
162    // ANCHOR: streaming
163    // Streaming: attach a generator + radii to a grid, then pump once
164    // per frame with the camera's world position. Chunks within
165    // `r_active` stream in; chunks beyond `r_evict` drop out; the band
166    // between is hysteresis so a camera hovering on a boundary
167    // doesn't thrash.
168    let stream_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 4096.0, 0.0)));
169    let store = Arc::new(MemoryStore::default());
170    let grid = scene.grid_mut(stream_id).expect("just added");
171    grid.set_generator(Some(Arc::new(FloorGenerator)));
172    grid.set_chunk_store(Some(store));
173    grid.stream_radius = StreamRadius::new(256.0, 384.0);
174
175    let camera_near = DVec3::new(0.0, 4096.0, 100.0);
176    scene.pump_streaming_sync(camera_near); // real games: `pump_streaming`
177    let grid = scene.grid(stream_id).expect("still there");
178    assert!(grid.chunk_count() > 0); // floor streamed in around the camera
179    assert!(grid.voxel_solid(IVec3::new(5, 5, 240)));
180    // ANCHOR_END: streaming
181
182    // ANCHOR: streaming_edit
183    // Edit the streamed floor, walk far away, come back. The active
184    // set follows the camera: the old region evicts (edited chunks
185    // are handed to the ChunkStore first) while a fresh set streams
186    // in around the new position. On return the store wins over the
187    // generator, so the edit survives; without a store, eviction
188    // would silently revert the chunk to generator output.
189    scene
190        .grid_mut(stream_id)
191        .expect("still there")
192        .set_voxel(IVec3::new(5, 5, 240), None); // dig a hole
193    scene.pump_streaming_sync(camera_near + DVec3::new(0.0, 100_000.0, 0.0));
194    let grid = scene.grid(stream_id).expect("still there");
195    assert!(grid.chunk(IVec3::ZERO).is_none()); // home chunk evicted…
196    assert!(grid.chunk_count() > 0); // …but the far region streamed in
197    scene.pump_streaming_sync(camera_near);
198    let grid = scene.grid(stream_id).expect("re-streamed");
199    // The hole survived evict + re-stream.
200    assert!(!grid.voxel_solid(IVec3::new(5, 5, 240)));
201    // ANCHOR_END: streaming_edit
202
203    // ANCHOR: grid_scale
204    // Per-grid scale: `voxel_world_size` is world units per voxel. A grid
205    // built with `at_scale(origin, 2.0)` has voxels twice world size — a
206    // coarse "planet" — while `0.25` gives a fine detail grid; both coexist
207    // at their true relative sizes. Only the world↔grid boundary scales:
208    // edits + queries stay in voxels, unchanged.
209    let mut scaled = Scene::new();
210    let planet = scaled.add_grid(GridTransform::at_scale(DVec3::ZERO, 2.0));
211    scaled
212        .grid_mut(planet)
213        .expect("just added")
214        // Same voxel edit API — grid-local coordinates.
215        .set_voxel(IVec3::new(5, 5, 10), Some(STONE));
216    // Grid-local voxel z=10 sits at WORLD z = 20 (10 voxels × 2.0). A world
217    // raycast down that column reports the grid-local voxel it hit AND a
218    // WORLD-space `t` — so hits across grids of different scale compare
219    // correctly (the raycaster marches voxels but returns world distance).
220    let hit = scaled
221        .raycast(DVec3::new(11.0, 11.0, 0.0), DVec3::new(0.0, 0.0, 1.0), 64.0)
222        .expect("ray hits the scaled voxel");
223    assert_eq!(hit.voxel, IVec3::new(5, 5, 10)); // grid-local, unscaled
224    assert!((hit.t - 20.0).abs() < 1e-4); // WORLD distance: 10 voxels × 2.0
225                                          // ANCHOR_END: grid_scale
226
227    println!("book_scene_graph: all scene-graph assertions hold");
228}
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 195)
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        voxel_world_size: 1.0,
94    });
95    // Object grids should not paint their own (grid-local, rotating)
96    // sky — leave the sky to the world grid.
97    scene.grid_mut(ship_id).expect("just added").render_sky = false;
98    // ANCHOR_END: scene_grids
99
100    // ANCHOR: edits
101    let ground = scene.grid_mut(ground_id).expect("just added");
102    // Solid slab: grid-local voxel coords, inclusive on both ends,
103    // decomposed across as many chunks as it spans (here 4×4).
104    ground.set_rect(
105        IVec3::new(-200, -200, 200),
106        IVec3::new(199, 199, 254),
107        Some(GRASS),
108    );
109    // `Some(colour)` inserts, `None` carves back to air.
110    ground.set_sphere(IVec3::new(0, 0, 195), 12, Some(STONE));
111    ground.set_rect(IVec3::new(-3, -200, 190), IVec3::new(3, 199, 196), None);
112    // ANCHOR_END: edits
113
114    // ANCHOR: queries
115    // Solid tests: inside the slab, then inside the carved tunnel.
116    assert!(ground.voxel_solid(IVec3::new(0, 0, 210)));
117    assert!(!ground.voxel_solid(IVec3::new(0, 100, 193)));
118    // Colour queries answer on surface voxels; deep interior cells
119    // are untextured in the slab format and read as `None`.
120    assert_eq!(ground.voxel_color(IVec3::new(50, 50, 200)), Some(GRASS));
121    // ANCHOR_END: queries
122
123    // ANCHOR: recolour
124    // GOTCHA: inserting over already-solid voxels does NOT repaint
125    // them — span insertion only fills air. Recolour = carve, then
126    // insert.
127    let (lo, hi) = (IVec3::new(20, 20, 200), IVec3::new(24, 24, 204));
128    ground.set_rect(lo, hi, Some(RED));
129    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(GRASS)); // unchanged!
130    ground.set_rect(lo, hi, None); // carve…
131    ground.set_rect(lo, hi, Some(RED)); // …then insert
132    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
133    // ANCHOR_END: recolour
134
135    // ANCHOR: colfunc
136    // A carve exposes fresh interior walls; the plain edits paint
137    // them colour 0 (black). The `_with_colfunc` variants ask a
138    // closure instead — it sees grid-local coordinates, so gradients
139    // stay continuous across chunk seams. Here: a crater whose walls
140    // darken with depth.
141    ground.set_sphere_with_colfunc(IVec3::new(60, 60, 200), 8, SpanOp::Carve, |_x, _y, z| {
142        let depth = (z - 192).clamp(0, 15) as u8;
143        VoxColor::rgb(0x6b, 0x40 + depth * 3, 0x2f)
144    });
145    // ANCHOR_END: colfunc
146
147    // ANCHOR: snapshot
148    // Name grids before saving: ids survive the round-trip, but the
149    // generator / store hooks are host code and cannot be serialised
150    // — the name is your key for rebinding them after a load.
151    scene.grid_mut(ground_id).expect("ground exists").name = Some("ground".into());
152    let bytes = scene.save_snapshot(); // versioned envelope
153    let restored = Scene::load_snapshot(&bytes).expect("self-authored snapshot");
154    assert_eq!(restored.grid_count(), 2);
155    let (_, ground2) = restored
156        .grids()
157        .find(|(_, g)| g.name.as_deref() == Some("ground"))
158        .expect("rebind by name");
159    assert_eq!(ground2.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
160    // ANCHOR_END: snapshot
161
162    // ANCHOR: streaming
163    // Streaming: attach a generator + radii to a grid, then pump once
164    // per frame with the camera's world position. Chunks within
165    // `r_active` stream in; chunks beyond `r_evict` drop out; the band
166    // between is hysteresis so a camera hovering on a boundary
167    // doesn't thrash.
168    let stream_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 4096.0, 0.0)));
169    let store = Arc::new(MemoryStore::default());
170    let grid = scene.grid_mut(stream_id).expect("just added");
171    grid.set_generator(Some(Arc::new(FloorGenerator)));
172    grid.set_chunk_store(Some(store));
173    grid.stream_radius = StreamRadius::new(256.0, 384.0);
174
175    let camera_near = DVec3::new(0.0, 4096.0, 100.0);
176    scene.pump_streaming_sync(camera_near); // real games: `pump_streaming`
177    let grid = scene.grid(stream_id).expect("still there");
178    assert!(grid.chunk_count() > 0); // floor streamed in around the camera
179    assert!(grid.voxel_solid(IVec3::new(5, 5, 240)));
180    // ANCHOR_END: streaming
181
182    // ANCHOR: streaming_edit
183    // Edit the streamed floor, walk far away, come back. The active
184    // set follows the camera: the old region evicts (edited chunks
185    // are handed to the ChunkStore first) while a fresh set streams
186    // in around the new position. On return the store wins over the
187    // generator, so the edit survives; without a store, eviction
188    // would silently revert the chunk to generator output.
189    scene
190        .grid_mut(stream_id)
191        .expect("still there")
192        .set_voxel(IVec3::new(5, 5, 240), None); // dig a hole
193    scene.pump_streaming_sync(camera_near + DVec3::new(0.0, 100_000.0, 0.0));
194    let grid = scene.grid(stream_id).expect("still there");
195    assert!(grid.chunk(IVec3::ZERO).is_none()); // home chunk evicted…
196    assert!(grid.chunk_count() > 0); // …but the far region streamed in
197    scene.pump_streaming_sync(camera_near);
198    let grid = scene.grid(stream_id).expect("re-streamed");
199    // The hole survived evict + re-stream.
200    assert!(!grid.voxel_solid(IVec3::new(5, 5, 240)));
201    // ANCHOR_END: streaming_edit
202
203    // ANCHOR: grid_scale
204    // Per-grid scale: `voxel_world_size` is world units per voxel. A grid
205    // built with `at_scale(origin, 2.0)` has voxels twice world size — a
206    // coarse "planet" — while `0.25` gives a fine detail grid; both coexist
207    // at their true relative sizes. Only the world↔grid boundary scales:
208    // edits + queries stay in voxels, unchanged.
209    let mut scaled = Scene::new();
210    let planet = scaled.add_grid(GridTransform::at_scale(DVec3::ZERO, 2.0));
211    scaled
212        .grid_mut(planet)
213        .expect("just added")
214        // Same voxel edit API — grid-local coordinates.
215        .set_voxel(IVec3::new(5, 5, 10), Some(STONE));
216    // Grid-local voxel z=10 sits at WORLD z = 20 (10 voxels × 2.0). A world
217    // raycast down that column reports the grid-local voxel it hit AND a
218    // WORLD-space `t` — so hits across grids of different scale compare
219    // correctly (the raycaster marches voxels but returns world distance).
220    let hit = scaled
221        .raycast(DVec3::new(11.0, 11.0, 0.0), DVec3::new(0.0, 0.0, 1.0), 64.0)
222        .expect("ray hits the scaled voxel");
223    assert_eq!(hit.voxel, IVec3::new(5, 5, 10)); // grid-local, unscaled
224    assert!((hit.t - 20.0).abs() < 1e-4); // WORLD distance: 10 voxels × 2.0
225                                          // ANCHOR_END: grid_scale
226
227    println!("book_scene_graph: all scene-graph assertions hold");
228}
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 178)
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        voxel_world_size: 1.0,
94    });
95    // Object grids should not paint their own (grid-local, rotating)
96    // sky — leave the sky to the world grid.
97    scene.grid_mut(ship_id).expect("just added").render_sky = false;
98    // ANCHOR_END: scene_grids
99
100    // ANCHOR: edits
101    let ground = scene.grid_mut(ground_id).expect("just added");
102    // Solid slab: grid-local voxel coords, inclusive on both ends,
103    // decomposed across as many chunks as it spans (here 4×4).
104    ground.set_rect(
105        IVec3::new(-200, -200, 200),
106        IVec3::new(199, 199, 254),
107        Some(GRASS),
108    );
109    // `Some(colour)` inserts, `None` carves back to air.
110    ground.set_sphere(IVec3::new(0, 0, 195), 12, Some(STONE));
111    ground.set_rect(IVec3::new(-3, -200, 190), IVec3::new(3, 199, 196), None);
112    // ANCHOR_END: edits
113
114    // ANCHOR: queries
115    // Solid tests: inside the slab, then inside the carved tunnel.
116    assert!(ground.voxel_solid(IVec3::new(0, 0, 210)));
117    assert!(!ground.voxel_solid(IVec3::new(0, 100, 193)));
118    // Colour queries answer on surface voxels; deep interior cells
119    // are untextured in the slab format and read as `None`.
120    assert_eq!(ground.voxel_color(IVec3::new(50, 50, 200)), Some(GRASS));
121    // ANCHOR_END: queries
122
123    // ANCHOR: recolour
124    // GOTCHA: inserting over already-solid voxels does NOT repaint
125    // them — span insertion only fills air. Recolour = carve, then
126    // insert.
127    let (lo, hi) = (IVec3::new(20, 20, 200), IVec3::new(24, 24, 204));
128    ground.set_rect(lo, hi, Some(RED));
129    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(GRASS)); // unchanged!
130    ground.set_rect(lo, hi, None); // carve…
131    ground.set_rect(lo, hi, Some(RED)); // …then insert
132    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
133    // ANCHOR_END: recolour
134
135    // ANCHOR: colfunc
136    // A carve exposes fresh interior walls; the plain edits paint
137    // them colour 0 (black). The `_with_colfunc` variants ask a
138    // closure instead — it sees grid-local coordinates, so gradients
139    // stay continuous across chunk seams. Here: a crater whose walls
140    // darken with depth.
141    ground.set_sphere_with_colfunc(IVec3::new(60, 60, 200), 8, SpanOp::Carve, |_x, _y, z| {
142        let depth = (z - 192).clamp(0, 15) as u8;
143        VoxColor::rgb(0x6b, 0x40 + depth * 3, 0x2f)
144    });
145    // ANCHOR_END: colfunc
146
147    // ANCHOR: snapshot
148    // Name grids before saving: ids survive the round-trip, but the
149    // generator / store hooks are host code and cannot be serialised
150    // — the name is your key for rebinding them after a load.
151    scene.grid_mut(ground_id).expect("ground exists").name = Some("ground".into());
152    let bytes = scene.save_snapshot(); // versioned envelope
153    let restored = Scene::load_snapshot(&bytes).expect("self-authored snapshot");
154    assert_eq!(restored.grid_count(), 2);
155    let (_, ground2) = restored
156        .grids()
157        .find(|(_, g)| g.name.as_deref() == Some("ground"))
158        .expect("rebind by name");
159    assert_eq!(ground2.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
160    // ANCHOR_END: snapshot
161
162    // ANCHOR: streaming
163    // Streaming: attach a generator + radii to a grid, then pump once
164    // per frame with the camera's world position. Chunks within
165    // `r_active` stream in; chunks beyond `r_evict` drop out; the band
166    // between is hysteresis so a camera hovering on a boundary
167    // doesn't thrash.
168    let stream_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 4096.0, 0.0)));
169    let store = Arc::new(MemoryStore::default());
170    let grid = scene.grid_mut(stream_id).expect("just added");
171    grid.set_generator(Some(Arc::new(FloorGenerator)));
172    grid.set_chunk_store(Some(store));
173    grid.stream_radius = StreamRadius::new(256.0, 384.0);
174
175    let camera_near = DVec3::new(0.0, 4096.0, 100.0);
176    scene.pump_streaming_sync(camera_near); // real games: `pump_streaming`
177    let grid = scene.grid(stream_id).expect("still there");
178    assert!(grid.chunk_count() > 0); // floor streamed in around the camera
179    assert!(grid.voxel_solid(IVec3::new(5, 5, 240)));
180    // ANCHOR_END: streaming
181
182    // ANCHOR: streaming_edit
183    // Edit the streamed floor, walk far away, come back. The active
184    // set follows the camera: the old region evicts (edited chunks
185    // are handed to the ChunkStore first) while a fresh set streams
186    // in around the new position. On return the store wins over the
187    // generator, so the edit survives; without a store, eviction
188    // would silently revert the chunk to generator output.
189    scene
190        .grid_mut(stream_id)
191        .expect("still there")
192        .set_voxel(IVec3::new(5, 5, 240), None); // dig a hole
193    scene.pump_streaming_sync(camera_near + DVec3::new(0.0, 100_000.0, 0.0));
194    let grid = scene.grid(stream_id).expect("still there");
195    assert!(grid.chunk(IVec3::ZERO).is_none()); // home chunk evicted…
196    assert!(grid.chunk_count() > 0); // …but the far region streamed in
197    scene.pump_streaming_sync(camera_near);
198    let grid = scene.grid(stream_id).expect("re-streamed");
199    // The hole survived evict + re-stream.
200    assert!(!grid.voxel_solid(IVec3::new(5, 5, 240)));
201    // ANCHOR_END: streaming_edit
202
203    // ANCHOR: grid_scale
204    // Per-grid scale: `voxel_world_size` is world units per voxel. A grid
205    // built with `at_scale(origin, 2.0)` has voxels twice world size — a
206    // coarse "planet" — while `0.25` gives a fine detail grid; both coexist
207    // at their true relative sizes. Only the world↔grid boundary scales:
208    // edits + queries stay in voxels, unchanged.
209    let mut scaled = Scene::new();
210    let planet = scaled.add_grid(GridTransform::at_scale(DVec3::ZERO, 2.0));
211    scaled
212        .grid_mut(planet)
213        .expect("just added")
214        // Same voxel edit API — grid-local coordinates.
215        .set_voxel(IVec3::new(5, 5, 10), Some(STONE));
216    // Grid-local voxel z=10 sits at WORLD z = 20 (10 voxels × 2.0). A world
217    // raycast down that column reports the grid-local voxel it hit AND a
218    // WORLD-space `t` — so hits across grids of different scale compare
219    // correctly (the raycaster marches voxels but returns world distance).
220    let hit = scaled
221        .raycast(DVec3::new(11.0, 11.0, 0.0), DVec3::new(0.0, 0.0, 1.0), 64.0)
222        .expect("ray hits the scaled voxel");
223    assert_eq!(hit.voxel, IVec3::new(5, 5, 10)); // grid-local, unscaled
224    assert!((hit.t - 20.0).abs() < 1e-4); // WORLD distance: 10 voxels × 2.0
225                                          // ANCHOR_END: grid_scale
226
227    println!("book_scene_graph: all scene-graph assertions hold");
228}
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 192)
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        voxel_world_size: 1.0,
94    });
95    // Object grids should not paint their own (grid-local, rotating)
96    // sky — leave the sky to the world grid.
97    scene.grid_mut(ship_id).expect("just added").render_sky = false;
98    // ANCHOR_END: scene_grids
99
100    // ANCHOR: edits
101    let ground = scene.grid_mut(ground_id).expect("just added");
102    // Solid slab: grid-local voxel coords, inclusive on both ends,
103    // decomposed across as many chunks as it spans (here 4×4).
104    ground.set_rect(
105        IVec3::new(-200, -200, 200),
106        IVec3::new(199, 199, 254),
107        Some(GRASS),
108    );
109    // `Some(colour)` inserts, `None` carves back to air.
110    ground.set_sphere(IVec3::new(0, 0, 195), 12, Some(STONE));
111    ground.set_rect(IVec3::new(-3, -200, 190), IVec3::new(3, 199, 196), None);
112    // ANCHOR_END: edits
113
114    // ANCHOR: queries
115    // Solid tests: inside the slab, then inside the carved tunnel.
116    assert!(ground.voxel_solid(IVec3::new(0, 0, 210)));
117    assert!(!ground.voxel_solid(IVec3::new(0, 100, 193)));
118    // Colour queries answer on surface voxels; deep interior cells
119    // are untextured in the slab format and read as `None`.
120    assert_eq!(ground.voxel_color(IVec3::new(50, 50, 200)), Some(GRASS));
121    // ANCHOR_END: queries
122
123    // ANCHOR: recolour
124    // GOTCHA: inserting over already-solid voxels does NOT repaint
125    // them — span insertion only fills air. Recolour = carve, then
126    // insert.
127    let (lo, hi) = (IVec3::new(20, 20, 200), IVec3::new(24, 24, 204));
128    ground.set_rect(lo, hi, Some(RED));
129    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(GRASS)); // unchanged!
130    ground.set_rect(lo, hi, None); // carve…
131    ground.set_rect(lo, hi, Some(RED)); // …then insert
132    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
133    // ANCHOR_END: recolour
134
135    // ANCHOR: colfunc
136    // A carve exposes fresh interior walls; the plain edits paint
137    // them colour 0 (black). The `_with_colfunc` variants ask a
138    // closure instead — it sees grid-local coordinates, so gradients
139    // stay continuous across chunk seams. Here: a crater whose walls
140    // darken with depth.
141    ground.set_sphere_with_colfunc(IVec3::new(60, 60, 200), 8, SpanOp::Carve, |_x, _y, z| {
142        let depth = (z - 192).clamp(0, 15) as u8;
143        VoxColor::rgb(0x6b, 0x40 + depth * 3, 0x2f)
144    });
145    // ANCHOR_END: colfunc
146
147    // ANCHOR: snapshot
148    // Name grids before saving: ids survive the round-trip, but the
149    // generator / store hooks are host code and cannot be serialised
150    // — the name is your key for rebinding them after a load.
151    scene.grid_mut(ground_id).expect("ground exists").name = Some("ground".into());
152    let bytes = scene.save_snapshot(); // versioned envelope
153    let restored = Scene::load_snapshot(&bytes).expect("self-authored snapshot");
154    assert_eq!(restored.grid_count(), 2);
155    let (_, ground2) = restored
156        .grids()
157        .find(|(_, g)| g.name.as_deref() == Some("ground"))
158        .expect("rebind by name");
159    assert_eq!(ground2.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
160    // ANCHOR_END: snapshot
161
162    // ANCHOR: streaming
163    // Streaming: attach a generator + radii to a grid, then pump once
164    // per frame with the camera's world position. Chunks within
165    // `r_active` stream in; chunks beyond `r_evict` drop out; the band
166    // between is hysteresis so a camera hovering on a boundary
167    // doesn't thrash.
168    let stream_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 4096.0, 0.0)));
169    let store = Arc::new(MemoryStore::default());
170    let grid = scene.grid_mut(stream_id).expect("just added");
171    grid.set_generator(Some(Arc::new(FloorGenerator)));
172    grid.set_chunk_store(Some(store));
173    grid.stream_radius = StreamRadius::new(256.0, 384.0);
174
175    let camera_near = DVec3::new(0.0, 4096.0, 100.0);
176    scene.pump_streaming_sync(camera_near); // real games: `pump_streaming`
177    let grid = scene.grid(stream_id).expect("still there");
178    assert!(grid.chunk_count() > 0); // floor streamed in around the camera
179    assert!(grid.voxel_solid(IVec3::new(5, 5, 240)));
180    // ANCHOR_END: streaming
181
182    // ANCHOR: streaming_edit
183    // Edit the streamed floor, walk far away, come back. The active
184    // set follows the camera: the old region evicts (edited chunks
185    // are handed to the ChunkStore first) while a fresh set streams
186    // in around the new position. On return the store wins over the
187    // generator, so the edit survives; without a store, eviction
188    // would silently revert the chunk to generator output.
189    scene
190        .grid_mut(stream_id)
191        .expect("still there")
192        .set_voxel(IVec3::new(5, 5, 240), None); // dig a hole
193    scene.pump_streaming_sync(camera_near + DVec3::new(0.0, 100_000.0, 0.0));
194    let grid = scene.grid(stream_id).expect("still there");
195    assert!(grid.chunk(IVec3::ZERO).is_none()); // home chunk evicted…
196    assert!(grid.chunk_count() > 0); // …but the far region streamed in
197    scene.pump_streaming_sync(camera_near);
198    let grid = scene.grid(stream_id).expect("re-streamed");
199    // The hole survived evict + re-stream.
200    assert!(!grid.voxel_solid(IVec3::new(5, 5, 240)));
201    // ANCHOR_END: streaming_edit
202
203    // ANCHOR: grid_scale
204    // Per-grid scale: `voxel_world_size` is world units per voxel. A grid
205    // built with `at_scale(origin, 2.0)` has voxels twice world size — a
206    // coarse "planet" — while `0.25` gives a fine detail grid; both coexist
207    // at their true relative sizes. Only the world↔grid boundary scales:
208    // edits + queries stay in voxels, unchanged.
209    let mut scaled = Scene::new();
210    let planet = scaled.add_grid(GridTransform::at_scale(DVec3::ZERO, 2.0));
211    scaled
212        .grid_mut(planet)
213        .expect("just added")
214        // Same voxel edit API — grid-local coordinates.
215        .set_voxel(IVec3::new(5, 5, 10), Some(STONE));
216    // Grid-local voxel z=10 sits at WORLD z = 20 (10 voxels × 2.0). A world
217    // raycast down that column reports the grid-local voxel it hit AND a
218    // WORLD-space `t` — so hits across grids of different scale compare
219    // correctly (the raycaster marches voxels but returns world distance).
220    let hit = scaled
221        .raycast(DVec3::new(11.0, 11.0, 0.0), DVec3::new(0.0, 0.0, 1.0), 64.0)
222        .expect("ray hits the scaled voxel");
223    assert_eq!(hit.voxel, IVec3::new(5, 5, 10)); // grid-local, unscaled
224    assert!((hit.t - 20.0).abs() < 1e-4); // WORLD distance: 10 voxels × 2.0
225                                          // ANCHOR_END: grid_scale
226
227    println!("book_scene_graph: all scene-graph assertions hold");
228}
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}
60
61fn main() {
62    let scene = build_world();
63
64    // ANCHOR: setup
65    // A walking body: a feet-positioned collision box, f64 world.
66    // Distances are voxels, times seconds; +z is DOWN, so gravity is
67    // positive and a jump impulse negative.
68    let mut body = CharacterBody::new(CharacterDef {
69        radius: 0.4,      // xy half-extent of the box
70        height: 1.8,      // feet → head (toward -z)
71        eye_height: 1.62, // feet → camera anchor
72        step_up: 1.05,    // auto-step ledges up to 1 voxel
73        solidity: Solidity {
74            bedrock_blocks: false, // match your renderer's policy
75            passable: Some(water_passes),
76        },
77        ..CharacterDef::default()
78    });
79    body.teleport(DVec3::new(50.0, 50.0, 95.0)); // FEET position
80                                                 // ANCHOR_END: setup
81
82    // ANCHOR: frame
83    // Per frame: build a wish direction from input (unit-length or
84    // zero — walk() clamps), call walk() EVERY frame (a zero wish is
85    // what stops the body), then anchor the camera at the eye.
86    let dt = 1.0 / 60.0;
87    for frame in 0..480 {
88        let input = WalkInput {
89            wish: DVec3::new(1.0, 0.2, 0.0), // toward the ledge
90            jump: frame == 120,              // one hop on the way
91            ..WalkInput::default()           // sink (WT.1) + future fields
92        };
93        body.walk(&scene, dt, input);
94    }
95    let eye = body.eye_pos(); // = Camera::from_yaw_pitch(eye.into(), yaw, pitch)
96                              // ANCHOR_END: frame
97
98    // The trajectory the walk above promises, pinned:
99    assert!(body.on_ground(), "settled after the hop");
100    // It fell to the floor plane, then stepped up the 1-voxel ledge
101    // (surface z = 99) while walking +x.
102    assert!(body.pos().x > 90.0, "reached the ledge region");
103    assert!(
104        (body.pos().z - 99.0).abs() < 0.01,
105        "standing on the ledge, feet at {}",
106        body.pos().z
107    );
108    assert!(eye.z < body.pos().z, "the eye is above the feet (-z is up)");
109
110    // Fly mode is the demos' free camera: no gravity, full-3D wish,
111    // instant start/stop, still sliding along walls.
112    body.set_mode(MoveMode::Fly);
113    body.walk(&scene, dt, WalkInput::default());
114    assert_eq!(body.vel(), DVec3::ZERO, "fly idles in place");
115
116    // The water curtain from `build_world` is passable: probe the
117    // veto directly (the same query walk() uses).
118    let wading = roxlap_scene::box_overlaps_solid(
119        &scene,
120        DVec3::new(40.2, 50.0, 90.0),
121        DVec3::new(40.8, 50.6, 91.8),
122        body.def().solidity,
123    );
124    assert!(!wading, "water is passable under the veto");
125
126    swim_demo();
127
128    println!("book_controller: all controller assertions hold");
129}
130
131/// WT — the water + swimming section's snippets: a deep pool the body
132/// falls into, floats in, dives through and breaches out of.
133fn swim_demo() {
134    // ANCHOR: water
135    // Physics water is a WaterVolume on the grid — a grid-local voxel
136    // AABB whose TOP face (min z — +z is down) is the surface. It is
137    // independent of the voxels: fill the same region with a
138    // volumetric-material shell for the visuals, or don't.
139    let mut scene = Scene::new();
140    let id = scene.add_grid(GridTransform::identity());
141    let g = scene.grid_mut(id).expect("grid");
142    // Basin floor, then 20 voxels of water above it (surface z = 100).
143    g.set_rect(
144        IVec3::new(20, 20, 120),
145        IVec3::new(140, 140, 130),
146        Some(VoxColor::rgb(0x50, 0x90, 0x50)),
147    );
148    g.add_water_volume(IVec3::new(20, 20, 100), IVec3::new(140, 140, 119));
149
150    // A Walk-mode body dropped in swims AUTOMATICALLY once submerged
151    // past `swim_enter_frac` of its height: buoyancy floats it to a
152    // bob at the surface, `jump` strokes up, `sink` strokes down, and
153    // a jump with the head above water breaches.
154    let mut body = CharacterBody::new(CharacterDef::default());
155    body.teleport(DVec3::new(80.0, 80.0, 115.0)); // deep in the pool
156    for _ in 0..600 {
157        body.walk(&scene, 1.0 / 60.0, WalkInput::default());
158    }
159    assert!(body.is_swimming(), "floated to the surface bob");
160    assert!(!body.eye_in_water(&scene), "bobbing: the camera is dry");
161
162    // Dive: hold `sink`. The submerged EYE is the hook for the
163    // underwater feel — drive the WT.2 frame tint and the WT.3
164    // listener lowpass from this one flag.
165    for _ in 0..90 {
166        body.walk(
167            &scene,
168            1.0 / 60.0,
169            WalkInput {
170                sink: true,
171                ..WalkInput::default()
172            },
173        );
174    }
175    assert!(body.eye_in_water(&scene), "diving: tint + muffle now");
176    // ANCHOR_END: water
177}
More examples
Hide additional examples
examples/book_scene_graph.rs (lines 104-108)
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        voxel_world_size: 1.0,
94    });
95    // Object grids should not paint their own (grid-local, rotating)
96    // sky — leave the sky to the world grid.
97    scene.grid_mut(ship_id).expect("just added").render_sky = false;
98    // ANCHOR_END: scene_grids
99
100    // ANCHOR: edits
101    let ground = scene.grid_mut(ground_id).expect("just added");
102    // Solid slab: grid-local voxel coords, inclusive on both ends,
103    // decomposed across as many chunks as it spans (here 4×4).
104    ground.set_rect(
105        IVec3::new(-200, -200, 200),
106        IVec3::new(199, 199, 254),
107        Some(GRASS),
108    );
109    // `Some(colour)` inserts, `None` carves back to air.
110    ground.set_sphere(IVec3::new(0, 0, 195), 12, Some(STONE));
111    ground.set_rect(IVec3::new(-3, -200, 190), IVec3::new(3, 199, 196), None);
112    // ANCHOR_END: edits
113
114    // ANCHOR: queries
115    // Solid tests: inside the slab, then inside the carved tunnel.
116    assert!(ground.voxel_solid(IVec3::new(0, 0, 210)));
117    assert!(!ground.voxel_solid(IVec3::new(0, 100, 193)));
118    // Colour queries answer on surface voxels; deep interior cells
119    // are untextured in the slab format and read as `None`.
120    assert_eq!(ground.voxel_color(IVec3::new(50, 50, 200)), Some(GRASS));
121    // ANCHOR_END: queries
122
123    // ANCHOR: recolour
124    // GOTCHA: inserting over already-solid voxels does NOT repaint
125    // them — span insertion only fills air. Recolour = carve, then
126    // insert.
127    let (lo, hi) = (IVec3::new(20, 20, 200), IVec3::new(24, 24, 204));
128    ground.set_rect(lo, hi, Some(RED));
129    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(GRASS)); // unchanged!
130    ground.set_rect(lo, hi, None); // carve…
131    ground.set_rect(lo, hi, Some(RED)); // …then insert
132    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
133    // ANCHOR_END: recolour
134
135    // ANCHOR: colfunc
136    // A carve exposes fresh interior walls; the plain edits paint
137    // them colour 0 (black). The `_with_colfunc` variants ask a
138    // closure instead — it sees grid-local coordinates, so gradients
139    // stay continuous across chunk seams. Here: a crater whose walls
140    // darken with depth.
141    ground.set_sphere_with_colfunc(IVec3::new(60, 60, 200), 8, SpanOp::Carve, |_x, _y, z| {
142        let depth = (z - 192).clamp(0, 15) as u8;
143        VoxColor::rgb(0x6b, 0x40 + depth * 3, 0x2f)
144    });
145    // ANCHOR_END: colfunc
146
147    // ANCHOR: snapshot
148    // Name grids before saving: ids survive the round-trip, but the
149    // generator / store hooks are host code and cannot be serialised
150    // — the name is your key for rebinding them after a load.
151    scene.grid_mut(ground_id).expect("ground exists").name = Some("ground".into());
152    let bytes = scene.save_snapshot(); // versioned envelope
153    let restored = Scene::load_snapshot(&bytes).expect("self-authored snapshot");
154    assert_eq!(restored.grid_count(), 2);
155    let (_, ground2) = restored
156        .grids()
157        .find(|(_, g)| g.name.as_deref() == Some("ground"))
158        .expect("rebind by name");
159    assert_eq!(ground2.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
160    // ANCHOR_END: snapshot
161
162    // ANCHOR: streaming
163    // Streaming: attach a generator + radii to a grid, then pump once
164    // per frame with the camera's world position. Chunks within
165    // `r_active` stream in; chunks beyond `r_evict` drop out; the band
166    // between is hysteresis so a camera hovering on a boundary
167    // doesn't thrash.
168    let stream_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 4096.0, 0.0)));
169    let store = Arc::new(MemoryStore::default());
170    let grid = scene.grid_mut(stream_id).expect("just added");
171    grid.set_generator(Some(Arc::new(FloorGenerator)));
172    grid.set_chunk_store(Some(store));
173    grid.stream_radius = StreamRadius::new(256.0, 384.0);
174
175    let camera_near = DVec3::new(0.0, 4096.0, 100.0);
176    scene.pump_streaming_sync(camera_near); // real games: `pump_streaming`
177    let grid = scene.grid(stream_id).expect("still there");
178    assert!(grid.chunk_count() > 0); // floor streamed in around the camera
179    assert!(grid.voxel_solid(IVec3::new(5, 5, 240)));
180    // ANCHOR_END: streaming
181
182    // ANCHOR: streaming_edit
183    // Edit the streamed floor, walk far away, come back. The active
184    // set follows the camera: the old region evicts (edited chunks
185    // are handed to the ChunkStore first) while a fresh set streams
186    // in around the new position. On return the store wins over the
187    // generator, so the edit survives; without a store, eviction
188    // would silently revert the chunk to generator output.
189    scene
190        .grid_mut(stream_id)
191        .expect("still there")
192        .set_voxel(IVec3::new(5, 5, 240), None); // dig a hole
193    scene.pump_streaming_sync(camera_near + DVec3::new(0.0, 100_000.0, 0.0));
194    let grid = scene.grid(stream_id).expect("still there");
195    assert!(grid.chunk(IVec3::ZERO).is_none()); // home chunk evicted…
196    assert!(grid.chunk_count() > 0); // …but the far region streamed in
197    scene.pump_streaming_sync(camera_near);
198    let grid = scene.grid(stream_id).expect("re-streamed");
199    // The hole survived evict + re-stream.
200    assert!(!grid.voxel_solid(IVec3::new(5, 5, 240)));
201    // ANCHOR_END: streaming_edit
202
203    // ANCHOR: grid_scale
204    // Per-grid scale: `voxel_world_size` is world units per voxel. A grid
205    // built with `at_scale(origin, 2.0)` has voxels twice world size — a
206    // coarse "planet" — while `0.25` gives a fine detail grid; both coexist
207    // at their true relative sizes. Only the world↔grid boundary scales:
208    // edits + queries stay in voxels, unchanged.
209    let mut scaled = Scene::new();
210    let planet = scaled.add_grid(GridTransform::at_scale(DVec3::ZERO, 2.0));
211    scaled
212        .grid_mut(planet)
213        .expect("just added")
214        // Same voxel edit API — grid-local coordinates.
215        .set_voxel(IVec3::new(5, 5, 10), Some(STONE));
216    // Grid-local voxel z=10 sits at WORLD z = 20 (10 voxels × 2.0). A world
217    // raycast down that column reports the grid-local voxel it hit AND a
218    // WORLD-space `t` — so hits across grids of different scale compare
219    // correctly (the raycaster marches voxels but returns world distance).
220    let hit = scaled
221        .raycast(DVec3::new(11.0, 11.0, 0.0), DVec3::new(0.0, 0.0, 1.0), 64.0)
222        .expect("ray hits the scaled voxel");
223    assert_eq!(hit.voxel, IVec3::new(5, 5, 10)); // grid-local, unscaled
224    assert!((hit.t - 20.0).abs() < 1e-4); // WORLD distance: 10 voxels × 2.0
225                                          // ANCHOR_END: grid_scale
226
227    println!("book_scene_graph: all scene-graph assertions hold");
228}
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 110)
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        voxel_world_size: 1.0,
94    });
95    // Object grids should not paint their own (grid-local, rotating)
96    // sky — leave the sky to the world grid.
97    scene.grid_mut(ship_id).expect("just added").render_sky = false;
98    // ANCHOR_END: scene_grids
99
100    // ANCHOR: edits
101    let ground = scene.grid_mut(ground_id).expect("just added");
102    // Solid slab: grid-local voxel coords, inclusive on both ends,
103    // decomposed across as many chunks as it spans (here 4×4).
104    ground.set_rect(
105        IVec3::new(-200, -200, 200),
106        IVec3::new(199, 199, 254),
107        Some(GRASS),
108    );
109    // `Some(colour)` inserts, `None` carves back to air.
110    ground.set_sphere(IVec3::new(0, 0, 195), 12, Some(STONE));
111    ground.set_rect(IVec3::new(-3, -200, 190), IVec3::new(3, 199, 196), None);
112    // ANCHOR_END: edits
113
114    // ANCHOR: queries
115    // Solid tests: inside the slab, then inside the carved tunnel.
116    assert!(ground.voxel_solid(IVec3::new(0, 0, 210)));
117    assert!(!ground.voxel_solid(IVec3::new(0, 100, 193)));
118    // Colour queries answer on surface voxels; deep interior cells
119    // are untextured in the slab format and read as `None`.
120    assert_eq!(ground.voxel_color(IVec3::new(50, 50, 200)), Some(GRASS));
121    // ANCHOR_END: queries
122
123    // ANCHOR: recolour
124    // GOTCHA: inserting over already-solid voxels does NOT repaint
125    // them — span insertion only fills air. Recolour = carve, then
126    // insert.
127    let (lo, hi) = (IVec3::new(20, 20, 200), IVec3::new(24, 24, 204));
128    ground.set_rect(lo, hi, Some(RED));
129    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(GRASS)); // unchanged!
130    ground.set_rect(lo, hi, None); // carve…
131    ground.set_rect(lo, hi, Some(RED)); // …then insert
132    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
133    // ANCHOR_END: recolour
134
135    // ANCHOR: colfunc
136    // A carve exposes fresh interior walls; the plain edits paint
137    // them colour 0 (black). The `_with_colfunc` variants ask a
138    // closure instead — it sees grid-local coordinates, so gradients
139    // stay continuous across chunk seams. Here: a crater whose walls
140    // darken with depth.
141    ground.set_sphere_with_colfunc(IVec3::new(60, 60, 200), 8, SpanOp::Carve, |_x, _y, z| {
142        let depth = (z - 192).clamp(0, 15) as u8;
143        VoxColor::rgb(0x6b, 0x40 + depth * 3, 0x2f)
144    });
145    // ANCHOR_END: colfunc
146
147    // ANCHOR: snapshot
148    // Name grids before saving: ids survive the round-trip, but the
149    // generator / store hooks are host code and cannot be serialised
150    // — the name is your key for rebinding them after a load.
151    scene.grid_mut(ground_id).expect("ground exists").name = Some("ground".into());
152    let bytes = scene.save_snapshot(); // versioned envelope
153    let restored = Scene::load_snapshot(&bytes).expect("self-authored snapshot");
154    assert_eq!(restored.grid_count(), 2);
155    let (_, ground2) = restored
156        .grids()
157        .find(|(_, g)| g.name.as_deref() == Some("ground"))
158        .expect("rebind by name");
159    assert_eq!(ground2.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
160    // ANCHOR_END: snapshot
161
162    // ANCHOR: streaming
163    // Streaming: attach a generator + radii to a grid, then pump once
164    // per frame with the camera's world position. Chunks within
165    // `r_active` stream in; chunks beyond `r_evict` drop out; the band
166    // between is hysteresis so a camera hovering on a boundary
167    // doesn't thrash.
168    let stream_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 4096.0, 0.0)));
169    let store = Arc::new(MemoryStore::default());
170    let grid = scene.grid_mut(stream_id).expect("just added");
171    grid.set_generator(Some(Arc::new(FloorGenerator)));
172    grid.set_chunk_store(Some(store));
173    grid.stream_radius = StreamRadius::new(256.0, 384.0);
174
175    let camera_near = DVec3::new(0.0, 4096.0, 100.0);
176    scene.pump_streaming_sync(camera_near); // real games: `pump_streaming`
177    let grid = scene.grid(stream_id).expect("still there");
178    assert!(grid.chunk_count() > 0); // floor streamed in around the camera
179    assert!(grid.voxel_solid(IVec3::new(5, 5, 240)));
180    // ANCHOR_END: streaming
181
182    // ANCHOR: streaming_edit
183    // Edit the streamed floor, walk far away, come back. The active
184    // set follows the camera: the old region evicts (edited chunks
185    // are handed to the ChunkStore first) while a fresh set streams
186    // in around the new position. On return the store wins over the
187    // generator, so the edit survives; without a store, eviction
188    // would silently revert the chunk to generator output.
189    scene
190        .grid_mut(stream_id)
191        .expect("still there")
192        .set_voxel(IVec3::new(5, 5, 240), None); // dig a hole
193    scene.pump_streaming_sync(camera_near + DVec3::new(0.0, 100_000.0, 0.0));
194    let grid = scene.grid(stream_id).expect("still there");
195    assert!(grid.chunk(IVec3::ZERO).is_none()); // home chunk evicted…
196    assert!(grid.chunk_count() > 0); // …but the far region streamed in
197    scene.pump_streaming_sync(camera_near);
198    let grid = scene.grid(stream_id).expect("re-streamed");
199    // The hole survived evict + re-stream.
200    assert!(!grid.voxel_solid(IVec3::new(5, 5, 240)));
201    // ANCHOR_END: streaming_edit
202
203    // ANCHOR: grid_scale
204    // Per-grid scale: `voxel_world_size` is world units per voxel. A grid
205    // built with `at_scale(origin, 2.0)` has voxels twice world size — a
206    // coarse "planet" — while `0.25` gives a fine detail grid; both coexist
207    // at their true relative sizes. Only the world↔grid boundary scales:
208    // edits + queries stay in voxels, unchanged.
209    let mut scaled = Scene::new();
210    let planet = scaled.add_grid(GridTransform::at_scale(DVec3::ZERO, 2.0));
211    scaled
212        .grid_mut(planet)
213        .expect("just added")
214        // Same voxel edit API — grid-local coordinates.
215        .set_voxel(IVec3::new(5, 5, 10), Some(STONE));
216    // Grid-local voxel z=10 sits at WORLD z = 20 (10 voxels × 2.0). A world
217    // raycast down that column reports the grid-local voxel it hit AND a
218    // WORLD-space `t` — so hits across grids of different scale compare
219    // correctly (the raycaster marches voxels but returns world distance).
220    let hit = scaled
221        .raycast(DVec3::new(11.0, 11.0, 0.0), DVec3::new(0.0, 0.0, 1.0), 64.0)
222        .expect("ray hits the scaled voxel");
223    assert_eq!(hit.voxel, IVec3::new(5, 5, 10)); // grid-local, unscaled
224    assert!((hit.t - 20.0).abs() < 1e-4); // WORLD distance: 10 voxels × 2.0
225                                          // ANCHOR_END: grid_scale
226
227    println!("book_scene_graph: all scene-graph assertions hold");
228}
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 141-144)
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        voxel_world_size: 1.0,
94    });
95    // Object grids should not paint their own (grid-local, rotating)
96    // sky — leave the sky to the world grid.
97    scene.grid_mut(ship_id).expect("just added").render_sky = false;
98    // ANCHOR_END: scene_grids
99
100    // ANCHOR: edits
101    let ground = scene.grid_mut(ground_id).expect("just added");
102    // Solid slab: grid-local voxel coords, inclusive on both ends,
103    // decomposed across as many chunks as it spans (here 4×4).
104    ground.set_rect(
105        IVec3::new(-200, -200, 200),
106        IVec3::new(199, 199, 254),
107        Some(GRASS),
108    );
109    // `Some(colour)` inserts, `None` carves back to air.
110    ground.set_sphere(IVec3::new(0, 0, 195), 12, Some(STONE));
111    ground.set_rect(IVec3::new(-3, -200, 190), IVec3::new(3, 199, 196), None);
112    // ANCHOR_END: edits
113
114    // ANCHOR: queries
115    // Solid tests: inside the slab, then inside the carved tunnel.
116    assert!(ground.voxel_solid(IVec3::new(0, 0, 210)));
117    assert!(!ground.voxel_solid(IVec3::new(0, 100, 193)));
118    // Colour queries answer on surface voxels; deep interior cells
119    // are untextured in the slab format and read as `None`.
120    assert_eq!(ground.voxel_color(IVec3::new(50, 50, 200)), Some(GRASS));
121    // ANCHOR_END: queries
122
123    // ANCHOR: recolour
124    // GOTCHA: inserting over already-solid voxels does NOT repaint
125    // them — span insertion only fills air. Recolour = carve, then
126    // insert.
127    let (lo, hi) = (IVec3::new(20, 20, 200), IVec3::new(24, 24, 204));
128    ground.set_rect(lo, hi, Some(RED));
129    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(GRASS)); // unchanged!
130    ground.set_rect(lo, hi, None); // carve…
131    ground.set_rect(lo, hi, Some(RED)); // …then insert
132    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
133    // ANCHOR_END: recolour
134
135    // ANCHOR: colfunc
136    // A carve exposes fresh interior walls; the plain edits paint
137    // them colour 0 (black). The `_with_colfunc` variants ask a
138    // closure instead — it sees grid-local coordinates, so gradients
139    // stay continuous across chunk seams. Here: a crater whose walls
140    // darken with depth.
141    ground.set_sphere_with_colfunc(IVec3::new(60, 60, 200), 8, SpanOp::Carve, |_x, _y, z| {
142        let depth = (z - 192).clamp(0, 15) as u8;
143        VoxColor::rgb(0x6b, 0x40 + depth * 3, 0x2f)
144    });
145    // ANCHOR_END: colfunc
146
147    // ANCHOR: snapshot
148    // Name grids before saving: ids survive the round-trip, but the
149    // generator / store hooks are host code and cannot be serialised
150    // — the name is your key for rebinding them after a load.
151    scene.grid_mut(ground_id).expect("ground exists").name = Some("ground".into());
152    let bytes = scene.save_snapshot(); // versioned envelope
153    let restored = Scene::load_snapshot(&bytes).expect("self-authored snapshot");
154    assert_eq!(restored.grid_count(), 2);
155    let (_, ground2) = restored
156        .grids()
157        .find(|(_, g)| g.name.as_deref() == Some("ground"))
158        .expect("rebind by name");
159    assert_eq!(ground2.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
160    // ANCHOR_END: snapshot
161
162    // ANCHOR: streaming
163    // Streaming: attach a generator + radii to a grid, then pump once
164    // per frame with the camera's world position. Chunks within
165    // `r_active` stream in; chunks beyond `r_evict` drop out; the band
166    // between is hysteresis so a camera hovering on a boundary
167    // doesn't thrash.
168    let stream_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 4096.0, 0.0)));
169    let store = Arc::new(MemoryStore::default());
170    let grid = scene.grid_mut(stream_id).expect("just added");
171    grid.set_generator(Some(Arc::new(FloorGenerator)));
172    grid.set_chunk_store(Some(store));
173    grid.stream_radius = StreamRadius::new(256.0, 384.0);
174
175    let camera_near = DVec3::new(0.0, 4096.0, 100.0);
176    scene.pump_streaming_sync(camera_near); // real games: `pump_streaming`
177    let grid = scene.grid(stream_id).expect("still there");
178    assert!(grid.chunk_count() > 0); // floor streamed in around the camera
179    assert!(grid.voxel_solid(IVec3::new(5, 5, 240)));
180    // ANCHOR_END: streaming
181
182    // ANCHOR: streaming_edit
183    // Edit the streamed floor, walk far away, come back. The active
184    // set follows the camera: the old region evicts (edited chunks
185    // are handed to the ChunkStore first) while a fresh set streams
186    // in around the new position. On return the store wins over the
187    // generator, so the edit survives; without a store, eviction
188    // would silently revert the chunk to generator output.
189    scene
190        .grid_mut(stream_id)
191        .expect("still there")
192        .set_voxel(IVec3::new(5, 5, 240), None); // dig a hole
193    scene.pump_streaming_sync(camera_near + DVec3::new(0.0, 100_000.0, 0.0));
194    let grid = scene.grid(stream_id).expect("still there");
195    assert!(grid.chunk(IVec3::ZERO).is_none()); // home chunk evicted…
196    assert!(grid.chunk_count() > 0); // …but the far region streamed in
197    scene.pump_streaming_sync(camera_near);
198    let grid = scene.grid(stream_id).expect("re-streamed");
199    // The hole survived evict + re-stream.
200    assert!(!grid.voxel_solid(IVec3::new(5, 5, 240)));
201    // ANCHOR_END: streaming_edit
202
203    // ANCHOR: grid_scale
204    // Per-grid scale: `voxel_world_size` is world units per voxel. A grid
205    // built with `at_scale(origin, 2.0)` has voxels twice world size — a
206    // coarse "planet" — while `0.25` gives a fine detail grid; both coexist
207    // at their true relative sizes. Only the world↔grid boundary scales:
208    // edits + queries stay in voxels, unchanged.
209    let mut scaled = Scene::new();
210    let planet = scaled.add_grid(GridTransform::at_scale(DVec3::ZERO, 2.0));
211    scaled
212        .grid_mut(planet)
213        .expect("just added")
214        // Same voxel edit API — grid-local coordinates.
215        .set_voxel(IVec3::new(5, 5, 10), Some(STONE));
216    // Grid-local voxel z=10 sits at WORLD z = 20 (10 voxels × 2.0). A world
217    // raycast down that column reports the grid-local voxel it hit AND a
218    // WORLD-space `t` — so hits across grids of different scale compare
219    // correctly (the raycaster marches voxels but returns world distance).
220    let hit = scaled
221        .raycast(DVec3::new(11.0, 11.0, 0.0), DVec3::new(0.0, 0.0, 1.0), 64.0)
222        .expect("ray hits the scaled voxel");
223    assert_eq!(hit.voxel, IVec3::new(5, 5, 10)); // grid-local, unscaled
224    assert!((hit.t - 20.0).abs() < 1e-4); // WORLD distance: 10 voxels × 2.0
225                                          // ANCHOR_END: grid_scale
226
227    println!("book_scene_graph: all scene-graph assertions hold");
228}
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 add_water_volume(&mut self, a: IVec3, b: IVec3)

Register a water volume spanning the two grid-local voxel corners (any order). Convenience over pushing onto water_volumes directly — this normalises the corners.

Examples found in repository?
examples/book_controller.rs (line 148)
133fn swim_demo() {
134    // ANCHOR: water
135    // Physics water is a WaterVolume on the grid — a grid-local voxel
136    // AABB whose TOP face (min z — +z is down) is the surface. It is
137    // independent of the voxels: fill the same region with a
138    // volumetric-material shell for the visuals, or don't.
139    let mut scene = Scene::new();
140    let id = scene.add_grid(GridTransform::identity());
141    let g = scene.grid_mut(id).expect("grid");
142    // Basin floor, then 20 voxels of water above it (surface z = 100).
143    g.set_rect(
144        IVec3::new(20, 20, 120),
145        IVec3::new(140, 140, 130),
146        Some(VoxColor::rgb(0x50, 0x90, 0x50)),
147    );
148    g.add_water_volume(IVec3::new(20, 20, 100), IVec3::new(140, 140, 119));
149
150    // A Walk-mode body dropped in swims AUTOMATICALLY once submerged
151    // past `swim_enter_frac` of its height: buoyancy floats it to a
152    // bob at the surface, `jump` strokes up, `sink` strokes down, and
153    // a jump with the head above water breaches.
154    let mut body = CharacterBody::new(CharacterDef::default());
155    body.teleport(DVec3::new(80.0, 80.0, 115.0)); // deep in the pool
156    for _ in 0..600 {
157        body.walk(&scene, 1.0 / 60.0, WalkInput::default());
158    }
159    assert!(body.is_swimming(), "floated to the surface bob");
160    assert!(!body.eye_in_water(&scene), "bobbing: the camera is dry");
161
162    // Dive: hold `sink`. The submerged EYE is the hook for the
163    // underwater feel — drive the WT.2 frame tint and the WT.3
164    // listener lowpass from this one flag.
165    for _ in 0..90 {
166        body.walk(
167            &scene,
168            1.0 / 60.0,
169            WalkInput {
170                sink: true,
171                ..WalkInput::default()
172            },
173        );
174    }
175    assert!(body.eye_in_water(&scene), "diving: tint + muffle now");
176    // ANCHOR_END: water
177}
Source

pub fn water_depth_local(&self, p: DVec3) -> Option<f64>

Depth of a continuous grid-local point below the water surface, in voxel units — the deepest submersion across this grid’s volumes (overlapping volumes: the highest surface wins). None when the point is in no volume.

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 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        voxel_world_size: 1.0,
94    });
95    // Object grids should not paint their own (grid-local, rotating)
96    // sky — leave the sky to the world grid.
97    scene.grid_mut(ship_id).expect("just added").render_sky = false;
98    // ANCHOR_END: scene_grids
99
100    // ANCHOR: edits
101    let ground = scene.grid_mut(ground_id).expect("just added");
102    // Solid slab: grid-local voxel coords, inclusive on both ends,
103    // decomposed across as many chunks as it spans (here 4×4).
104    ground.set_rect(
105        IVec3::new(-200, -200, 200),
106        IVec3::new(199, 199, 254),
107        Some(GRASS),
108    );
109    // `Some(colour)` inserts, `None` carves back to air.
110    ground.set_sphere(IVec3::new(0, 0, 195), 12, Some(STONE));
111    ground.set_rect(IVec3::new(-3, -200, 190), IVec3::new(3, 199, 196), None);
112    // ANCHOR_END: edits
113
114    // ANCHOR: queries
115    // Solid tests: inside the slab, then inside the carved tunnel.
116    assert!(ground.voxel_solid(IVec3::new(0, 0, 210)));
117    assert!(!ground.voxel_solid(IVec3::new(0, 100, 193)));
118    // Colour queries answer on surface voxels; deep interior cells
119    // are untextured in the slab format and read as `None`.
120    assert_eq!(ground.voxel_color(IVec3::new(50, 50, 200)), Some(GRASS));
121    // ANCHOR_END: queries
122
123    // ANCHOR: recolour
124    // GOTCHA: inserting over already-solid voxels does NOT repaint
125    // them — span insertion only fills air. Recolour = carve, then
126    // insert.
127    let (lo, hi) = (IVec3::new(20, 20, 200), IVec3::new(24, 24, 204));
128    ground.set_rect(lo, hi, Some(RED));
129    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(GRASS)); // unchanged!
130    ground.set_rect(lo, hi, None); // carve…
131    ground.set_rect(lo, hi, Some(RED)); // …then insert
132    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
133    // ANCHOR_END: recolour
134
135    // ANCHOR: colfunc
136    // A carve exposes fresh interior walls; the plain edits paint
137    // them colour 0 (black). The `_with_colfunc` variants ask a
138    // closure instead — it sees grid-local coordinates, so gradients
139    // stay continuous across chunk seams. Here: a crater whose walls
140    // darken with depth.
141    ground.set_sphere_with_colfunc(IVec3::new(60, 60, 200), 8, SpanOp::Carve, |_x, _y, z| {
142        let depth = (z - 192).clamp(0, 15) as u8;
143        VoxColor::rgb(0x6b, 0x40 + depth * 3, 0x2f)
144    });
145    // ANCHOR_END: colfunc
146
147    // ANCHOR: snapshot
148    // Name grids before saving: ids survive the round-trip, but the
149    // generator / store hooks are host code and cannot be serialised
150    // — the name is your key for rebinding them after a load.
151    scene.grid_mut(ground_id).expect("ground exists").name = Some("ground".into());
152    let bytes = scene.save_snapshot(); // versioned envelope
153    let restored = Scene::load_snapshot(&bytes).expect("self-authored snapshot");
154    assert_eq!(restored.grid_count(), 2);
155    let (_, ground2) = restored
156        .grids()
157        .find(|(_, g)| g.name.as_deref() == Some("ground"))
158        .expect("rebind by name");
159    assert_eq!(ground2.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
160    // ANCHOR_END: snapshot
161
162    // ANCHOR: streaming
163    // Streaming: attach a generator + radii to a grid, then pump once
164    // per frame with the camera's world position. Chunks within
165    // `r_active` stream in; chunks beyond `r_evict` drop out; the band
166    // between is hysteresis so a camera hovering on a boundary
167    // doesn't thrash.
168    let stream_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 4096.0, 0.0)));
169    let store = Arc::new(MemoryStore::default());
170    let grid = scene.grid_mut(stream_id).expect("just added");
171    grid.set_generator(Some(Arc::new(FloorGenerator)));
172    grid.set_chunk_store(Some(store));
173    grid.stream_radius = StreamRadius::new(256.0, 384.0);
174
175    let camera_near = DVec3::new(0.0, 4096.0, 100.0);
176    scene.pump_streaming_sync(camera_near); // real games: `pump_streaming`
177    let grid = scene.grid(stream_id).expect("still there");
178    assert!(grid.chunk_count() > 0); // floor streamed in around the camera
179    assert!(grid.voxel_solid(IVec3::new(5, 5, 240)));
180    // ANCHOR_END: streaming
181
182    // ANCHOR: streaming_edit
183    // Edit the streamed floor, walk far away, come back. The active
184    // set follows the camera: the old region evicts (edited chunks
185    // are handed to the ChunkStore first) while a fresh set streams
186    // in around the new position. On return the store wins over the
187    // generator, so the edit survives; without a store, eviction
188    // would silently revert the chunk to generator output.
189    scene
190        .grid_mut(stream_id)
191        .expect("still there")
192        .set_voxel(IVec3::new(5, 5, 240), None); // dig a hole
193    scene.pump_streaming_sync(camera_near + DVec3::new(0.0, 100_000.0, 0.0));
194    let grid = scene.grid(stream_id).expect("still there");
195    assert!(grid.chunk(IVec3::ZERO).is_none()); // home chunk evicted…
196    assert!(grid.chunk_count() > 0); // …but the far region streamed in
197    scene.pump_streaming_sync(camera_near);
198    let grid = scene.grid(stream_id).expect("re-streamed");
199    // The hole survived evict + re-stream.
200    assert!(!grid.voxel_solid(IVec3::new(5, 5, 240)));
201    // ANCHOR_END: streaming_edit
202
203    // ANCHOR: grid_scale
204    // Per-grid scale: `voxel_world_size` is world units per voxel. A grid
205    // built with `at_scale(origin, 2.0)` has voxels twice world size — a
206    // coarse "planet" — while `0.25` gives a fine detail grid; both coexist
207    // at their true relative sizes. Only the world↔grid boundary scales:
208    // edits + queries stay in voxels, unchanged.
209    let mut scaled = Scene::new();
210    let planet = scaled.add_grid(GridTransform::at_scale(DVec3::ZERO, 2.0));
211    scaled
212        .grid_mut(planet)
213        .expect("just added")
214        // Same voxel edit API — grid-local coordinates.
215        .set_voxel(IVec3::new(5, 5, 10), Some(STONE));
216    // Grid-local voxel z=10 sits at WORLD z = 20 (10 voxels × 2.0). A world
217    // raycast down that column reports the grid-local voxel it hit AND a
218    // WORLD-space `t` — so hits across grids of different scale compare
219    // correctly (the raycaster marches voxels but returns world distance).
220    let hit = scaled
221        .raycast(DVec3::new(11.0, 11.0, 0.0), DVec3::new(0.0, 0.0, 1.0), 64.0)
222        .expect("ray hits the scaled voxel");
223    assert_eq!(hit.voxel, IVec3::new(5, 5, 10)); // grid-local, unscaled
224    assert!((hit.t - 20.0).abs() < 1e-4); // WORLD distance: 10 voxels × 2.0
225                                          // ANCHOR_END: grid_scale
226
227    println!("book_scene_graph: all scene-graph assertions hold");
228}
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 172)
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        voxel_world_size: 1.0,
94    });
95    // Object grids should not paint their own (grid-local, rotating)
96    // sky — leave the sky to the world grid.
97    scene.grid_mut(ship_id).expect("just added").render_sky = false;
98    // ANCHOR_END: scene_grids
99
100    // ANCHOR: edits
101    let ground = scene.grid_mut(ground_id).expect("just added");
102    // Solid slab: grid-local voxel coords, inclusive on both ends,
103    // decomposed across as many chunks as it spans (here 4×4).
104    ground.set_rect(
105        IVec3::new(-200, -200, 200),
106        IVec3::new(199, 199, 254),
107        Some(GRASS),
108    );
109    // `Some(colour)` inserts, `None` carves back to air.
110    ground.set_sphere(IVec3::new(0, 0, 195), 12, Some(STONE));
111    ground.set_rect(IVec3::new(-3, -200, 190), IVec3::new(3, 199, 196), None);
112    // ANCHOR_END: edits
113
114    // ANCHOR: queries
115    // Solid tests: inside the slab, then inside the carved tunnel.
116    assert!(ground.voxel_solid(IVec3::new(0, 0, 210)));
117    assert!(!ground.voxel_solid(IVec3::new(0, 100, 193)));
118    // Colour queries answer on surface voxels; deep interior cells
119    // are untextured in the slab format and read as `None`.
120    assert_eq!(ground.voxel_color(IVec3::new(50, 50, 200)), Some(GRASS));
121    // ANCHOR_END: queries
122
123    // ANCHOR: recolour
124    // GOTCHA: inserting over already-solid voxels does NOT repaint
125    // them — span insertion only fills air. Recolour = carve, then
126    // insert.
127    let (lo, hi) = (IVec3::new(20, 20, 200), IVec3::new(24, 24, 204));
128    ground.set_rect(lo, hi, Some(RED));
129    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(GRASS)); // unchanged!
130    ground.set_rect(lo, hi, None); // carve…
131    ground.set_rect(lo, hi, Some(RED)); // …then insert
132    assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
133    // ANCHOR_END: recolour
134
135    // ANCHOR: colfunc
136    // A carve exposes fresh interior walls; the plain edits paint
137    // them colour 0 (black). The `_with_colfunc` variants ask a
138    // closure instead — it sees grid-local coordinates, so gradients
139    // stay continuous across chunk seams. Here: a crater whose walls
140    // darken with depth.
141    ground.set_sphere_with_colfunc(IVec3::new(60, 60, 200), 8, SpanOp::Carve, |_x, _y, z| {
142        let depth = (z - 192).clamp(0, 15) as u8;
143        VoxColor::rgb(0x6b, 0x40 + depth * 3, 0x2f)
144    });
145    // ANCHOR_END: colfunc
146
147    // ANCHOR: snapshot
148    // Name grids before saving: ids survive the round-trip, but the
149    // generator / store hooks are host code and cannot be serialised
150    // — the name is your key for rebinding them after a load.
151    scene.grid_mut(ground_id).expect("ground exists").name = Some("ground".into());
152    let bytes = scene.save_snapshot(); // versioned envelope
153    let restored = Scene::load_snapshot(&bytes).expect("self-authored snapshot");
154    assert_eq!(restored.grid_count(), 2);
155    let (_, ground2) = restored
156        .grids()
157        .find(|(_, g)| g.name.as_deref() == Some("ground"))
158        .expect("rebind by name");
159    assert_eq!(ground2.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
160    // ANCHOR_END: snapshot
161
162    // ANCHOR: streaming
163    // Streaming: attach a generator + radii to a grid, then pump once
164    // per frame with the camera's world position. Chunks within
165    // `r_active` stream in; chunks beyond `r_evict` drop out; the band
166    // between is hysteresis so a camera hovering on a boundary
167    // doesn't thrash.
168    let stream_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 4096.0, 0.0)));
169    let store = Arc::new(MemoryStore::default());
170    let grid = scene.grid_mut(stream_id).expect("just added");
171    grid.set_generator(Some(Arc::new(FloorGenerator)));
172    grid.set_chunk_store(Some(store));
173    grid.stream_radius = StreamRadius::new(256.0, 384.0);
174
175    let camera_near = DVec3::new(0.0, 4096.0, 100.0);
176    scene.pump_streaming_sync(camera_near); // real games: `pump_streaming`
177    let grid = scene.grid(stream_id).expect("still there");
178    assert!(grid.chunk_count() > 0); // floor streamed in around the camera
179    assert!(grid.voxel_solid(IVec3::new(5, 5, 240)));
180    // ANCHOR_END: streaming
181
182    // ANCHOR: streaming_edit
183    // Edit the streamed floor, walk far away, come back. The active
184    // set follows the camera: the old region evicts (edited chunks
185    // are handed to the ChunkStore first) while a fresh set streams
186    // in around the new position. On return the store wins over the
187    // generator, so the edit survives; without a store, eviction
188    // would silently revert the chunk to generator output.
189    scene
190        .grid_mut(stream_id)
191        .expect("still there")
192        .set_voxel(IVec3::new(5, 5, 240), None); // dig a hole
193    scene.pump_streaming_sync(camera_near + DVec3::new(0.0, 100_000.0, 0.0));
194    let grid = scene.grid(stream_id).expect("still there");
195    assert!(grid.chunk(IVec3::ZERO).is_none()); // home chunk evicted…
196    assert!(grid.chunk_count() > 0); // …but the far region streamed in
197    scene.pump_streaming_sync(camera_near);
198    let grid = scene.grid(stream_id).expect("re-streamed");
199    // The hole survived evict + re-stream.
200    assert!(!grid.voxel_solid(IVec3::new(5, 5, 240)));
201    // ANCHOR_END: streaming_edit
202
203    // ANCHOR: grid_scale
204    // Per-grid scale: `voxel_world_size` is world units per voxel. A grid
205    // built with `at_scale(origin, 2.0)` has voxels twice world size — a
206    // coarse "planet" — while `0.25` gives a fine detail grid; both coexist
207    // at their true relative sizes. Only the world↔grid boundary scales:
208    // edits + queries stay in voxels, unchanged.
209    let mut scaled = Scene::new();
210    let planet = scaled.add_grid(GridTransform::at_scale(DVec3::ZERO, 2.0));
211    scaled
212        .grid_mut(planet)
213        .expect("just added")
214        // Same voxel edit API — grid-local coordinates.
215        .set_voxel(IVec3::new(5, 5, 10), Some(STONE));
216    // Grid-local voxel z=10 sits at WORLD z = 20 (10 voxels × 2.0). A world
217    // raycast down that column reports the grid-local voxel it hit AND a
218    // WORLD-space `t` — so hits across grids of different scale compare
219    // correctly (the raycaster marches voxels but returns world distance).
220    let hit = scaled
221        .raycast(DVec3::new(11.0, 11.0, 0.0), DVec3::new(0.0, 0.0, 1.0), 64.0)
222        .expect("ray hits the scaled voxel");
223    assert_eq!(hit.voxel, IVec3::new(5, 5, 10)); // grid-local, unscaled
224    assert!((hit.t - 20.0).abs() < 1e-4); // WORLD distance: 10 voxels × 2.0
225                                          // ANCHOR_END: grid_scale
226
227    println!("book_scene_graph: all scene-graph assertions hold");
228}
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 world space (SC.3 — the voxel half-extent is scaled by voxel_world_size, so it pairs directly with the world-distance LOD thresholds in crate::LodThresholds::from_radius).

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.