pub struct Scene { /* private fields */ }Expand description
Top-level scene container. Holds a flat collection of grids
keyed by GridId.
S2.0 only exposes registration / removal / lookup. Address math helpers (S2.x), edit API (S2.x), and rendering composition (S3) land in later sub-substages.
Implementations§
Source§impl Scene
impl Scene
Sourcepub fn to_snapshot(&self) -> SceneSnapshot
pub fn to_snapshot(&self) -> SceneSnapshot
Capture the scene’s full state as a serde-friendly value.
Each chunk is encoded via
roxlap_formats::vxl::serialize; the rest is plain field
data.
Grid iteration order in the produced snapshot is sorted by
GridId so two snapshots of the same scene produce
byte-identical output (the live HashMap iteration order
would be non-deterministic).
Sourcepub fn from_snapshot(snap: &SceneSnapshot) -> Result<Self, FromSnapshotError>
pub fn from_snapshot(snap: &SceneSnapshot) -> Result<Self, FromSnapshotError>
Restore a Scene from a snapshot. Each chunk’s bytes are
re-parsed via roxlap_formats::vxl::parse and re-armed
for edits via roxlap_formats::vxl::Vxl::reserve_edit_capacity.
§Errors
Returns FromSnapshotError::ChunkParse tagged with the
owning grid + chunk index if any chunk’s bytes fail to
parse. The partial scene is dropped — restoration is
all-or-nothing.
Source§impl Scene
impl Scene
Sourcepub fn save_snapshot(&self) -> Vec<u8> ⓘ
pub fn save_snapshot(&self) -> Vec<u8> ⓘ
Serialise the scene to the versioned snapshot wire format
(QE.5b): SNAPSHOT_MAGIC + little-endian
SNAPSHOT_VERSION + a bincode SceneSnapshot payload.
This is the save-file API — a blob written today stays
loadable by future engine versions (Self::load_snapshot
dispatches on the version), which bare serde values can’t
promise under positional codecs.
Hosts that want a different payload codec (JSON for debugging,
postcard for size) can still serialise
Self::to_snapshot’s value themselves — and then own their
format’s evolution story.
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 load_snapshot(bytes: &[u8]) -> Result<Self, SnapshotLoadError>
pub fn load_snapshot(bytes: &[u8]) -> Result<Self, SnapshotLoadError>
Load a scene from Self::save_snapshot bytes, dispatching
on the embedded wire version.
Migration note: saves written before QE.5b (bare bincode
of a SceneSnapshot, no envelope) fail with
SnapshotLoadError::BadMagic; decode those with the engine
version that wrote them and re-save.
§Errors
SnapshotLoadError — bad magic, unknown version, corrupted
payload, or a failing chunk restore.
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}Source§impl Scene
impl Scene
Sourcepub fn new() -> Self
pub fn new() -> Self
New empty scene — no grids.
Examples found in repository?
29fn build_world() -> Scene {
30 let mut scene = Scene::new();
31 let id = scene.add_grid(GridTransform::identity());
32 let g = scene.grid_mut(id).expect("grid");
33 // Ground slab: surface plane at z = 100 (+z is down, so the
34 // slab fills downward).
35 g.set_rect(
36 IVec3::new(20, 20, 100),
37 IVec3::new(140, 140, 120),
38 Some(VoxColor::rgb(0x4d, 0x8a, 0x3a)),
39 );
40 // A 1-voxel ledge to step onto, and a wall to slide along.
41 g.set_rect(
42 IVec3::new(90, 20, 99),
43 IVec3::new(140, 140, 99),
44 Some(VoxColor::rgb(0x77, 0x66, 0x55)),
45 );
46 g.set_rect(
47 IVec3::new(60, 70, 60),
48 IVec3::new(62, 140, 99),
49 Some(VoxColor::rgb(0x88, 0x44, 0x44)),
50 );
51 // A water curtain the veto lets the body wade through. Two
52 // format facts (both pinned as engine tests): pass-through
53 // geometry works up to 2 voxels thick — thicker walls grow a
54 // colourless UnexposedSolid core no veto can classify — and
55 // must sit ≥ 1 voxel inside the chunk (edge voxels lose their
56 // side colours).
57 g.set_rect(IVec3::new(40, 40, 80), IVec3::new(41, 60, 99), Some(WATER));
58 scene
59}More examples
83fn main() {
84 // ANCHOR: scene_grids
85 let mut scene = Scene::new();
86 // A static "world" grid at the origin…
87 let ground_id = scene.add_grid(GridTransform::at(DVec3::ZERO));
88 // …and a second grid placed and rotated independently — its own
89 // f64 world position + quaternion, same chunked voxel payload.
90 let ship_id = scene.add_grid(GridTransform {
91 origin: DVec3::new(300.0, -80.0, -40.0),
92 rotation: DQuat::from_rotation_z(0.5),
93 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 grid_count(&self) -> usize
pub fn grid_count(&self) -> usize
Number of grids currently registered.
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 add_grid(&mut self, transform: GridTransform) -> GridId
pub fn add_grid(&mut self, transform: GridTransform) -> GridId
Register a new grid. Returns its fresh, unique GridId.
SC — debug-asserts transform.voxel_world_size is finite and > 0
(catches a direct GridTransform { .. } literal that bypasses
GridTransform::at_scale); a 0 breaks the scaled marcher.
Examples found in repository?
29fn build_world() -> Scene {
30 let mut scene = Scene::new();
31 let id = scene.add_grid(GridTransform::identity());
32 let g = scene.grid_mut(id).expect("grid");
33 // Ground slab: surface plane at z = 100 (+z is down, so the
34 // slab fills downward).
35 g.set_rect(
36 IVec3::new(20, 20, 100),
37 IVec3::new(140, 140, 120),
38 Some(VoxColor::rgb(0x4d, 0x8a, 0x3a)),
39 );
40 // A 1-voxel ledge to step onto, and a wall to slide along.
41 g.set_rect(
42 IVec3::new(90, 20, 99),
43 IVec3::new(140, 140, 99),
44 Some(VoxColor::rgb(0x77, 0x66, 0x55)),
45 );
46 g.set_rect(
47 IVec3::new(60, 70, 60),
48 IVec3::new(62, 140, 99),
49 Some(VoxColor::rgb(0x88, 0x44, 0x44)),
50 );
51 // A water curtain the veto lets the body wade through. Two
52 // format facts (both pinned as engine tests): pass-through
53 // geometry works up to 2 voxels thick — thicker walls grow a
54 // colourless UnexposedSolid core no veto can classify — and
55 // must sit ≥ 1 voxel inside the chunk (edge voxels lose their
56 // side colours).
57 g.set_rect(IVec3::new(40, 40, 80), IVec3::new(41, 60, 99), Some(WATER));
58 scene
59}More examples
83fn main() {
84 // ANCHOR: scene_grids
85 let mut scene = Scene::new();
86 // A static "world" grid at the origin…
87 let ground_id = scene.add_grid(GridTransform::at(DVec3::ZERO));
88 // …and a second grid placed and rotated independently — its own
89 // f64 world position + quaternion, same chunked voxel payload.
90 let ship_id = scene.add_grid(GridTransform {
91 origin: DVec3::new(300.0, -80.0, -40.0),
92 rotation: DQuat::from_rotation_z(0.5),
93 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 remove_grid(&mut self, id: GridId) -> Option<Grid>
pub fn remove_grid(&mut self, id: GridId) -> Option<Grid>
Remove a grid by id. Returns the removed Grid (so the
caller can reclaim its chunks) or None if the id wasn’t
registered. Removed ids are not reissued.
Sourcepub fn grid(&self, id: GridId) -> Option<&Grid>
pub fn grid(&self, id: GridId) -> Option<&Grid>
Borrow a registered grid.
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 grid_mut(&mut self, id: GridId) -> Option<&mut Grid>
pub fn grid_mut(&mut self, id: GridId) -> Option<&mut Grid>
Mutably borrow a registered grid.
Examples found in repository?
29fn build_world() -> Scene {
30 let mut scene = Scene::new();
31 let id = scene.add_grid(GridTransform::identity());
32 let g = scene.grid_mut(id).expect("grid");
33 // Ground slab: surface plane at z = 100 (+z is down, so the
34 // slab fills downward).
35 g.set_rect(
36 IVec3::new(20, 20, 100),
37 IVec3::new(140, 140, 120),
38 Some(VoxColor::rgb(0x4d, 0x8a, 0x3a)),
39 );
40 // A 1-voxel ledge to step onto, and a wall to slide along.
41 g.set_rect(
42 IVec3::new(90, 20, 99),
43 IVec3::new(140, 140, 99),
44 Some(VoxColor::rgb(0x77, 0x66, 0x55)),
45 );
46 g.set_rect(
47 IVec3::new(60, 70, 60),
48 IVec3::new(62, 140, 99),
49 Some(VoxColor::rgb(0x88, 0x44, 0x44)),
50 );
51 // A water curtain the veto lets the body wade through. Two
52 // format facts (both pinned as engine tests): pass-through
53 // geometry works up to 2 voxels thick — thicker walls grow a
54 // colourless UnexposedSolid core no veto can classify — and
55 // must sit ≥ 1 voxel inside the chunk (edge voxels lose their
56 // side colours).
57 g.set_rect(IVec3::new(40, 40, 80), IVec3::new(41, 60, 99), Some(WATER));
58 scene
59}More examples
83fn main() {
84 // ANCHOR: scene_grids
85 let mut scene = Scene::new();
86 // A static "world" grid at the origin…
87 let ground_id = scene.add_grid(GridTransform::at(DVec3::ZERO));
88 // …and a second grid placed and rotated independently — its own
89 // f64 world position + quaternion, same chunked voxel payload.
90 let ship_id = scene.add_grid(GridTransform {
91 origin: DVec3::new(300.0, -80.0, -40.0),
92 rotation: DQuat::from_rotation_z(0.5),
93 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 grids(&self) -> impl Iterator<Item = (GridId, &Grid)>
pub fn grids(&self) -> impl Iterator<Item = (GridId, &Grid)>
Iterator over all (id, grid) pairs in registration order
is not guaranteed — the underlying map is a HashMap.
Callers that need a stable order must sort by GridId.
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 grids_mut(&mut self) -> impl Iterator<Item = (GridId, &mut Grid)>
pub fn grids_mut(&mut self) -> impl Iterator<Item = (GridId, &mut Grid)>
Mutable iterator over all (id, grid) pairs. Yield order
is not guaranteed (HashMap-backed).
Sourcepub fn resolve_voxel(
&self,
world: DVec3,
ray_dir: DVec3,
) -> Option<(GridId, IVec3)>
pub fn resolve_voxel( &self, world: DVec3, ray_dir: DVec3, ) -> Option<(GridId, IVec3)>
Resolve a world-space surface hit to the owning grid + its
grid-local voxel — the picking back half. ray_dir is the view
direction the hit was found along (need not be normalised); the
point is nudged half a voxel along it, past the surface and into
the solid cell, before each grid’s Grid::voxel_solid test.
Returns the first grid that is solid there (transform-correct
for rotated/translated grids), or None if none claims it.
Backend-agnostic: pair with a renderer depth read to turn a
click into a voxel — world = cam.pos + t · normalize(ray_dir),
then resolve_voxel(world, ray_dir). roxlap-render’s
SceneRenderer::pick wires exactly that.
Sourcepub fn raycast(
&self,
origin: DVec3,
dir: DVec3,
max_dist: f64,
) -> Option<RayHit>
pub fn raycast( &self, origin: DVec3, dir: DVec3, max_dist: f64, ) -> Option<RayHit>
Cast a world-space ray and return the nearest solid voxel hit
across all grids, or None if nothing solid lies within
max_dist. Renderer-independent (no depth buffer, no camera) —
the primitive for line-of-sight, projectiles, AI probing, and
off-screen / backend-agnostic picking.
dir need not be normalised. Each grid’s ray is transformed
into the grid’s local frame (so rotated / translated grids are
handled exactly) and marched with a voxel DDA against
Grid::voxel_solid; the closest hit by world distance t
wins. The step budget is bounded by max_dist, so empty space
is safe but not free — a chunk-level skip is a future
optimisation if hot.
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_streaming_threads(&mut self, n: usize)
pub fn set_streaming_threads(&mut self, n: usize)
Configure the number of worker threads in the dedicated streaming pool (S7.3).
Lazily applied — the pool itself is constructed on the first
Self::pump_streaming call. If the pool was already built
(i.e. a previous pump_streaming already dispatched at
least one task), it gets dropped and rebuilt. Dropping the
old pool blocks until all of its in-flight tasks finish
(rayon’s contract); any results those tasks sent are still
drained by the next pump_streaming because the channel
survives the rebuild.
The streaming pool is separate from rayon’s global pool (which R12 multicore rendering uses), so chunk generation doesn’t compete with render threads. Sensible values are 1 to ~4 — generation work is CPU-bound but should leave most of the box for everything else.
On wasm32 this is a no-op (no rayon pool available);
pump_streaming runs synchronously there.
§Panics
Panics on native if n == 0 (zero-thread pools are not
supported; the scene crate’s S7.1 helper already disallows
the equivalent for StreamRadius::r_active < 0).
Sourcepub fn pump_streaming(&mut self, camera_world_pos: DVec3)
pub fn pump_streaming(&mut self, camera_world_pos: DVec3)
Asynchronous streaming pump (S7.3).
On native, dispatches missing-chunk generations onto a
dedicated rayon pool, drains any results that arrived since
the last pump, runs the eviction pass, and tracks in-flight
tasks in each grid’s Grid::pending_gen set. The drain
uses the per-chunk version counter from S7.2 to discard
results whose chunk was edited mid-generation.
On wasm32 this short-circuits to Self::pump_streaming_sync
— no thread pool is available there, but the same per-grid
stream-in / evict semantics apply.
Call once per frame from the render thread. Cheap when nothing changed (early-exit on disabled grids, try_recv loops empty fast).
Sourcepub fn pump_streaming_sync(&mut self, camera_world_pos: DVec3)
pub fn pump_streaming_sync(&mut self, camera_world_pos: DVec3)
Synchronous streaming pump (S7.1).
For each grid with a non-StreamRadius::DISABLED policy:
- Project the world-space camera into grid-local coords (inverse rotation + origin subtract).
- Stream in any chunk whose AABB-to-camera distance is
<= r_active, callingGrid::ensure_chunk_generated. No-ops gracefully if the grid has no generator attached (so callers can use the eviction half of streaming on a purely-edited grid). - Evict any chunk whose AABB-to-camera distance exceeds
r_evictfrom the grid’s chunk map. Eviction also clears the cachedBillboardCache(the bounding sphere may shrink, invalidating impostor projections; the next Far-tier render rebuilds lazily).
Both passes use the f64 grid-local position so rotation +
non-axis-aligned grids stream and evict correctly. The
generate path is blocking — S7.3 will move it to a
background rayon pool with pump_streaming (non-blocking).
Callers that want the async variant in S7.0/S7.1 stages
should keep r_active small.
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}Trait Implementations§
Auto Trait Implementations§
impl !RefUnwindSafe for Scene
impl !UnwindSafe for Scene
impl Freeze for Scene
impl Send for Scene
impl Sync for Scene
impl Unpin for Scene
impl UnsafeUnpin for Scene
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