Skip to main content

Scene

Struct Scene 

Source
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

Source

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).

Source

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

Source

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

pub fn 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?
examples/book_scene_graph.rs (line 153)
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

Source

pub fn water_depth_at(&self, world: DVec3) -> Option<(GridId, f64)>

Depth of a world-space point below a water surface, in world units, with the grid that owns the deepest water at that point. None when no grid’s water contains the point.

The world↔grid boundary rule (SC) applies: the point is converted into each grid’s local voxel frame (un-rotate, un-translate, / voxel_world_size), the depth is measured along the grid’s local z (world-vertical for the usual identity/yaw-only water grids), and converted back to world units (× voxel_world_size).

Deterministic: on an exact depth tie between grids (two identity grids sharing a waterline at their seam), the smallest GridId wins — Scene::grids iterates a HashMap, so without the explicit tie-break the winner would flip between runs (and WT.2/WT.3 hang per-grid state off this id).

Source

pub fn in_water(&self, world: DVec3) -> bool

true when the world-space point is inside any water volume. Short-circuits on the first hit — the query the swim state (WT.1) runs per actor per frame; use Self::water_depth_at only when the depth is needed.

Source§

impl Scene

Source

pub fn new() -> Self

New empty scene — no grids.

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

pub fn grid_count(&self) -> usize

Number of grids currently registered.

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

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

pub fn 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.

Source

pub fn grid(&self, id: GridId) -> Option<&Grid>

Borrow a registered grid.

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

pub fn grid_mut(&mut self, id: GridId) -> Option<&mut Grid>

Mutably borrow a registered grid.

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

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

pub fn grids_mut(&mut self) -> impl Iterator<Item = (GridId, &mut Grid)>

Mutable iterator over all (id, grid) pairs. Yield order is not guaranteed (HashMap-backed).

Source

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.

Source

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

pub fn set_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).

Source

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).

Source

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:

  1. Project the world-space camera into grid-local coords (inverse rotation + origin subtract).
  2. Stream in any chunk whose AABB-to-camera distance is <= r_active, calling Grid::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).
  3. Evict any chunk whose AABB-to-camera distance exceeds r_evict from the grid’s chunk map. Eviction also clears the cached BillboardCache (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?
examples/book_scene_graph.rs (line 176)
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§

Source§

impl Debug for Scene

Source§

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

Formats the value using the given formatter. Read more
Source§

impl Default for Scene

Source§

fn default() -> Scene

Returns the “default value” for a type. Read more

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> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

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

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

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

Source§

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

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

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

Source§

fn into(self) -> U

Calls U::from(self).

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

Source§

impl<T> IntoEither for T

Source§

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

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

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

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

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

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

Initializes a with the given initializer. Read more
Source§

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

Dereferences the given pointer. Read more
Source§

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

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

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

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

Source§

type Error = Infallible

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

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

Performs the conversion.
Source§

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

Source§

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

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

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

Performs the conversion.