pub struct Grid {Show 13 fields
pub transform: GridTransform,
pub chunks: HashMap<IVec3, Vxl>,
pub render_sky: bool,
pub mip_levels_override: Option<u32>,
pub lod_thresholds: LodThresholds,
pub billboards: Option<BillboardCache>,
pub generator: Option<Arc<dyn ChunkGenerator>>,
pub name: Option<String>,
pub store: Option<Arc<dyn ChunkStore>>,
pub stream_radius: StreamRadius,
pub bake_lights: Vec<BakeLight>,
pub pending_gen: HashSet<IVec3>,
pub dda_brick_cache: BrickCache,
/* private fields */
}Expand description
One independent voxel grid in a scene. Holds its world placement and a sparse map of populated chunks. Empty chunk slots are implicit air and skipped during rendering / raycasts.
Each chunk is internally a Vxl with vsid = CHUNK_SIZE_XY
— the existing per-chunk renderer (opticast + grouscan +
sprites + lighting in roxlap-core) runs on each chunk
unchanged. Vertical worlds are built by stacking chunks along
grid-local +z.
Fields§
§transform: GridTransformWorld 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: boolWhether 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: LodThresholdsWorld-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: StreamRadiusStreaming activity / eviction radii used by
Scene::pump_streaming_sync (S7.1). Defaults to
StreamRadius::DISABLED so existing grids see no change
in behaviour until the caller opts in.
bake_lights: Vec<BakeLight>EV.3 — baked point lights (BakeLight, grid-local voxel
coords) consumed by BakeMode::PointLights: Grid::bake
and Grid::bake_bbox write each light’s Lambertian pool
into the brightness bytes, so incremental carve relights keep
their glow. Authoring state only — editing this list does
not rebake by itself (call Grid::bake after) and it is
not carried through snapshots (the baked bytes are; re-set the
list after a load if you keep editing). Ignored by the other
bake modes and by the dynamic LightRig.
pending_gen: HashSet<IVec3>In-flight background generation tasks (S7.3).
Populated by Scene::pump_streaming when it dispatches a
generator call onto the streaming rayon pool, drained when
the corresponding ChunkResult is received and processed
(either installed or discarded). The set is consulted to
avoid re-dispatching the same chunk while a previous task
is still running.
Stays empty when only the synchronous
Scene::pump_streaming_sync is used — that path generates
inline on the calling thread.
dda_brick_cache: BrickCacheCross-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
impl Grid
Sourcepub fn voxel_solid(&self, voxel: IVec3) -> bool
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?
83fn main() {
84 // ANCHOR: scene_grids
85 let mut scene = Scene::new();
86 // A static "world" grid at the origin…
87 let ground_id = scene.add_grid(GridTransform::at(DVec3::ZERO));
88 // …and a second grid placed and rotated independently — its own
89 // f64 world position + quaternion, same chunked voxel payload.
90 let ship_id = scene.add_grid(GridTransform {
91 origin: DVec3::new(300.0, -80.0, -40.0),
92 rotation: DQuat::from_rotation_z(0.5),
93 });
94 // Object grids should not paint their own (grid-local, rotating)
95 // sky — leave the sky to the world grid.
96 scene.grid_mut(ship_id).expect("just added").render_sky = false;
97 // ANCHOR_END: scene_grids
98
99 // ANCHOR: edits
100 let ground = scene.grid_mut(ground_id).expect("just added");
101 // Solid slab: grid-local voxel coords, inclusive on both ends,
102 // decomposed across as many chunks as it spans (here 4×4).
103 ground.set_rect(
104 IVec3::new(-200, -200, 200),
105 IVec3::new(199, 199, 254),
106 Some(GRASS),
107 );
108 // `Some(colour)` inserts, `None` carves back to air.
109 ground.set_sphere(IVec3::new(0, 0, 195), 12, Some(STONE));
110 ground.set_rect(IVec3::new(-3, -200, 190), IVec3::new(3, 199, 196), None);
111 // ANCHOR_END: edits
112
113 // ANCHOR: queries
114 // Solid tests: inside the slab, then inside the carved tunnel.
115 assert!(ground.voxel_solid(IVec3::new(0, 0, 210)));
116 assert!(!ground.voxel_solid(IVec3::new(0, 100, 193)));
117 // Colour queries answer on surface voxels; deep interior cells
118 // are untextured in the slab format and read as `None`.
119 assert_eq!(ground.voxel_color(IVec3::new(50, 50, 200)), Some(GRASS));
120 // ANCHOR_END: queries
121
122 // ANCHOR: recolour
123 // GOTCHA: inserting over already-solid voxels does NOT repaint
124 // them — span insertion only fills air. Recolour = carve, then
125 // insert.
126 let (lo, hi) = (IVec3::new(20, 20, 200), IVec3::new(24, 24, 204));
127 ground.set_rect(lo, hi, Some(RED));
128 assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(GRASS)); // unchanged!
129 ground.set_rect(lo, hi, None); // carve…
130 ground.set_rect(lo, hi, Some(RED)); // …then insert
131 assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
132 // ANCHOR_END: recolour
133
134 // ANCHOR: colfunc
135 // A carve exposes fresh interior walls; the plain edits paint
136 // them colour 0 (black). The `_with_colfunc` variants ask a
137 // closure instead — it sees grid-local coordinates, so gradients
138 // stay continuous across chunk seams. Here: a crater whose walls
139 // darken with depth.
140 ground.set_sphere_with_colfunc(IVec3::new(60, 60, 200), 8, SpanOp::Carve, |_x, _y, z| {
141 let depth = (z - 192).clamp(0, 15) as u8;
142 VoxColor::rgb(0x6b, 0x40 + depth * 3, 0x2f)
143 });
144 // ANCHOR_END: colfunc
145
146 // ANCHOR: snapshot
147 // Name grids before saving: ids survive the round-trip, but the
148 // generator / store hooks are host code and cannot be serialised
149 // — the name is your key for rebinding them after a load.
150 scene.grid_mut(ground_id).expect("ground exists").name = Some("ground".into());
151 let bytes = scene.save_snapshot(); // versioned envelope
152 let restored = Scene::load_snapshot(&bytes).expect("self-authored snapshot");
153 assert_eq!(restored.grid_count(), 2);
154 let (_, ground2) = restored
155 .grids()
156 .find(|(_, g)| g.name.as_deref() == Some("ground"))
157 .expect("rebind by name");
158 assert_eq!(ground2.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
159 // ANCHOR_END: snapshot
160
161 // ANCHOR: streaming
162 // Streaming: attach a generator + radii to a grid, then pump once
163 // per frame with the camera's world position. Chunks within
164 // `r_active` stream in; chunks beyond `r_evict` drop out; the band
165 // between is hysteresis so a camera hovering on a boundary
166 // doesn't thrash.
167 let stream_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 4096.0, 0.0)));
168 let store = Arc::new(MemoryStore::default());
169 let grid = scene.grid_mut(stream_id).expect("just added");
170 grid.set_generator(Some(Arc::new(FloorGenerator)));
171 grid.set_chunk_store(Some(store));
172 grid.stream_radius = StreamRadius::new(256.0, 384.0);
173
174 let camera_near = DVec3::new(0.0, 4096.0, 100.0);
175 scene.pump_streaming_sync(camera_near); // real games: `pump_streaming`
176 let grid = scene.grid(stream_id).expect("still there");
177 assert!(grid.chunk_count() > 0); // floor streamed in around the camera
178 assert!(grid.voxel_solid(IVec3::new(5, 5, 240)));
179 // ANCHOR_END: streaming
180
181 // ANCHOR: streaming_edit
182 // Edit the streamed floor, walk far away, come back. The active
183 // set follows the camera: the old region evicts (edited chunks
184 // are handed to the ChunkStore first) while a fresh set streams
185 // in around the new position. On return the store wins over the
186 // generator, so the edit survives; without a store, eviction
187 // would silently revert the chunk to generator output.
188 scene
189 .grid_mut(stream_id)
190 .expect("still there")
191 .set_voxel(IVec3::new(5, 5, 240), None); // dig a hole
192 scene.pump_streaming_sync(camera_near + DVec3::new(0.0, 100_000.0, 0.0));
193 let grid = scene.grid(stream_id).expect("still there");
194 assert!(grid.chunk(IVec3::ZERO).is_none()); // home chunk evicted…
195 assert!(grid.chunk_count() > 0); // …but the far region streamed in
196 scene.pump_streaming_sync(camera_near);
197 let grid = scene.grid(stream_id).expect("re-streamed");
198 // The hole survived evict + re-stream.
199 assert!(!grid.voxel_solid(IVec3::new(5, 5, 240)));
200 // ANCHOR_END: streaming_edit
201
202 println!("book_scene_graph: all scene-graph assertions hold");
203}Sourcepub fn chunk_voxel_solid(vxl: &Vxl, in_chunk: UVec3) -> bool
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.
Sourcepub fn voxel_color(&self, voxel: IVec3) -> Option<VoxColor>
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?
83fn main() {
84 // ANCHOR: scene_grids
85 let mut scene = Scene::new();
86 // A static "world" grid at the origin…
87 let ground_id = scene.add_grid(GridTransform::at(DVec3::ZERO));
88 // …and a second grid placed and rotated independently — its own
89 // f64 world position + quaternion, same chunked voxel payload.
90 let ship_id = scene.add_grid(GridTransform {
91 origin: DVec3::new(300.0, -80.0, -40.0),
92 rotation: DQuat::from_rotation_z(0.5),
93 });
94 // Object grids should not paint their own (grid-local, rotating)
95 // sky — leave the sky to the world grid.
96 scene.grid_mut(ship_id).expect("just added").render_sky = false;
97 // ANCHOR_END: scene_grids
98
99 // ANCHOR: edits
100 let ground = scene.grid_mut(ground_id).expect("just added");
101 // Solid slab: grid-local voxel coords, inclusive on both ends,
102 // decomposed across as many chunks as it spans (here 4×4).
103 ground.set_rect(
104 IVec3::new(-200, -200, 200),
105 IVec3::new(199, 199, 254),
106 Some(GRASS),
107 );
108 // `Some(colour)` inserts, `None` carves back to air.
109 ground.set_sphere(IVec3::new(0, 0, 195), 12, Some(STONE));
110 ground.set_rect(IVec3::new(-3, -200, 190), IVec3::new(3, 199, 196), None);
111 // ANCHOR_END: edits
112
113 // ANCHOR: queries
114 // Solid tests: inside the slab, then inside the carved tunnel.
115 assert!(ground.voxel_solid(IVec3::new(0, 0, 210)));
116 assert!(!ground.voxel_solid(IVec3::new(0, 100, 193)));
117 // Colour queries answer on surface voxels; deep interior cells
118 // are untextured in the slab format and read as `None`.
119 assert_eq!(ground.voxel_color(IVec3::new(50, 50, 200)), Some(GRASS));
120 // ANCHOR_END: queries
121
122 // ANCHOR: recolour
123 // GOTCHA: inserting over already-solid voxels does NOT repaint
124 // them — span insertion only fills air. Recolour = carve, then
125 // insert.
126 let (lo, hi) = (IVec3::new(20, 20, 200), IVec3::new(24, 24, 204));
127 ground.set_rect(lo, hi, Some(RED));
128 assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(GRASS)); // unchanged!
129 ground.set_rect(lo, hi, None); // carve…
130 ground.set_rect(lo, hi, Some(RED)); // …then insert
131 assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
132 // ANCHOR_END: recolour
133
134 // ANCHOR: colfunc
135 // A carve exposes fresh interior walls; the plain edits paint
136 // them colour 0 (black). The `_with_colfunc` variants ask a
137 // closure instead — it sees grid-local coordinates, so gradients
138 // stay continuous across chunk seams. Here: a crater whose walls
139 // darken with depth.
140 ground.set_sphere_with_colfunc(IVec3::new(60, 60, 200), 8, SpanOp::Carve, |_x, _y, z| {
141 let depth = (z - 192).clamp(0, 15) as u8;
142 VoxColor::rgb(0x6b, 0x40 + depth * 3, 0x2f)
143 });
144 // ANCHOR_END: colfunc
145
146 // ANCHOR: snapshot
147 // Name grids before saving: ids survive the round-trip, but the
148 // generator / store hooks are host code and cannot be serialised
149 // — the name is your key for rebinding them after a load.
150 scene.grid_mut(ground_id).expect("ground exists").name = Some("ground".into());
151 let bytes = scene.save_snapshot(); // versioned envelope
152 let restored = Scene::load_snapshot(&bytes).expect("self-authored snapshot");
153 assert_eq!(restored.grid_count(), 2);
154 let (_, ground2) = restored
155 .grids()
156 .find(|(_, g)| g.name.as_deref() == Some("ground"))
157 .expect("rebind by name");
158 assert_eq!(ground2.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
159 // ANCHOR_END: snapshot
160
161 // ANCHOR: streaming
162 // Streaming: attach a generator + radii to a grid, then pump once
163 // per frame with the camera's world position. Chunks within
164 // `r_active` stream in; chunks beyond `r_evict` drop out; the band
165 // between is hysteresis so a camera hovering on a boundary
166 // doesn't thrash.
167 let stream_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 4096.0, 0.0)));
168 let store = Arc::new(MemoryStore::default());
169 let grid = scene.grid_mut(stream_id).expect("just added");
170 grid.set_generator(Some(Arc::new(FloorGenerator)));
171 grid.set_chunk_store(Some(store));
172 grid.stream_radius = StreamRadius::new(256.0, 384.0);
173
174 let camera_near = DVec3::new(0.0, 4096.0, 100.0);
175 scene.pump_streaming_sync(camera_near); // real games: `pump_streaming`
176 let grid = scene.grid(stream_id).expect("still there");
177 assert!(grid.chunk_count() > 0); // floor streamed in around the camera
178 assert!(grid.voxel_solid(IVec3::new(5, 5, 240)));
179 // ANCHOR_END: streaming
180
181 // ANCHOR: streaming_edit
182 // Edit the streamed floor, walk far away, come back. The active
183 // set follows the camera: the old region evicts (edited chunks
184 // are handed to the ChunkStore first) while a fresh set streams
185 // in around the new position. On return the store wins over the
186 // generator, so the edit survives; without a store, eviction
187 // would silently revert the chunk to generator output.
188 scene
189 .grid_mut(stream_id)
190 .expect("still there")
191 .set_voxel(IVec3::new(5, 5, 240), None); // dig a hole
192 scene.pump_streaming_sync(camera_near + DVec3::new(0.0, 100_000.0, 0.0));
193 let grid = scene.grid(stream_id).expect("still there");
194 assert!(grid.chunk(IVec3::ZERO).is_none()); // home chunk evicted…
195 assert!(grid.chunk_count() > 0); // …but the far region streamed in
196 scene.pump_streaming_sync(camera_near);
197 let grid = scene.grid(stream_id).expect("re-streamed");
198 // The hole survived evict + re-stream.
199 assert!(!grid.voxel_solid(IVec3::new(5, 5, 240)));
200 // ANCHOR_END: streaming_edit
201
202 println!("book_scene_graph: all scene-graph assertions hold");
203}Sourcepub fn chunk(&self, chunk_idx: IVec3) -> Option<&Vxl>
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?
83fn main() {
84 // ANCHOR: scene_grids
85 let mut scene = Scene::new();
86 // A static "world" grid at the origin…
87 let ground_id = scene.add_grid(GridTransform::at(DVec3::ZERO));
88 // …and a second grid placed and rotated independently — its own
89 // f64 world position + quaternion, same chunked voxel payload.
90 let ship_id = scene.add_grid(GridTransform {
91 origin: DVec3::new(300.0, -80.0, -40.0),
92 rotation: DQuat::from_rotation_z(0.5),
93 });
94 // Object grids should not paint their own (grid-local, rotating)
95 // sky — leave the sky to the world grid.
96 scene.grid_mut(ship_id).expect("just added").render_sky = false;
97 // ANCHOR_END: scene_grids
98
99 // ANCHOR: edits
100 let ground = scene.grid_mut(ground_id).expect("just added");
101 // Solid slab: grid-local voxel coords, inclusive on both ends,
102 // decomposed across as many chunks as it spans (here 4×4).
103 ground.set_rect(
104 IVec3::new(-200, -200, 200),
105 IVec3::new(199, 199, 254),
106 Some(GRASS),
107 );
108 // `Some(colour)` inserts, `None` carves back to air.
109 ground.set_sphere(IVec3::new(0, 0, 195), 12, Some(STONE));
110 ground.set_rect(IVec3::new(-3, -200, 190), IVec3::new(3, 199, 196), None);
111 // ANCHOR_END: edits
112
113 // ANCHOR: queries
114 // Solid tests: inside the slab, then inside the carved tunnel.
115 assert!(ground.voxel_solid(IVec3::new(0, 0, 210)));
116 assert!(!ground.voxel_solid(IVec3::new(0, 100, 193)));
117 // Colour queries answer on surface voxels; deep interior cells
118 // are untextured in the slab format and read as `None`.
119 assert_eq!(ground.voxel_color(IVec3::new(50, 50, 200)), Some(GRASS));
120 // ANCHOR_END: queries
121
122 // ANCHOR: recolour
123 // GOTCHA: inserting over already-solid voxels does NOT repaint
124 // them — span insertion only fills air. Recolour = carve, then
125 // insert.
126 let (lo, hi) = (IVec3::new(20, 20, 200), IVec3::new(24, 24, 204));
127 ground.set_rect(lo, hi, Some(RED));
128 assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(GRASS)); // unchanged!
129 ground.set_rect(lo, hi, None); // carve…
130 ground.set_rect(lo, hi, Some(RED)); // …then insert
131 assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
132 // ANCHOR_END: recolour
133
134 // ANCHOR: colfunc
135 // A carve exposes fresh interior walls; the plain edits paint
136 // them colour 0 (black). The `_with_colfunc` variants ask a
137 // closure instead — it sees grid-local coordinates, so gradients
138 // stay continuous across chunk seams. Here: a crater whose walls
139 // darken with depth.
140 ground.set_sphere_with_colfunc(IVec3::new(60, 60, 200), 8, SpanOp::Carve, |_x, _y, z| {
141 let depth = (z - 192).clamp(0, 15) as u8;
142 VoxColor::rgb(0x6b, 0x40 + depth * 3, 0x2f)
143 });
144 // ANCHOR_END: colfunc
145
146 // ANCHOR: snapshot
147 // Name grids before saving: ids survive the round-trip, but the
148 // generator / store hooks are host code and cannot be serialised
149 // — the name is your key for rebinding them after a load.
150 scene.grid_mut(ground_id).expect("ground exists").name = Some("ground".into());
151 let bytes = scene.save_snapshot(); // versioned envelope
152 let restored = Scene::load_snapshot(&bytes).expect("self-authored snapshot");
153 assert_eq!(restored.grid_count(), 2);
154 let (_, ground2) = restored
155 .grids()
156 .find(|(_, g)| g.name.as_deref() == Some("ground"))
157 .expect("rebind by name");
158 assert_eq!(ground2.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
159 // ANCHOR_END: snapshot
160
161 // ANCHOR: streaming
162 // Streaming: attach a generator + radii to a grid, then pump once
163 // per frame with the camera's world position. Chunks within
164 // `r_active` stream in; chunks beyond `r_evict` drop out; the band
165 // between is hysteresis so a camera hovering on a boundary
166 // doesn't thrash.
167 let stream_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 4096.0, 0.0)));
168 let store = Arc::new(MemoryStore::default());
169 let grid = scene.grid_mut(stream_id).expect("just added");
170 grid.set_generator(Some(Arc::new(FloorGenerator)));
171 grid.set_chunk_store(Some(store));
172 grid.stream_radius = StreamRadius::new(256.0, 384.0);
173
174 let camera_near = DVec3::new(0.0, 4096.0, 100.0);
175 scene.pump_streaming_sync(camera_near); // real games: `pump_streaming`
176 let grid = scene.grid(stream_id).expect("still there");
177 assert!(grid.chunk_count() > 0); // floor streamed in around the camera
178 assert!(grid.voxel_solid(IVec3::new(5, 5, 240)));
179 // ANCHOR_END: streaming
180
181 // ANCHOR: streaming_edit
182 // Edit the streamed floor, walk far away, come back. The active
183 // set follows the camera: the old region evicts (edited chunks
184 // are handed to the ChunkStore first) while a fresh set streams
185 // in around the new position. On return the store wins over the
186 // generator, so the edit survives; without a store, eviction
187 // would silently revert the chunk to generator output.
188 scene
189 .grid_mut(stream_id)
190 .expect("still there")
191 .set_voxel(IVec3::new(5, 5, 240), None); // dig a hole
192 scene.pump_streaming_sync(camera_near + DVec3::new(0.0, 100_000.0, 0.0));
193 let grid = scene.grid(stream_id).expect("still there");
194 assert!(grid.chunk(IVec3::ZERO).is_none()); // home chunk evicted…
195 assert!(grid.chunk_count() > 0); // …but the far region streamed in
196 scene.pump_streaming_sync(camera_near);
197 let grid = scene.grid(stream_id).expect("re-streamed");
198 // The hole survived evict + re-stream.
199 assert!(!grid.voxel_solid(IVec3::new(5, 5, 240)));
200 // ANCHOR_END: streaming_edit
201
202 println!("book_scene_graph: all scene-graph assertions hold");
203}Sourcepub fn bake(&mut self, mode: BakeMode)
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.
Sourcepub fn bake_lightmode(&mut self, lightmode: u32)
👎Deprecated since 0.23.0: use bake(BakeMode::..)
pub fn bake_lightmode(&mut self, lightmode: u32)
use bake(BakeMode::..)
QE-B6 — magic u32 lightmode; use Self::bake with a typed
BakeMode instead (1 → BakeMode::Directional, 3 →
BakeMode::AmbientOcclusion(AoParams::default())).
Sourcepub fn bake_lightmode_with_ao(&mut self, lightmode: u32, ao: AoParams)
👎Deprecated since 0.23.0: use bake(BakeMode::AmbientOcclusion(ao))
pub fn bake_lightmode_with_ao(&mut self, lightmode: u32, ao: AoParams)
use bake(BakeMode::AmbientOcclusion(ao))
QE-B6 — use Self::bake with
BakeMode::AmbientOcclusion(ao) instead.
Sourcepub fn bake_bbox(&mut self, lo: IVec3, hi: IVec3, mode: BakeMode)
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.
Sourcepub fn bake_lightmode_bbox(&mut self, lo: IVec3, hi: IVec3, lightmode: u32)
👎Deprecated since 0.23.0: use bake_bbox(lo, hi, BakeMode::..)
pub fn bake_lightmode_bbox(&mut self, lo: IVec3, hi: IVec3, lightmode: u32)
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).
Sourcepub fn chunk_mut(&mut self, chunk_idx: IVec3) -> Option<&mut Vxl>
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.
Sourcepub fn ensure_chunk(&mut self, chunk_idx: IVec3) -> &mut Vxl
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.
Sourcepub fn chunk_count(&self) -> usize
pub fn chunk_count(&self) -> usize
Number of materialised chunks. Implicit-air chunks don’t count.
Examples found in repository?
83fn main() {
84 // ANCHOR: scene_grids
85 let mut scene = Scene::new();
86 // A static "world" grid at the origin…
87 let ground_id = scene.add_grid(GridTransform::at(DVec3::ZERO));
88 // …and a second grid placed and rotated independently — its own
89 // f64 world position + quaternion, same chunked voxel payload.
90 let ship_id = scene.add_grid(GridTransform {
91 origin: DVec3::new(300.0, -80.0, -40.0),
92 rotation: DQuat::from_rotation_z(0.5),
93 });
94 // Object grids should not paint their own (grid-local, rotating)
95 // sky — leave the sky to the world grid.
96 scene.grid_mut(ship_id).expect("just added").render_sky = false;
97 // ANCHOR_END: scene_grids
98
99 // ANCHOR: edits
100 let ground = scene.grid_mut(ground_id).expect("just added");
101 // Solid slab: grid-local voxel coords, inclusive on both ends,
102 // decomposed across as many chunks as it spans (here 4×4).
103 ground.set_rect(
104 IVec3::new(-200, -200, 200),
105 IVec3::new(199, 199, 254),
106 Some(GRASS),
107 );
108 // `Some(colour)` inserts, `None` carves back to air.
109 ground.set_sphere(IVec3::new(0, 0, 195), 12, Some(STONE));
110 ground.set_rect(IVec3::new(-3, -200, 190), IVec3::new(3, 199, 196), None);
111 // ANCHOR_END: edits
112
113 // ANCHOR: queries
114 // Solid tests: inside the slab, then inside the carved tunnel.
115 assert!(ground.voxel_solid(IVec3::new(0, 0, 210)));
116 assert!(!ground.voxel_solid(IVec3::new(0, 100, 193)));
117 // Colour queries answer on surface voxels; deep interior cells
118 // are untextured in the slab format and read as `None`.
119 assert_eq!(ground.voxel_color(IVec3::new(50, 50, 200)), Some(GRASS));
120 // ANCHOR_END: queries
121
122 // ANCHOR: recolour
123 // GOTCHA: inserting over already-solid voxels does NOT repaint
124 // them — span insertion only fills air. Recolour = carve, then
125 // insert.
126 let (lo, hi) = (IVec3::new(20, 20, 200), IVec3::new(24, 24, 204));
127 ground.set_rect(lo, hi, Some(RED));
128 assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(GRASS)); // unchanged!
129 ground.set_rect(lo, hi, None); // carve…
130 ground.set_rect(lo, hi, Some(RED)); // …then insert
131 assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
132 // ANCHOR_END: recolour
133
134 // ANCHOR: colfunc
135 // A carve exposes fresh interior walls; the plain edits paint
136 // them colour 0 (black). The `_with_colfunc` variants ask a
137 // closure instead — it sees grid-local coordinates, so gradients
138 // stay continuous across chunk seams. Here: a crater whose walls
139 // darken with depth.
140 ground.set_sphere_with_colfunc(IVec3::new(60, 60, 200), 8, SpanOp::Carve, |_x, _y, z| {
141 let depth = (z - 192).clamp(0, 15) as u8;
142 VoxColor::rgb(0x6b, 0x40 + depth * 3, 0x2f)
143 });
144 // ANCHOR_END: colfunc
145
146 // ANCHOR: snapshot
147 // Name grids before saving: ids survive the round-trip, but the
148 // generator / store hooks are host code and cannot be serialised
149 // — the name is your key for rebinding them after a load.
150 scene.grid_mut(ground_id).expect("ground exists").name = Some("ground".into());
151 let bytes = scene.save_snapshot(); // versioned envelope
152 let restored = Scene::load_snapshot(&bytes).expect("self-authored snapshot");
153 assert_eq!(restored.grid_count(), 2);
154 let (_, ground2) = restored
155 .grids()
156 .find(|(_, g)| g.name.as_deref() == Some("ground"))
157 .expect("rebind by name");
158 assert_eq!(ground2.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
159 // ANCHOR_END: snapshot
160
161 // ANCHOR: streaming
162 // Streaming: attach a generator + radii to a grid, then pump once
163 // per frame with the camera's world position. Chunks within
164 // `r_active` stream in; chunks beyond `r_evict` drop out; the band
165 // between is hysteresis so a camera hovering on a boundary
166 // doesn't thrash.
167 let stream_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 4096.0, 0.0)));
168 let store = Arc::new(MemoryStore::default());
169 let grid = scene.grid_mut(stream_id).expect("just added");
170 grid.set_generator(Some(Arc::new(FloorGenerator)));
171 grid.set_chunk_store(Some(store));
172 grid.stream_radius = StreamRadius::new(256.0, 384.0);
173
174 let camera_near = DVec3::new(0.0, 4096.0, 100.0);
175 scene.pump_streaming_sync(camera_near); // real games: `pump_streaming`
176 let grid = scene.grid(stream_id).expect("still there");
177 assert!(grid.chunk_count() > 0); // floor streamed in around the camera
178 assert!(grid.voxel_solid(IVec3::new(5, 5, 240)));
179 // ANCHOR_END: streaming
180
181 // ANCHOR: streaming_edit
182 // Edit the streamed floor, walk far away, come back. The active
183 // set follows the camera: the old region evicts (edited chunks
184 // are handed to the ChunkStore first) while a fresh set streams
185 // in around the new position. On return the store wins over the
186 // generator, so the edit survives; without a store, eviction
187 // would silently revert the chunk to generator output.
188 scene
189 .grid_mut(stream_id)
190 .expect("still there")
191 .set_voxel(IVec3::new(5, 5, 240), None); // dig a hole
192 scene.pump_streaming_sync(camera_near + DVec3::new(0.0, 100_000.0, 0.0));
193 let grid = scene.grid(stream_id).expect("still there");
194 assert!(grid.chunk(IVec3::ZERO).is_none()); // home chunk evicted…
195 assert!(grid.chunk_count() > 0); // …but the far region streamed in
196 scene.pump_streaming_sync(camera_near);
197 let grid = scene.grid(stream_id).expect("re-streamed");
198 // The hole survived evict + re-stream.
199 assert!(!grid.voxel_solid(IVec3::new(5, 5, 240)));
200 // ANCHOR_END: streaming_edit
201
202 println!("book_scene_graph: all scene-graph assertions hold");
203}Sourcepub fn chunk_xy_backing(&self) -> Option<ChunkXyBacking<'_>>
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.
Sourcepub fn chunk_xyz_backing(&self) -> Option<ChunkXyBacking<'_>>
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
impl Grid
Sourcepub fn set_voxel(&mut self, voxel: IVec3, color: Option<VoxColor>)
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?
83fn main() {
84 // ANCHOR: scene_grids
85 let mut scene = Scene::new();
86 // A static "world" grid at the origin…
87 let ground_id = scene.add_grid(GridTransform::at(DVec3::ZERO));
88 // …and a second grid placed and rotated independently — its own
89 // f64 world position + quaternion, same chunked voxel payload.
90 let ship_id = scene.add_grid(GridTransform {
91 origin: DVec3::new(300.0, -80.0, -40.0),
92 rotation: DQuat::from_rotation_z(0.5),
93 });
94 // Object grids should not paint their own (grid-local, rotating)
95 // sky — leave the sky to the world grid.
96 scene.grid_mut(ship_id).expect("just added").render_sky = false;
97 // ANCHOR_END: scene_grids
98
99 // ANCHOR: edits
100 let ground = scene.grid_mut(ground_id).expect("just added");
101 // Solid slab: grid-local voxel coords, inclusive on both ends,
102 // decomposed across as many chunks as it spans (here 4×4).
103 ground.set_rect(
104 IVec3::new(-200, -200, 200),
105 IVec3::new(199, 199, 254),
106 Some(GRASS),
107 );
108 // `Some(colour)` inserts, `None` carves back to air.
109 ground.set_sphere(IVec3::new(0, 0, 195), 12, Some(STONE));
110 ground.set_rect(IVec3::new(-3, -200, 190), IVec3::new(3, 199, 196), None);
111 // ANCHOR_END: edits
112
113 // ANCHOR: queries
114 // Solid tests: inside the slab, then inside the carved tunnel.
115 assert!(ground.voxel_solid(IVec3::new(0, 0, 210)));
116 assert!(!ground.voxel_solid(IVec3::new(0, 100, 193)));
117 // Colour queries answer on surface voxels; deep interior cells
118 // are untextured in the slab format and read as `None`.
119 assert_eq!(ground.voxel_color(IVec3::new(50, 50, 200)), Some(GRASS));
120 // ANCHOR_END: queries
121
122 // ANCHOR: recolour
123 // GOTCHA: inserting over already-solid voxels does NOT repaint
124 // them — span insertion only fills air. Recolour = carve, then
125 // insert.
126 let (lo, hi) = (IVec3::new(20, 20, 200), IVec3::new(24, 24, 204));
127 ground.set_rect(lo, hi, Some(RED));
128 assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(GRASS)); // unchanged!
129 ground.set_rect(lo, hi, None); // carve…
130 ground.set_rect(lo, hi, Some(RED)); // …then insert
131 assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
132 // ANCHOR_END: recolour
133
134 // ANCHOR: colfunc
135 // A carve exposes fresh interior walls; the plain edits paint
136 // them colour 0 (black). The `_with_colfunc` variants ask a
137 // closure instead — it sees grid-local coordinates, so gradients
138 // stay continuous across chunk seams. Here: a crater whose walls
139 // darken with depth.
140 ground.set_sphere_with_colfunc(IVec3::new(60, 60, 200), 8, SpanOp::Carve, |_x, _y, z| {
141 let depth = (z - 192).clamp(0, 15) as u8;
142 VoxColor::rgb(0x6b, 0x40 + depth * 3, 0x2f)
143 });
144 // ANCHOR_END: colfunc
145
146 // ANCHOR: snapshot
147 // Name grids before saving: ids survive the round-trip, but the
148 // generator / store hooks are host code and cannot be serialised
149 // — the name is your key for rebinding them after a load.
150 scene.grid_mut(ground_id).expect("ground exists").name = Some("ground".into());
151 let bytes = scene.save_snapshot(); // versioned envelope
152 let restored = Scene::load_snapshot(&bytes).expect("self-authored snapshot");
153 assert_eq!(restored.grid_count(), 2);
154 let (_, ground2) = restored
155 .grids()
156 .find(|(_, g)| g.name.as_deref() == Some("ground"))
157 .expect("rebind by name");
158 assert_eq!(ground2.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
159 // ANCHOR_END: snapshot
160
161 // ANCHOR: streaming
162 // Streaming: attach a generator + radii to a grid, then pump once
163 // per frame with the camera's world position. Chunks within
164 // `r_active` stream in; chunks beyond `r_evict` drop out; the band
165 // between is hysteresis so a camera hovering on a boundary
166 // doesn't thrash.
167 let stream_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 4096.0, 0.0)));
168 let store = Arc::new(MemoryStore::default());
169 let grid = scene.grid_mut(stream_id).expect("just added");
170 grid.set_generator(Some(Arc::new(FloorGenerator)));
171 grid.set_chunk_store(Some(store));
172 grid.stream_radius = StreamRadius::new(256.0, 384.0);
173
174 let camera_near = DVec3::new(0.0, 4096.0, 100.0);
175 scene.pump_streaming_sync(camera_near); // real games: `pump_streaming`
176 let grid = scene.grid(stream_id).expect("still there");
177 assert!(grid.chunk_count() > 0); // floor streamed in around the camera
178 assert!(grid.voxel_solid(IVec3::new(5, 5, 240)));
179 // ANCHOR_END: streaming
180
181 // ANCHOR: streaming_edit
182 // Edit the streamed floor, walk far away, come back. The active
183 // set follows the camera: the old region evicts (edited chunks
184 // are handed to the ChunkStore first) while a fresh set streams
185 // in around the new position. On return the store wins over the
186 // generator, so the edit survives; without a store, eviction
187 // would silently revert the chunk to generator output.
188 scene
189 .grid_mut(stream_id)
190 .expect("still there")
191 .set_voxel(IVec3::new(5, 5, 240), None); // dig a hole
192 scene.pump_streaming_sync(camera_near + DVec3::new(0.0, 100_000.0, 0.0));
193 let grid = scene.grid(stream_id).expect("still there");
194 assert!(grid.chunk(IVec3::ZERO).is_none()); // home chunk evicted…
195 assert!(grid.chunk_count() > 0); // …but the far region streamed in
196 scene.pump_streaming_sync(camera_near);
197 let grid = scene.grid(stream_id).expect("re-streamed");
198 // The hole survived evict + re-stream.
199 assert!(!grid.voxel_solid(IVec3::new(5, 5, 240)));
200 // ANCHOR_END: streaming_edit
201
202 println!("book_scene_graph: all scene-graph assertions hold");
203}Sourcepub fn set_rect(&mut self, lo: IVec3, hi: IVec3, color: Option<VoxColor>)
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?
29fn build_world() -> Scene {
30 let mut scene = Scene::new();
31 let id = scene.add_grid(GridTransform::identity());
32 let g = scene.grid_mut(id).expect("grid");
33 // Ground slab: surface plane at z = 100 (+z is down, so the
34 // slab fills downward).
35 g.set_rect(
36 IVec3::new(20, 20, 100),
37 IVec3::new(140, 140, 120),
38 Some(VoxColor::rgb(0x4d, 0x8a, 0x3a)),
39 );
40 // A 1-voxel ledge to step onto, and a wall to slide along.
41 g.set_rect(
42 IVec3::new(90, 20, 99),
43 IVec3::new(140, 140, 99),
44 Some(VoxColor::rgb(0x77, 0x66, 0x55)),
45 );
46 g.set_rect(
47 IVec3::new(60, 70, 60),
48 IVec3::new(62, 140, 99),
49 Some(VoxColor::rgb(0x88, 0x44, 0x44)),
50 );
51 // A water curtain the veto lets the body wade through. Two
52 // format facts (both pinned as engine tests): pass-through
53 // geometry works up to 2 voxels thick — thicker walls grow a
54 // colourless UnexposedSolid core no veto can classify — and
55 // must sit ≥ 1 voxel inside the chunk (edge voxels lose their
56 // side colours).
57 g.set_rect(IVec3::new(40, 40, 80), IVec3::new(41, 60, 99), Some(WATER));
58 scene
59}More examples
83fn main() {
84 // ANCHOR: scene_grids
85 let mut scene = Scene::new();
86 // A static "world" grid at the origin…
87 let ground_id = scene.add_grid(GridTransform::at(DVec3::ZERO));
88 // …and a second grid placed and rotated independently — its own
89 // f64 world position + quaternion, same chunked voxel payload.
90 let ship_id = scene.add_grid(GridTransform {
91 origin: DVec3::new(300.0, -80.0, -40.0),
92 rotation: DQuat::from_rotation_z(0.5),
93 });
94 // Object grids should not paint their own (grid-local, rotating)
95 // sky — leave the sky to the world grid.
96 scene.grid_mut(ship_id).expect("just added").render_sky = false;
97 // ANCHOR_END: scene_grids
98
99 // ANCHOR: edits
100 let ground = scene.grid_mut(ground_id).expect("just added");
101 // Solid slab: grid-local voxel coords, inclusive on both ends,
102 // decomposed across as many chunks as it spans (here 4×4).
103 ground.set_rect(
104 IVec3::new(-200, -200, 200),
105 IVec3::new(199, 199, 254),
106 Some(GRASS),
107 );
108 // `Some(colour)` inserts, `None` carves back to air.
109 ground.set_sphere(IVec3::new(0, 0, 195), 12, Some(STONE));
110 ground.set_rect(IVec3::new(-3, -200, 190), IVec3::new(3, 199, 196), None);
111 // ANCHOR_END: edits
112
113 // ANCHOR: queries
114 // Solid tests: inside the slab, then inside the carved tunnel.
115 assert!(ground.voxel_solid(IVec3::new(0, 0, 210)));
116 assert!(!ground.voxel_solid(IVec3::new(0, 100, 193)));
117 // Colour queries answer on surface voxels; deep interior cells
118 // are untextured in the slab format and read as `None`.
119 assert_eq!(ground.voxel_color(IVec3::new(50, 50, 200)), Some(GRASS));
120 // ANCHOR_END: queries
121
122 // ANCHOR: recolour
123 // GOTCHA: inserting over already-solid voxels does NOT repaint
124 // them — span insertion only fills air. Recolour = carve, then
125 // insert.
126 let (lo, hi) = (IVec3::new(20, 20, 200), IVec3::new(24, 24, 204));
127 ground.set_rect(lo, hi, Some(RED));
128 assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(GRASS)); // unchanged!
129 ground.set_rect(lo, hi, None); // carve…
130 ground.set_rect(lo, hi, Some(RED)); // …then insert
131 assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
132 // ANCHOR_END: recolour
133
134 // ANCHOR: colfunc
135 // A carve exposes fresh interior walls; the plain edits paint
136 // them colour 0 (black). The `_with_colfunc` variants ask a
137 // closure instead — it sees grid-local coordinates, so gradients
138 // stay continuous across chunk seams. Here: a crater whose walls
139 // darken with depth.
140 ground.set_sphere_with_colfunc(IVec3::new(60, 60, 200), 8, SpanOp::Carve, |_x, _y, z| {
141 let depth = (z - 192).clamp(0, 15) as u8;
142 VoxColor::rgb(0x6b, 0x40 + depth * 3, 0x2f)
143 });
144 // ANCHOR_END: colfunc
145
146 // ANCHOR: snapshot
147 // Name grids before saving: ids survive the round-trip, but the
148 // generator / store hooks are host code and cannot be serialised
149 // — the name is your key for rebinding them after a load.
150 scene.grid_mut(ground_id).expect("ground exists").name = Some("ground".into());
151 let bytes = scene.save_snapshot(); // versioned envelope
152 let restored = Scene::load_snapshot(&bytes).expect("self-authored snapshot");
153 assert_eq!(restored.grid_count(), 2);
154 let (_, ground2) = restored
155 .grids()
156 .find(|(_, g)| g.name.as_deref() == Some("ground"))
157 .expect("rebind by name");
158 assert_eq!(ground2.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
159 // ANCHOR_END: snapshot
160
161 // ANCHOR: streaming
162 // Streaming: attach a generator + radii to a grid, then pump once
163 // per frame with the camera's world position. Chunks within
164 // `r_active` stream in; chunks beyond `r_evict` drop out; the band
165 // between is hysteresis so a camera hovering on a boundary
166 // doesn't thrash.
167 let stream_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 4096.0, 0.0)));
168 let store = Arc::new(MemoryStore::default());
169 let grid = scene.grid_mut(stream_id).expect("just added");
170 grid.set_generator(Some(Arc::new(FloorGenerator)));
171 grid.set_chunk_store(Some(store));
172 grid.stream_radius = StreamRadius::new(256.0, 384.0);
173
174 let camera_near = DVec3::new(0.0, 4096.0, 100.0);
175 scene.pump_streaming_sync(camera_near); // real games: `pump_streaming`
176 let grid = scene.grid(stream_id).expect("still there");
177 assert!(grid.chunk_count() > 0); // floor streamed in around the camera
178 assert!(grid.voxel_solid(IVec3::new(5, 5, 240)));
179 // ANCHOR_END: streaming
180
181 // ANCHOR: streaming_edit
182 // Edit the streamed floor, walk far away, come back. The active
183 // set follows the camera: the old region evicts (edited chunks
184 // are handed to the ChunkStore first) while a fresh set streams
185 // in around the new position. On return the store wins over the
186 // generator, so the edit survives; without a store, eviction
187 // would silently revert the chunk to generator output.
188 scene
189 .grid_mut(stream_id)
190 .expect("still there")
191 .set_voxel(IVec3::new(5, 5, 240), None); // dig a hole
192 scene.pump_streaming_sync(camera_near + DVec3::new(0.0, 100_000.0, 0.0));
193 let grid = scene.grid(stream_id).expect("still there");
194 assert!(grid.chunk(IVec3::ZERO).is_none()); // home chunk evicted…
195 assert!(grid.chunk_count() > 0); // …but the far region streamed in
196 scene.pump_streaming_sync(camera_near);
197 let grid = scene.grid(stream_id).expect("re-streamed");
198 // The hole survived evict + re-stream.
199 assert!(!grid.voxel_solid(IVec3::new(5, 5, 240)));
200 // ANCHOR_END: streaming_edit
201
202 println!("book_scene_graph: all scene-graph assertions hold");
203}Sourcepub fn set_sphere(
&mut self,
centre: IVec3,
radius: u32,
color: Option<VoxColor>,
)
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?
83fn main() {
84 // ANCHOR: scene_grids
85 let mut scene = Scene::new();
86 // A static "world" grid at the origin…
87 let ground_id = scene.add_grid(GridTransform::at(DVec3::ZERO));
88 // …and a second grid placed and rotated independently — its own
89 // f64 world position + quaternion, same chunked voxel payload.
90 let ship_id = scene.add_grid(GridTransform {
91 origin: DVec3::new(300.0, -80.0, -40.0),
92 rotation: DQuat::from_rotation_z(0.5),
93 });
94 // Object grids should not paint their own (grid-local, rotating)
95 // sky — leave the sky to the world grid.
96 scene.grid_mut(ship_id).expect("just added").render_sky = false;
97 // ANCHOR_END: scene_grids
98
99 // ANCHOR: edits
100 let ground = scene.grid_mut(ground_id).expect("just added");
101 // Solid slab: grid-local voxel coords, inclusive on both ends,
102 // decomposed across as many chunks as it spans (here 4×4).
103 ground.set_rect(
104 IVec3::new(-200, -200, 200),
105 IVec3::new(199, 199, 254),
106 Some(GRASS),
107 );
108 // `Some(colour)` inserts, `None` carves back to air.
109 ground.set_sphere(IVec3::new(0, 0, 195), 12, Some(STONE));
110 ground.set_rect(IVec3::new(-3, -200, 190), IVec3::new(3, 199, 196), None);
111 // ANCHOR_END: edits
112
113 // ANCHOR: queries
114 // Solid tests: inside the slab, then inside the carved tunnel.
115 assert!(ground.voxel_solid(IVec3::new(0, 0, 210)));
116 assert!(!ground.voxel_solid(IVec3::new(0, 100, 193)));
117 // Colour queries answer on surface voxels; deep interior cells
118 // are untextured in the slab format and read as `None`.
119 assert_eq!(ground.voxel_color(IVec3::new(50, 50, 200)), Some(GRASS));
120 // ANCHOR_END: queries
121
122 // ANCHOR: recolour
123 // GOTCHA: inserting over already-solid voxels does NOT repaint
124 // them — span insertion only fills air. Recolour = carve, then
125 // insert.
126 let (lo, hi) = (IVec3::new(20, 20, 200), IVec3::new(24, 24, 204));
127 ground.set_rect(lo, hi, Some(RED));
128 assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(GRASS)); // unchanged!
129 ground.set_rect(lo, hi, None); // carve…
130 ground.set_rect(lo, hi, Some(RED)); // …then insert
131 assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
132 // ANCHOR_END: recolour
133
134 // ANCHOR: colfunc
135 // A carve exposes fresh interior walls; the plain edits paint
136 // them colour 0 (black). The `_with_colfunc` variants ask a
137 // closure instead — it sees grid-local coordinates, so gradients
138 // stay continuous across chunk seams. Here: a crater whose walls
139 // darken with depth.
140 ground.set_sphere_with_colfunc(IVec3::new(60, 60, 200), 8, SpanOp::Carve, |_x, _y, z| {
141 let depth = (z - 192).clamp(0, 15) as u8;
142 VoxColor::rgb(0x6b, 0x40 + depth * 3, 0x2f)
143 });
144 // ANCHOR_END: colfunc
145
146 // ANCHOR: snapshot
147 // Name grids before saving: ids survive the round-trip, but the
148 // generator / store hooks are host code and cannot be serialised
149 // — the name is your key for rebinding them after a load.
150 scene.grid_mut(ground_id).expect("ground exists").name = Some("ground".into());
151 let bytes = scene.save_snapshot(); // versioned envelope
152 let restored = Scene::load_snapshot(&bytes).expect("self-authored snapshot");
153 assert_eq!(restored.grid_count(), 2);
154 let (_, ground2) = restored
155 .grids()
156 .find(|(_, g)| g.name.as_deref() == Some("ground"))
157 .expect("rebind by name");
158 assert_eq!(ground2.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
159 // ANCHOR_END: snapshot
160
161 // ANCHOR: streaming
162 // Streaming: attach a generator + radii to a grid, then pump once
163 // per frame with the camera's world position. Chunks within
164 // `r_active` stream in; chunks beyond `r_evict` drop out; the band
165 // between is hysteresis so a camera hovering on a boundary
166 // doesn't thrash.
167 let stream_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 4096.0, 0.0)));
168 let store = Arc::new(MemoryStore::default());
169 let grid = scene.grid_mut(stream_id).expect("just added");
170 grid.set_generator(Some(Arc::new(FloorGenerator)));
171 grid.set_chunk_store(Some(store));
172 grid.stream_radius = StreamRadius::new(256.0, 384.0);
173
174 let camera_near = DVec3::new(0.0, 4096.0, 100.0);
175 scene.pump_streaming_sync(camera_near); // real games: `pump_streaming`
176 let grid = scene.grid(stream_id).expect("still there");
177 assert!(grid.chunk_count() > 0); // floor streamed in around the camera
178 assert!(grid.voxel_solid(IVec3::new(5, 5, 240)));
179 // ANCHOR_END: streaming
180
181 // ANCHOR: streaming_edit
182 // Edit the streamed floor, walk far away, come back. The active
183 // set follows the camera: the old region evicts (edited chunks
184 // are handed to the ChunkStore first) while a fresh set streams
185 // in around the new position. On return the store wins over the
186 // generator, so the edit survives; without a store, eviction
187 // would silently revert the chunk to generator output.
188 scene
189 .grid_mut(stream_id)
190 .expect("still there")
191 .set_voxel(IVec3::new(5, 5, 240), None); // dig a hole
192 scene.pump_streaming_sync(camera_near + DVec3::new(0.0, 100_000.0, 0.0));
193 let grid = scene.grid(stream_id).expect("still there");
194 assert!(grid.chunk(IVec3::ZERO).is_none()); // home chunk evicted…
195 assert!(grid.chunk_count() > 0); // …but the far region streamed in
196 scene.pump_streaming_sync(camera_near);
197 let grid = scene.grid(stream_id).expect("re-streamed");
198 // The hole survived evict + re-stream.
199 assert!(!grid.voxel_solid(IVec3::new(5, 5, 240)));
200 // ANCHOR_END: streaming_edit
201
202 println!("book_scene_graph: all scene-graph assertions hold");
203}Sourcepub fn set_sphere_with_colfunc<F>(
&mut self,
centre: IVec3,
radius: u32,
op: SpanOp,
colfunc: F,
)
pub fn set_sphere_with_colfunc<F>( &mut self, centre: IVec3, radius: u32, op: SpanOp, colfunc: F, )
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?
83fn main() {
84 // ANCHOR: scene_grids
85 let mut scene = Scene::new();
86 // A static "world" grid at the origin…
87 let ground_id = scene.add_grid(GridTransform::at(DVec3::ZERO));
88 // …and a second grid placed and rotated independently — its own
89 // f64 world position + quaternion, same chunked voxel payload.
90 let ship_id = scene.add_grid(GridTransform {
91 origin: DVec3::new(300.0, -80.0, -40.0),
92 rotation: DQuat::from_rotation_z(0.5),
93 });
94 // Object grids should not paint their own (grid-local, rotating)
95 // sky — leave the sky to the world grid.
96 scene.grid_mut(ship_id).expect("just added").render_sky = false;
97 // ANCHOR_END: scene_grids
98
99 // ANCHOR: edits
100 let ground = scene.grid_mut(ground_id).expect("just added");
101 // Solid slab: grid-local voxel coords, inclusive on both ends,
102 // decomposed across as many chunks as it spans (here 4×4).
103 ground.set_rect(
104 IVec3::new(-200, -200, 200),
105 IVec3::new(199, 199, 254),
106 Some(GRASS),
107 );
108 // `Some(colour)` inserts, `None` carves back to air.
109 ground.set_sphere(IVec3::new(0, 0, 195), 12, Some(STONE));
110 ground.set_rect(IVec3::new(-3, -200, 190), IVec3::new(3, 199, 196), None);
111 // ANCHOR_END: edits
112
113 // ANCHOR: queries
114 // Solid tests: inside the slab, then inside the carved tunnel.
115 assert!(ground.voxel_solid(IVec3::new(0, 0, 210)));
116 assert!(!ground.voxel_solid(IVec3::new(0, 100, 193)));
117 // Colour queries answer on surface voxels; deep interior cells
118 // are untextured in the slab format and read as `None`.
119 assert_eq!(ground.voxel_color(IVec3::new(50, 50, 200)), Some(GRASS));
120 // ANCHOR_END: queries
121
122 // ANCHOR: recolour
123 // GOTCHA: inserting over already-solid voxels does NOT repaint
124 // them — span insertion only fills air. Recolour = carve, then
125 // insert.
126 let (lo, hi) = (IVec3::new(20, 20, 200), IVec3::new(24, 24, 204));
127 ground.set_rect(lo, hi, Some(RED));
128 assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(GRASS)); // unchanged!
129 ground.set_rect(lo, hi, None); // carve…
130 ground.set_rect(lo, hi, Some(RED)); // …then insert
131 assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
132 // ANCHOR_END: recolour
133
134 // ANCHOR: colfunc
135 // A carve exposes fresh interior walls; the plain edits paint
136 // them colour 0 (black). The `_with_colfunc` variants ask a
137 // closure instead — it sees grid-local coordinates, so gradients
138 // stay continuous across chunk seams. Here: a crater whose walls
139 // darken with depth.
140 ground.set_sphere_with_colfunc(IVec3::new(60, 60, 200), 8, SpanOp::Carve, |_x, _y, z| {
141 let depth = (z - 192).clamp(0, 15) as u8;
142 VoxColor::rgb(0x6b, 0x40 + depth * 3, 0x2f)
143 });
144 // ANCHOR_END: colfunc
145
146 // ANCHOR: snapshot
147 // Name grids before saving: ids survive the round-trip, but the
148 // generator / store hooks are host code and cannot be serialised
149 // — the name is your key for rebinding them after a load.
150 scene.grid_mut(ground_id).expect("ground exists").name = Some("ground".into());
151 let bytes = scene.save_snapshot(); // versioned envelope
152 let restored = Scene::load_snapshot(&bytes).expect("self-authored snapshot");
153 assert_eq!(restored.grid_count(), 2);
154 let (_, ground2) = restored
155 .grids()
156 .find(|(_, g)| g.name.as_deref() == Some("ground"))
157 .expect("rebind by name");
158 assert_eq!(ground2.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
159 // ANCHOR_END: snapshot
160
161 // ANCHOR: streaming
162 // Streaming: attach a generator + radii to a grid, then pump once
163 // per frame with the camera's world position. Chunks within
164 // `r_active` stream in; chunks beyond `r_evict` drop out; the band
165 // between is hysteresis so a camera hovering on a boundary
166 // doesn't thrash.
167 let stream_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 4096.0, 0.0)));
168 let store = Arc::new(MemoryStore::default());
169 let grid = scene.grid_mut(stream_id).expect("just added");
170 grid.set_generator(Some(Arc::new(FloorGenerator)));
171 grid.set_chunk_store(Some(store));
172 grid.stream_radius = StreamRadius::new(256.0, 384.0);
173
174 let camera_near = DVec3::new(0.0, 4096.0, 100.0);
175 scene.pump_streaming_sync(camera_near); // real games: `pump_streaming`
176 let grid = scene.grid(stream_id).expect("still there");
177 assert!(grid.chunk_count() > 0); // floor streamed in around the camera
178 assert!(grid.voxel_solid(IVec3::new(5, 5, 240)));
179 // ANCHOR_END: streaming
180
181 // ANCHOR: streaming_edit
182 // Edit the streamed floor, walk far away, come back. The active
183 // set follows the camera: the old region evicts (edited chunks
184 // are handed to the ChunkStore first) while a fresh set streams
185 // in around the new position. On return the store wins over the
186 // generator, so the edit survives; without a store, eviction
187 // would silently revert the chunk to generator output.
188 scene
189 .grid_mut(stream_id)
190 .expect("still there")
191 .set_voxel(IVec3::new(5, 5, 240), None); // dig a hole
192 scene.pump_streaming_sync(camera_near + DVec3::new(0.0, 100_000.0, 0.0));
193 let grid = scene.grid(stream_id).expect("still there");
194 assert!(grid.chunk(IVec3::ZERO).is_none()); // home chunk evicted…
195 assert!(grid.chunk_count() > 0); // …but the far region streamed in
196 scene.pump_streaming_sync(camera_near);
197 let grid = scene.grid(stream_id).expect("re-streamed");
198 // The hole survived evict + re-stream.
199 assert!(!grid.voxel_solid(IVec3::new(5, 5, 240)));
200 // ANCHOR_END: streaming_edit
201
202 println!("book_scene_graph: all scene-graph assertions hold");
203}Sourcepub fn set_rect_with_colfunc<F>(
&mut self,
lo: IVec3,
hi: IVec3,
op: SpanOp,
colfunc: F,
)
pub fn set_rect_with_colfunc<F>( &mut self, lo: IVec3, hi: IVec3, op: SpanOp, colfunc: F, )
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
impl Grid
Sourcepub fn new(transform: GridTransform) -> Self
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.
Sourcepub fn ensure_dda_bricks(&mut self, requested_mip: u32) -> u32
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.
Sourcepub fn chunk_version(&self, chunk_idx: IVec3) -> u64
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.
Sourcepub fn bump_chunk_version(&mut self, chunk_idx: IVec3)
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.
Sourcepub fn mutation_counter(&self) -> u64
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.
Sourcepub fn bump_chunk_version_bbox(
&mut self,
chunk_idx: IVec3,
lo: IVec3,
hi: IVec3,
)
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.
Sourcepub fn take_chunk_dirty(&mut self, chunk_idx: IVec3) -> Option<DirtyExtent>
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).
Sourcepub fn chunk_versions(&self) -> &HashMap<IVec3, u64>
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.
Sourcepub fn set_generator(&mut self, generator: Option<Arc<dyn ChunkGenerator>>)
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?
83fn main() {
84 // ANCHOR: scene_grids
85 let mut scene = Scene::new();
86 // A static "world" grid at the origin…
87 let ground_id = scene.add_grid(GridTransform::at(DVec3::ZERO));
88 // …and a second grid placed and rotated independently — its own
89 // f64 world position + quaternion, same chunked voxel payload.
90 let ship_id = scene.add_grid(GridTransform {
91 origin: DVec3::new(300.0, -80.0, -40.0),
92 rotation: DQuat::from_rotation_z(0.5),
93 });
94 // Object grids should not paint their own (grid-local, rotating)
95 // sky — leave the sky to the world grid.
96 scene.grid_mut(ship_id).expect("just added").render_sky = false;
97 // ANCHOR_END: scene_grids
98
99 // ANCHOR: edits
100 let ground = scene.grid_mut(ground_id).expect("just added");
101 // Solid slab: grid-local voxel coords, inclusive on both ends,
102 // decomposed across as many chunks as it spans (here 4×4).
103 ground.set_rect(
104 IVec3::new(-200, -200, 200),
105 IVec3::new(199, 199, 254),
106 Some(GRASS),
107 );
108 // `Some(colour)` inserts, `None` carves back to air.
109 ground.set_sphere(IVec3::new(0, 0, 195), 12, Some(STONE));
110 ground.set_rect(IVec3::new(-3, -200, 190), IVec3::new(3, 199, 196), None);
111 // ANCHOR_END: edits
112
113 // ANCHOR: queries
114 // Solid tests: inside the slab, then inside the carved tunnel.
115 assert!(ground.voxel_solid(IVec3::new(0, 0, 210)));
116 assert!(!ground.voxel_solid(IVec3::new(0, 100, 193)));
117 // Colour queries answer on surface voxels; deep interior cells
118 // are untextured in the slab format and read as `None`.
119 assert_eq!(ground.voxel_color(IVec3::new(50, 50, 200)), Some(GRASS));
120 // ANCHOR_END: queries
121
122 // ANCHOR: recolour
123 // GOTCHA: inserting over already-solid voxels does NOT repaint
124 // them — span insertion only fills air. Recolour = carve, then
125 // insert.
126 let (lo, hi) = (IVec3::new(20, 20, 200), IVec3::new(24, 24, 204));
127 ground.set_rect(lo, hi, Some(RED));
128 assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(GRASS)); // unchanged!
129 ground.set_rect(lo, hi, None); // carve…
130 ground.set_rect(lo, hi, Some(RED)); // …then insert
131 assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
132 // ANCHOR_END: recolour
133
134 // ANCHOR: colfunc
135 // A carve exposes fresh interior walls; the plain edits paint
136 // them colour 0 (black). The `_with_colfunc` variants ask a
137 // closure instead — it sees grid-local coordinates, so gradients
138 // stay continuous across chunk seams. Here: a crater whose walls
139 // darken with depth.
140 ground.set_sphere_with_colfunc(IVec3::new(60, 60, 200), 8, SpanOp::Carve, |_x, _y, z| {
141 let depth = (z - 192).clamp(0, 15) as u8;
142 VoxColor::rgb(0x6b, 0x40 + depth * 3, 0x2f)
143 });
144 // ANCHOR_END: colfunc
145
146 // ANCHOR: snapshot
147 // Name grids before saving: ids survive the round-trip, but the
148 // generator / store hooks are host code and cannot be serialised
149 // — the name is your key for rebinding them after a load.
150 scene.grid_mut(ground_id).expect("ground exists").name = Some("ground".into());
151 let bytes = scene.save_snapshot(); // versioned envelope
152 let restored = Scene::load_snapshot(&bytes).expect("self-authored snapshot");
153 assert_eq!(restored.grid_count(), 2);
154 let (_, ground2) = restored
155 .grids()
156 .find(|(_, g)| g.name.as_deref() == Some("ground"))
157 .expect("rebind by name");
158 assert_eq!(ground2.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
159 // ANCHOR_END: snapshot
160
161 // ANCHOR: streaming
162 // Streaming: attach a generator + radii to a grid, then pump once
163 // per frame with the camera's world position. Chunks within
164 // `r_active` stream in; chunks beyond `r_evict` drop out; the band
165 // between is hysteresis so a camera hovering on a boundary
166 // doesn't thrash.
167 let stream_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 4096.0, 0.0)));
168 let store = Arc::new(MemoryStore::default());
169 let grid = scene.grid_mut(stream_id).expect("just added");
170 grid.set_generator(Some(Arc::new(FloorGenerator)));
171 grid.set_chunk_store(Some(store));
172 grid.stream_radius = StreamRadius::new(256.0, 384.0);
173
174 let camera_near = DVec3::new(0.0, 4096.0, 100.0);
175 scene.pump_streaming_sync(camera_near); // real games: `pump_streaming`
176 let grid = scene.grid(stream_id).expect("still there");
177 assert!(grid.chunk_count() > 0); // floor streamed in around the camera
178 assert!(grid.voxel_solid(IVec3::new(5, 5, 240)));
179 // ANCHOR_END: streaming
180
181 // ANCHOR: streaming_edit
182 // Edit the streamed floor, walk far away, come back. The active
183 // set follows the camera: the old region evicts (edited chunks
184 // are handed to the ChunkStore first) while a fresh set streams
185 // in around the new position. On return the store wins over the
186 // generator, so the edit survives; without a store, eviction
187 // would silently revert the chunk to generator output.
188 scene
189 .grid_mut(stream_id)
190 .expect("still there")
191 .set_voxel(IVec3::new(5, 5, 240), None); // dig a hole
192 scene.pump_streaming_sync(camera_near + DVec3::new(0.0, 100_000.0, 0.0));
193 let grid = scene.grid(stream_id).expect("still there");
194 assert!(grid.chunk(IVec3::ZERO).is_none()); // home chunk evicted…
195 assert!(grid.chunk_count() > 0); // …but the far region streamed in
196 scene.pump_streaming_sync(camera_near);
197 let grid = scene.grid(stream_id).expect("re-streamed");
198 // The hole survived evict + re-stream.
199 assert!(!grid.voxel_solid(IVec3::new(5, 5, 240)));
200 // ANCHOR_END: streaming_edit
201
202 println!("book_scene_graph: all scene-graph assertions hold");
203}Sourcepub fn set_chunk_store(&mut self, store: Option<Arc<dyn ChunkStore>>)
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?
83fn main() {
84 // ANCHOR: scene_grids
85 let mut scene = Scene::new();
86 // A static "world" grid at the origin…
87 let ground_id = scene.add_grid(GridTransform::at(DVec3::ZERO));
88 // …and a second grid placed and rotated independently — its own
89 // f64 world position + quaternion, same chunked voxel payload.
90 let ship_id = scene.add_grid(GridTransform {
91 origin: DVec3::new(300.0, -80.0, -40.0),
92 rotation: DQuat::from_rotation_z(0.5),
93 });
94 // Object grids should not paint their own (grid-local, rotating)
95 // sky — leave the sky to the world grid.
96 scene.grid_mut(ship_id).expect("just added").render_sky = false;
97 // ANCHOR_END: scene_grids
98
99 // ANCHOR: edits
100 let ground = scene.grid_mut(ground_id).expect("just added");
101 // Solid slab: grid-local voxel coords, inclusive on both ends,
102 // decomposed across as many chunks as it spans (here 4×4).
103 ground.set_rect(
104 IVec3::new(-200, -200, 200),
105 IVec3::new(199, 199, 254),
106 Some(GRASS),
107 );
108 // `Some(colour)` inserts, `None` carves back to air.
109 ground.set_sphere(IVec3::new(0, 0, 195), 12, Some(STONE));
110 ground.set_rect(IVec3::new(-3, -200, 190), IVec3::new(3, 199, 196), None);
111 // ANCHOR_END: edits
112
113 // ANCHOR: queries
114 // Solid tests: inside the slab, then inside the carved tunnel.
115 assert!(ground.voxel_solid(IVec3::new(0, 0, 210)));
116 assert!(!ground.voxel_solid(IVec3::new(0, 100, 193)));
117 // Colour queries answer on surface voxels; deep interior cells
118 // are untextured in the slab format and read as `None`.
119 assert_eq!(ground.voxel_color(IVec3::new(50, 50, 200)), Some(GRASS));
120 // ANCHOR_END: queries
121
122 // ANCHOR: recolour
123 // GOTCHA: inserting over already-solid voxels does NOT repaint
124 // them — span insertion only fills air. Recolour = carve, then
125 // insert.
126 let (lo, hi) = (IVec3::new(20, 20, 200), IVec3::new(24, 24, 204));
127 ground.set_rect(lo, hi, Some(RED));
128 assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(GRASS)); // unchanged!
129 ground.set_rect(lo, hi, None); // carve…
130 ground.set_rect(lo, hi, Some(RED)); // …then insert
131 assert_eq!(ground.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
132 // ANCHOR_END: recolour
133
134 // ANCHOR: colfunc
135 // A carve exposes fresh interior walls; the plain edits paint
136 // them colour 0 (black). The `_with_colfunc` variants ask a
137 // closure instead — it sees grid-local coordinates, so gradients
138 // stay continuous across chunk seams. Here: a crater whose walls
139 // darken with depth.
140 ground.set_sphere_with_colfunc(IVec3::new(60, 60, 200), 8, SpanOp::Carve, |_x, _y, z| {
141 let depth = (z - 192).clamp(0, 15) as u8;
142 VoxColor::rgb(0x6b, 0x40 + depth * 3, 0x2f)
143 });
144 // ANCHOR_END: colfunc
145
146 // ANCHOR: snapshot
147 // Name grids before saving: ids survive the round-trip, but the
148 // generator / store hooks are host code and cannot be serialised
149 // — the name is your key for rebinding them after a load.
150 scene.grid_mut(ground_id).expect("ground exists").name = Some("ground".into());
151 let bytes = scene.save_snapshot(); // versioned envelope
152 let restored = Scene::load_snapshot(&bytes).expect("self-authored snapshot");
153 assert_eq!(restored.grid_count(), 2);
154 let (_, ground2) = restored
155 .grids()
156 .find(|(_, g)| g.name.as_deref() == Some("ground"))
157 .expect("rebind by name");
158 assert_eq!(ground2.voxel_color(IVec3::new(22, 22, 200)), Some(RED));
159 // ANCHOR_END: snapshot
160
161 // ANCHOR: streaming
162 // Streaming: attach a generator + radii to a grid, then pump once
163 // per frame with the camera's world position. Chunks within
164 // `r_active` stream in; chunks beyond `r_evict` drop out; the band
165 // between is hysteresis so a camera hovering on a boundary
166 // doesn't thrash.
167 let stream_id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 4096.0, 0.0)));
168 let store = Arc::new(MemoryStore::default());
169 let grid = scene.grid_mut(stream_id).expect("just added");
170 grid.set_generator(Some(Arc::new(FloorGenerator)));
171 grid.set_chunk_store(Some(store));
172 grid.stream_radius = StreamRadius::new(256.0, 384.0);
173
174 let camera_near = DVec3::new(0.0, 4096.0, 100.0);
175 scene.pump_streaming_sync(camera_near); // real games: `pump_streaming`
176 let grid = scene.grid(stream_id).expect("still there");
177 assert!(grid.chunk_count() > 0); // floor streamed in around the camera
178 assert!(grid.voxel_solid(IVec3::new(5, 5, 240)));
179 // ANCHOR_END: streaming
180
181 // ANCHOR: streaming_edit
182 // Edit the streamed floor, walk far away, come back. The active
183 // set follows the camera: the old region evicts (edited chunks
184 // are handed to the ChunkStore first) while a fresh set streams
185 // in around the new position. On return the store wins over the
186 // generator, so the edit survives; without a store, eviction
187 // would silently revert the chunk to generator output.
188 scene
189 .grid_mut(stream_id)
190 .expect("still there")
191 .set_voxel(IVec3::new(5, 5, 240), None); // dig a hole
192 scene.pump_streaming_sync(camera_near + DVec3::new(0.0, 100_000.0, 0.0));
193 let grid = scene.grid(stream_id).expect("still there");
194 assert!(grid.chunk(IVec3::ZERO).is_none()); // home chunk evicted…
195 assert!(grid.chunk_count() > 0); // …but the far region streamed in
196 scene.pump_streaming_sync(camera_near);
197 let grid = scene.grid(stream_id).expect("re-streamed");
198 // The hole survived evict + re-stream.
199 assert!(!grid.voxel_solid(IVec3::new(5, 5, 240)));
200 // ANCHOR_END: streaming_edit
201
202 println!("book_scene_graph: all scene-graph assertions hold");
203}Sourcepub fn ensure_chunk_generated(&mut self, chunk_idx: IVec3) -> bool
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_generatedcall 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.
Sourcepub fn bounding_radius(&self) -> f64
pub fn bounding_radius(&self) -> f64
Bounding-sphere radius of the populated chunk set in grid-local space.
Walks the sparse chunk map once, computes the chunk-index
AABB, converts to voxel-space half-extent, returns its
Euclidean length. Empty grid → 0.0.
Conservative — bounds the full chunk volume, not just its
populated voxels (a chunk containing one voxel still
contributes CHUNK_SIZE_XY × CHUNK_SIZE_XY × CHUNK_SIZE_Z
to the bbox). For LOD picking that’s fine: an over-bound
sphere errs on the side of Near.
Cost: O(chunks.len()); recomputed on every call. Callers
who need this every frame should memoize at the
Scene-level cache (added when S6.2 needs it).
Sourcepub fn select_lod(&self, camera_world_pos: DVec3) -> Lod
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§
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> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
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 moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
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