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: 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.
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: 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 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}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 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}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 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}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 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}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 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}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}
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
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}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 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}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 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}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 add_water_volume(&mut self, a: IVec3, b: IVec3)
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?
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}Sourcepub fn water_depth_local(&self, p: DVec3) -> Option<f64>
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
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 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}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 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}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
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).
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