1use super::heightmap::{HeightMap, DiamondSquare, HydraulicErosion};
14use std::collections::{HashMap, VecDeque, BinaryHeap};
15use std::cmp::Ordering;
16
17#[derive(Clone, Debug)]
21pub struct TerrainMesh {
22 pub vertices: Vec<[f32; 3]>,
23 pub normals: Vec<[f32; 3]>,
24 pub indices: Vec<u32>,
25}
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
31pub struct ChunkCoord {
32 pub x: i32,
33 pub z: i32,
34}
35
36impl ChunkCoord {
37 pub const ZERO: Self = Self { x: 0, z: 0 };
38
39 pub fn new(x: i32, z: i32) -> Self { Self { x, z } }
40
41 pub fn manhattan(self, other: ChunkCoord) -> u32 {
43 ((self.x - other.x).abs() + (self.z - other.z).abs()) as u32
44 }
45
46 pub fn distance(self, other: ChunkCoord) -> f32 {
48 let dx = (self.x - other.x) as f32;
49 let dz = (self.z - other.z) as f32;
50 (dx*dx + dz*dz).sqrt()
51 }
52
53 pub fn world_center(self, chunk_size: f32) -> [f32; 3] {
55 let half = chunk_size * 0.5;
56 [self.x as f32 * chunk_size + half, 0.0, self.z as f32 * chunk_size + half]
57 }
58
59 pub fn neighbors_8(self) -> [ChunkCoord; 8] {
61 [
62 ChunkCoord::new(self.x - 1, self.z - 1),
63 ChunkCoord::new(self.x, self.z - 1),
64 ChunkCoord::new(self.x + 1, self.z - 1),
65 ChunkCoord::new(self.x - 1, self.z),
66 ChunkCoord::new(self.x + 1, self.z),
67 ChunkCoord::new(self.x - 1, self.z + 1),
68 ChunkCoord::new(self.x, self.z + 1),
69 ChunkCoord::new(self.x + 1, self.z + 1),
70 ]
71 }
72
73 pub fn neighbors_4(self) -> [ChunkCoord; 4] {
75 [
76 ChunkCoord::new(self.x, self.z - 1),
77 ChunkCoord::new(self.x, self.z + 1),
78 ChunkCoord::new(self.x - 1, self.z),
79 ChunkCoord::new(self.x + 1, self.z),
80 ]
81 }
82}
83
84impl std::fmt::Display for ChunkCoord {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 write!(f, "({}, {})", self.x, self.z)
87 }
88}
89
90#[derive(Clone, Copy, Debug, PartialEq, Eq)]
94pub enum ChunkState {
95 Unloaded,
97 Queued,
99 Loading,
101 Loaded,
103 Unloading,
105}
106
107impl ChunkState {
108 pub fn is_usable(self) -> bool {
109 self == ChunkState::Loaded
110 }
111
112 pub fn is_pending(self) -> bool {
113 matches!(self, ChunkState::Queued | ChunkState::Loading)
114 }
115}
116
117#[derive(Clone, Debug)]
121pub struct TerrainChunkData {
122 pub coord: ChunkCoord,
123 pub heightmap: HeightMap,
124 pub vertices: Vec<[f32; 3]>,
125 pub collision: CollisionHull,
126 pub state: ChunkState,
127 pub seed: u64,
128 pub last_used_frame: u64,
129}
130
131impl TerrainChunkData {
132 pub fn world_aabb(&self, chunk_size: f32, height_scale: f32) -> Aabb {
133 let x0 = self.coord.x as f32 * chunk_size;
134 let z0 = self.coord.z as f32 * chunk_size;
135 let x1 = x0 + chunk_size;
136 let z1 = z0 + chunk_size;
137 let y_min = self.heightmap.data.iter().cloned().fold(f32::INFINITY, f32::min) * height_scale;
138 let y_max = self.heightmap.data.iter().cloned().fold(f32::NEG_INFINITY, f32::max) * height_scale;
139 Aabb { min: [x0, y_min, z0], max: [x1, y_max, z1] }
140 }
141}
142
143#[derive(Clone, Debug)]
147pub struct Aabb {
148 pub min: [f32; 3],
149 pub max: [f32; 3],
150}
151
152impl Aabb {
153 pub fn center(&self) -> [f32; 3] {
154 [
155 (self.min[0] + self.max[0]) * 0.5,
156 (self.min[1] + self.max[1]) * 0.5,
157 (self.min[2] + self.max[2]) * 0.5,
158 ]
159 }
160
161 pub fn contains_xz(&self, x: f32, z: f32) -> bool {
162 x >= self.min[0] && x <= self.max[0] &&
163 z >= self.min[2] && z <= self.max[2]
164 }
165
166 pub fn intersects(&self, other: &Aabb) -> bool {
167 self.min[0] <= other.max[0] && self.max[0] >= other.min[0] &&
168 self.min[1] <= other.max[1] && self.max[1] >= other.min[1] &&
169 self.min[2] <= other.max[2] && self.max[2] >= other.min[2]
170 }
171}
172
173#[derive(Clone, Debug)]
179pub struct CollisionHull {
180 pub heights: Vec<f32>,
182 pub resolution: usize,
183 pub chunk_size: f32,
185 pub height_scale: f32,
186}
187
188impl CollisionHull {
189 pub fn generate(heightmap: &HeightMap, chunk_size: f32, height_scale: f32, resolution: usize) -> Self {
194 let res = resolution.max(2);
195 let mut heights = Vec::with_capacity(res * res);
196
197 for row in 0..res {
198 for col in 0..res {
199 let fx = col as f32 / (res - 1).max(1) as f32 * (heightmap.width - 1) as f32;
200 let fy = row as f32 / (res - 1).max(1) as f32 * (heightmap.height - 1) as f32;
201 heights.push(heightmap.sample_bilinear(fx, fy) * height_scale);
202 }
203 }
204
205 Self { heights, resolution: res, chunk_size, height_scale }
206 }
207
208 pub fn height_at_local(&self, lx: f32, lz: f32) -> f32 {
210 let fx = (lx / self.chunk_size * (self.resolution - 1) as f32).clamp(0.0, (self.resolution - 1) as f32);
211 let fz = (lz / self.chunk_size * (self.resolution - 1) as f32).clamp(0.0, (self.resolution - 1) as f32);
212 let col = fx as usize;
213 let row = fz as usize;
214 let tx = fx - col as f32;
215 let tz = fz - row as f32;
216 let col1 = (col + 1).min(self.resolution - 1);
217 let row1 = (row + 1).min(self.resolution - 1);
218 let h00 = self.heights[row * self.resolution + col];
219 let h10 = self.heights[row * self.resolution + col1];
220 let h01 = self.heights[row1 * self.resolution + col];
221 let h11 = self.heights[row1 * self.resolution + col1];
222 let h0 = h00 + (h10 - h00) * tx;
223 let h1 = h01 + (h11 - h01) * tx;
224 h0 + (h1 - h0) * tz
225 }
226
227 pub fn triangles(&self) -> Vec<[[f32; 3]; 3]> {
229 let res = self.resolution;
230 let cell_w = self.chunk_size / (res - 1).max(1) as f32;
231 let mut tris = Vec::with_capacity((res - 1) * (res - 1) * 2);
232
233 for row in 0..res.saturating_sub(1) {
234 for col in 0..res.saturating_sub(1) {
235 let x0 = col as f32 * cell_w;
236 let x1 = (col + 1) as f32 * cell_w;
237 let z0 = row as f32 * cell_w;
238 let z1 = (row + 1) as f32 * cell_w;
239 let h00 = self.heights[row * res + col];
240 let h10 = self.heights[row * res + col + 1];
241 let h01 = self.heights[(row+1) * res + col];
242 let h11 = self.heights[(row+1) * res + col + 1];
243
244 tris.push([[x0,h00,z0], [x0,h01,z1], [x1,h10,z0]]);
245 tris.push([[x1,h10,z0], [x0,h01,z1], [x1,h11,z1]]);
246 }
247 }
248
249 tris
250 }
251}
252
253#[derive(Clone, Debug)]
257struct PriorityItem {
258 coord: ChunkCoord,
259 priority: u32, }
261
262impl PartialEq for PriorityItem {
263 fn eq(&self, other: &Self) -> bool { self.priority == other.priority }
264}
265
266impl Eq for PriorityItem {}
267
268impl PartialOrd for PriorityItem {
269 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
270 Some(self.cmp(other))
271 }
272}
273
274impl Ord for PriorityItem {
275 fn cmp(&self, other: &Self) -> Ordering {
276 self.priority.cmp(&other.priority)
277 }
278}
279
280#[derive(Clone, Debug)]
284pub struct ChunkGenConfig {
285 pub cells_per_chunk: usize,
287 pub chunk_size: f32,
289 pub height_scale: f32,
291 pub roughness: f32,
293 pub erosion_iters: u32,
295 pub lod_levels: u32,
297 pub collision_res: usize,
299 pub base_seed: u64,
301}
302
303impl Default for ChunkGenConfig {
304 fn default() -> Self {
305 Self {
306 cells_per_chunk: 65,
307 chunk_size: 128.0,
308 height_scale: 80.0,
309 roughness: 0.55,
310 erosion_iters: 200,
311 lod_levels: 4,
312 collision_res: 16,
313 base_seed: 0xdeadbeef_cafebabe,
314 }
315 }
316}
317
318pub struct ChunkGenerator {
320 pub config: ChunkGenConfig,
321}
322
323impl ChunkGenerator {
324 pub fn new(config: ChunkGenConfig) -> Self { Self { config } }
325
326 fn chunk_seed(&self, coord: ChunkCoord) -> u64 {
328 let mut h = self.config.base_seed;
329 h ^= (coord.x as i64 as u64).wrapping_mul(0x9e3779b97f4a7c15);
330 h ^= (coord.z as i64 as u64).wrapping_mul(0x6c62272e07bb0142);
331 h ^= h >> 33;
332 h = h.wrapping_mul(0xff51afd7ed558ccd);
333 h ^= h >> 33;
334 h
335 }
336
337 pub fn generate(&self, coord: ChunkCoord) -> TerrainChunkData {
339 let seed = self.chunk_seed(coord);
340
341 let mut hm = DiamondSquare::generate(self.config.cells_per_chunk, self.config.roughness, seed);
343
344 if self.config.erosion_iters > 0 {
346 HydraulicErosion::erode(
347 &mut hm,
348 self.config.erosion_iters as usize,
349 0.01, 4.0, 0.01, seed ^ 0x1234,
353 );
354 }
355
356 let scale = self.config.chunk_size / (self.config.cells_per_chunk - 1) as f32;
357
358 let mut vertices = Vec::new();
360 for y in 0..hm.height {
361 for x in 0..hm.width {
362 vertices.push([
363 x as f32 * scale,
364 hm.get(x, y) * self.config.height_scale,
365 y as f32 * scale,
366 ]);
367 }
368 }
369
370 let collision = CollisionHull::generate(
372 &hm,
373 self.config.chunk_size,
374 self.config.height_scale,
375 self.config.collision_res,
376 );
377
378 TerrainChunkData {
379 coord,
380 heightmap: hm,
381 vertices,
382 collision,
383 state: ChunkState::Loaded,
384 seed,
385 last_used_frame: 0,
386 }
387 }
388}
389
390pub struct MeshCache {
396 pub capacity: usize,
397 chunks: HashMap<ChunkCoord, TerrainChunkData>,
398 access_order: VecDeque<ChunkCoord>,
400}
401
402impl MeshCache {
403 pub fn new(capacity: usize) -> Self {
404 Self {
405 capacity,
406 chunks: HashMap::with_capacity(capacity + 1),
407 access_order: VecDeque::with_capacity(capacity + 1),
408 }
409 }
410
411 pub fn insert(&mut self, data: TerrainChunkData) {
413 let coord = data.coord;
414 self.chunks.insert(coord, data);
415 self.touch(coord);
416 self.evict_if_needed();
417 }
418
419 pub fn get(&mut self, coord: ChunkCoord) -> Option<&TerrainChunkData> {
421 if self.chunks.contains_key(&coord) {
422 self.touch(coord);
423 self.chunks.get(&coord)
424 } else {
425 None
426 }
427 }
428
429 pub fn peek(&self, coord: ChunkCoord) -> Option<&TerrainChunkData> {
431 self.chunks.get(&coord)
432 }
433
434 pub fn remove(&mut self, coord: ChunkCoord) -> Option<TerrainChunkData> {
436 self.access_order.retain(|c| *c != coord);
437 self.chunks.remove(&coord)
438 }
439
440 pub fn contains(&self, coord: ChunkCoord) -> bool {
441 self.chunks.contains_key(&coord)
442 }
443
444 pub fn len(&self) -> usize { self.chunks.len() }
445 pub fn is_empty(&self) -> bool { self.chunks.is_empty() }
446
447 pub fn iter(&self) -> impl Iterator<Item = (&ChunkCoord, &TerrainChunkData)> {
449 self.chunks.iter()
450 }
451
452 fn touch(&mut self, coord: ChunkCoord) {
453 self.access_order.retain(|c| *c != coord);
454 self.access_order.push_front(coord);
455 }
456
457 fn evict_if_needed(&mut self) {
458 while self.chunks.len() > self.capacity {
459 if let Some(lru) = self.access_order.pop_back() {
460 self.chunks.remove(&lru);
461 } else {
462 break;
463 }
464 }
465 }
466
467 pub fn evict_unloading(&mut self) {
469 let to_remove: Vec<ChunkCoord> = self.chunks.iter()
470 .filter(|(_, v)| v.state == ChunkState::Unloading)
471 .map(|(k, _)| *k)
472 .collect();
473 for coord in to_remove {
474 self.remove(coord);
475 }
476 }
477}
478
479pub struct LoadQueue {
483 heap: BinaryHeap<PriorityItem>,
484 in_queue: HashMap<ChunkCoord, u32>, }
486
487impl LoadQueue {
488 pub fn new() -> Self {
489 Self {
490 heap: BinaryHeap::new(),
491 in_queue: HashMap::new(),
492 }
493 }
494
495 pub fn enqueue(&mut self, coord: ChunkCoord, priority: u32) {
497 if let Some(existing) = self.in_queue.get_mut(&coord) {
498 if priority <= *existing { return; }
499 *existing = priority;
500 } else {
501 self.in_queue.insert(coord, priority);
502 }
503 self.heap.push(PriorityItem { coord, priority });
504 }
505
506 pub fn drain(&mut self, max_count: usize) -> Vec<ChunkCoord> {
508 let mut result = Vec::with_capacity(max_count);
509 while result.len() < max_count {
510 match self.heap.pop() {
511 None => break,
512 Some(item) => {
513 let current = self.in_queue.get(&item.coord).copied().unwrap_or(0);
515 if current != item.priority { continue; }
516 self.in_queue.remove(&item.coord);
517 result.push(item.coord);
518 }
519 }
520 }
521 result
522 }
523
524 pub fn cancel(&mut self, coord: ChunkCoord) {
526 self.in_queue.remove(&coord);
527 }
529
530 pub fn len(&self) -> usize { self.in_queue.len() }
531 pub fn is_empty(&self) -> bool { self.in_queue.is_empty() }
532}
533
534impl Default for LoadQueue {
535 fn default() -> Self { Self::new() }
536}
537
538pub struct LodScheduler {
542 pub lod_distances: Vec<f32>,
545}
546
547impl LodScheduler {
548 pub fn new(chunk_size: f32, max_lod: u32) -> Self {
549 let mut dists = Vec::with_capacity(max_lod as usize + 1);
550 for i in 0..=max_lod {
551 dists.push(chunk_size * 2.0f32.powi(i as i32 + 1));
552 }
553 Self { lod_distances: dists }
554 }
555
556 pub fn select_lod(&self, distance: f32) -> u32 {
558 for (i, &threshold) in self.lod_distances.iter().enumerate() {
559 if distance <= threshold {
560 return i as u32;
561 }
562 }
563 self.lod_distances.len() as u32
564 }
565}
566
567#[derive(Default)]
571pub struct VisibilitySet {
572 visible: HashMap<ChunkCoord, u32>, }
574
575impl VisibilitySet {
576 pub fn new() -> Self { Self::default() }
577
578 pub fn mark_visible(&mut self, coord: ChunkCoord, lod: u32) {
579 self.visible.insert(coord, lod);
580 }
581
582 pub fn is_visible(&self, coord: ChunkCoord) -> bool {
583 self.visible.contains_key(&coord)
584 }
585
586 pub fn lod_for(&self, coord: ChunkCoord) -> Option<u32> {
587 self.visible.get(&coord).copied()
588 }
589
590 pub fn clear(&mut self) { self.visible.clear(); }
591
592 pub fn iter(&self) -> impl Iterator<Item = (&ChunkCoord, &u32)> {
593 self.visible.iter()
594 }
595
596 pub fn len(&self) -> usize { self.visible.len() }
597}
598
599#[derive(Clone, Debug)]
603pub struct StreamingConfig {
604 pub load_radius: u32,
606 pub unload_radius: u32,
608 pub loads_per_frame: usize,
610 pub cache_capacity: usize,
612 pub gen_config: ChunkGenConfig,
614}
615
616impl Default for StreamingConfig {
617 fn default() -> Self {
618 Self {
619 load_radius: 6,
620 unload_radius: 10,
621 loads_per_frame: 2,
622 cache_capacity: 200,
623 gen_config: ChunkGenConfig::default(),
624 }
625 }
626}
627
628#[derive(Clone, Debug, Default)]
630pub struct StreamingStats {
631 pub loaded_chunks: usize,
632 pub queued_chunks: usize,
633 pub chunks_loaded_this_frame: usize,
634 pub chunks_unloaded_this_frame: usize,
635 pub cache_hits: u64,
636 pub cache_misses: u64,
637 pub frame_count: u64,
638}
639
640pub struct ChunkStreamingManager {
645 pub config: StreamingConfig,
646 pub stats: StreamingStats,
647 cache: MeshCache,
648 load_queue: LoadQueue,
649 lod_scheduler: LodScheduler,
650 visibility: VisibilitySet,
651 generator: ChunkGenerator,
652 chunk_states: HashMap<ChunkCoord, ChunkState>,
653 current_frame: u64,
654}
655
656impl ChunkStreamingManager {
657 pub fn new(config: StreamingConfig) -> Self {
658 let gen = ChunkGenerator::new(config.gen_config.clone());
659 let lod = LodScheduler::new(
660 config.gen_config.chunk_size,
661 config.gen_config.lod_levels,
662 );
663 let cache_cap = config.cache_capacity;
664 Self {
665 config,
666 stats: StreamingStats::default(),
667 cache: MeshCache::new(cache_cap),
668 load_queue: LoadQueue::new(),
669 lod_scheduler: lod,
670 visibility: VisibilitySet::new(),
671 generator: gen,
672 chunk_states: HashMap::new(),
673 current_frame: 0,
674 }
675 }
676
677 pub fn update(&mut self, viewer_world_pos: [f32; 3]) {
681 self.current_frame += 1;
682 let chunk_size = self.config.gen_config.chunk_size;
683
684 let viewer_chunk = ChunkCoord::new(
686 (viewer_world_pos[0] / chunk_size).floor() as i32,
687 (viewer_world_pos[2] / chunk_size).floor() as i32,
688 );
689
690 self.visibility.clear();
692 let lr = self.config.load_radius as i32;
693 for dz in -lr..=lr {
694 for dx in -lr..=lr {
695 let coord = ChunkCoord::new(viewer_chunk.x + dx, viewer_chunk.z + dz);
696 let dist = viewer_chunk.distance(coord) * chunk_size;
697 let lod = self.lod_scheduler.select_lod(dist);
698 self.visibility.mark_visible(coord, lod);
699 }
700 }
701
702 let mut to_enqueue = Vec::new();
704 for (&coord, _) in self.visibility.iter() {
705 let state = self.chunk_states.get(&coord).copied().unwrap_or(ChunkState::Unloaded);
706 if state == ChunkState::Unloaded {
707 let dist = viewer_chunk.distance(coord);
708 let priority = (1000.0 / (dist + 1.0)) as u32;
710 to_enqueue.push((coord, priority));
711 }
712 }
713 for (coord, priority) in to_enqueue {
714 self.load_queue.enqueue(coord, priority);
715 self.chunk_states.insert(coord, ChunkState::Queued);
716 }
717
718 let to_load = self.load_queue.drain(self.config.loads_per_frame);
720 let mut loaded_count = 0usize;
721 for coord in to_load {
722 let state = self.chunk_states.get(&coord).copied().unwrap_or(ChunkState::Unloaded);
723 if state != ChunkState::Queued { continue; }
724
725 self.chunk_states.insert(coord, ChunkState::Loading);
726 let mut data = self.generator.generate(coord);
727 data.last_used_frame = self.current_frame;
728 self.cache.insert(data);
729 self.chunk_states.insert(coord, ChunkState::Loaded);
730 loaded_count += 1;
731 }
732
733 let unload_r = self.config.unload_radius as i32;
735 let mut to_unload = Vec::new();
736 for (&coord, &state) in &self.chunk_states {
737 if state == ChunkState::Loaded {
738 let dist = (viewer_chunk.x - coord.x).abs().max((viewer_chunk.z - coord.z).abs());
739 if dist > unload_r {
740 to_unload.push(coord);
741 }
742 }
743 }
744 let unloaded_count = to_unload.len();
745 for coord in to_unload {
746 if let Some(chunk) = self.cache.chunks.get_mut(&coord) {
747 chunk.state = ChunkState::Unloading;
748 }
749 self.chunk_states.insert(coord, ChunkState::Unloading);
750 }
751
752 self.cache.evict_unloading();
754 self.chunk_states.retain(|_, s| *s != ChunkState::Unloading);
755
756 self.stats.loaded_chunks = self.cache.len();
758 self.stats.queued_chunks = self.load_queue.len();
759 self.stats.chunks_loaded_this_frame = loaded_count;
760 self.stats.chunks_unloaded_this_frame = unloaded_count;
761 self.stats.frame_count = self.current_frame;
762 }
763
764 pub fn height_at(&mut self, world_x: f32, world_z: f32) -> Option<f32> {
768 let chunk_size = self.config.gen_config.chunk_size;
769 let cx = (world_x / chunk_size).floor() as i32;
770 let cz = (world_z / chunk_size).floor() as i32;
771 let coord = ChunkCoord::new(cx, cz);
772
773 if let Some(chunk) = self.cache.get(coord) {
774 self.stats.cache_hits += 1;
775 let local_x = world_x - cx as f32 * chunk_size;
776 let local_z = world_z - cz as f32 * chunk_size;
777 Some(chunk.collision.height_at_local(local_x, local_z))
778 } else {
779 self.stats.cache_misses += 1;
780 None
781 }
782 }
783
784 pub fn mesh_for(&mut self, coord: ChunkCoord) -> Option<&[[f32; 3]]> {
786 if let Some(chunk) = self.cache.get(coord) {
787 Some(&chunk.vertices)
788 } else {
789 None
790 }
791 }
792
793 pub fn force_load(&mut self, coord: ChunkCoord) {
795 if self.cache.contains(coord) { return; }
796 let mut data = self.generator.generate(coord);
797 data.last_used_frame = self.current_frame;
798 self.cache.insert(data);
799 self.chunk_states.insert(coord, ChunkState::Loaded);
800 }
801
802 pub fn get_chunk(&self, coord: ChunkCoord) -> Option<&TerrainChunkData> {
804 self.cache.peek(coord)
805 }
806
807 pub fn visible_coords(&self) -> Vec<ChunkCoord> {
809 self.visibility.iter().map(|(&c, _)| c).collect()
810 }
811
812 pub fn chunk_state(&self, coord: ChunkCoord) -> ChunkState {
814 self.chunk_states.get(&coord).copied().unwrap_or(ChunkState::Unloaded)
815 }
816}
817
818pub struct Prefetcher {
822 pub lookahead_frames: u32,
824 prev_chunk: Option<ChunkCoord>,
825}
826
827impl Prefetcher {
828 pub fn new(lookahead_frames: u32) -> Self {
829 Self { lookahead_frames, prev_chunk: None }
830 }
831
832 pub fn prefetch_coords(&mut self, current: ChunkCoord, radius: u32) -> Vec<ChunkCoord> {
834 let velocity = match self.prev_chunk {
835 None => (0, 0),
836 Some(p) => (current.x - p.x, current.z - p.z),
837 };
838 self.prev_chunk = Some(current);
839
840 let look = self.lookahead_frames as i32;
841 let predicted = ChunkCoord::new(
842 current.x + velocity.0 * look,
843 current.z + velocity.1 * look,
844 );
845
846 let r = radius as i32;
847 let mut coords = Vec::new();
848 for dz in -r..=r {
849 for dx in -r..=r {
850 coords.push(ChunkCoord::new(predicted.x + dx, predicted.z + dz));
851 }
852 }
853 coords
854 }
855}
856
857pub struct ChunkSerializer;
862
863impl ChunkSerializer {
864 pub fn serialize_heights(hm: &HeightMap) -> Vec<u8> {
866 let mut out = Vec::with_capacity(8 + hm.data.len() * 4);
867 out.extend_from_slice(&(hm.width as u32).to_le_bytes());
869 out.extend_from_slice(&(hm.height as u32).to_le_bytes());
870 for &v in &hm.data {
871 out.extend_from_slice(&v.to_le_bytes());
872 }
873 out
874 }
875
876 pub fn deserialize_heights(bytes: &[u8]) -> Option<HeightMap> {
878 if bytes.len() < 8 { return None; }
879 let w = u32::from_le_bytes(bytes[0..4].try_into().ok()?) as usize;
880 let h = u32::from_le_bytes(bytes[4..8].try_into().ok()?) as usize;
881 let expected = 8 + w * h * 4;
882 if bytes.len() < expected { return None; }
883 let mut data = Vec::with_capacity(w * h);
884 for i in 0..w*h {
885 let off = 8 + i * 4;
886 let v = f32::from_le_bytes(bytes[off..off+4].try_into().ok()?);
887 data.push(v);
888 }
889 Some(HeightMap { width: w, height: h, data })
890 }
891}
892
893#[cfg(test)]
896mod tests {
897 use super::*;
898
899 #[test]
900 fn test_chunk_coord_distance() {
901 let a = ChunkCoord::new(0, 0);
902 let b = ChunkCoord::new(3, 4);
903 assert!((a.distance(b) - 5.0).abs() < 1e-5);
904 }
905
906 #[test]
907 fn test_chunk_coord_neighbors() {
908 let c = ChunkCoord::new(5, 5);
909 let n4 = c.neighbors_4();
910 assert!(n4.contains(&ChunkCoord::new(5, 4)));
911 assert!(n4.contains(&ChunkCoord::new(5, 6)));
912 assert!(n4.contains(&ChunkCoord::new(4, 5)));
913 assert!(n4.contains(&ChunkCoord::new(6, 5)));
914 }
915
916 #[test]
917 fn test_load_queue_drain() {
918 let mut q = LoadQueue::new();
919 q.enqueue(ChunkCoord::new(0, 0), 100);
920 q.enqueue(ChunkCoord::new(1, 0), 50);
921 q.enqueue(ChunkCoord::new(0, 1), 200);
922 let batch = q.drain(2);
923 assert_eq!(batch.len(), 2);
924 assert_eq!(batch[0], ChunkCoord::new(0, 1));
926 assert_eq!(batch[1], ChunkCoord::new(0, 0));
927 }
928
929 #[test]
930 fn test_load_queue_cancel() {
931 let mut q = LoadQueue::new();
932 q.enqueue(ChunkCoord::new(0, 0), 100);
933 q.cancel(ChunkCoord::new(0, 0));
934 let batch = q.drain(10);
935 assert!(batch.is_empty());
936 }
937
938 #[test]
939 fn test_mesh_cache_lru_eviction() {
940 let mut cache = MeshCache::new(2);
941 let gen = ChunkGenerator::new(ChunkGenConfig {
942 cells_per_chunk: 9,
943 erosion_iters: 0,
944 lod_levels: 1,
945 ..ChunkGenConfig::default()
946 });
947 cache.insert(gen.generate(ChunkCoord::new(0, 0)));
948 cache.insert(gen.generate(ChunkCoord::new(1, 0)));
949 cache.get(ChunkCoord::new(0, 0));
951 cache.insert(gen.generate(ChunkCoord::new(2, 0)));
953 assert_eq!(cache.len(), 2);
954 assert!(cache.peek(ChunkCoord::new(0, 0)).is_some(), "0,0 should still be in cache");
955 assert!(cache.peek(ChunkCoord::new(1, 0)).is_none(), "1,0 should have been evicted");
956 }
957
958 #[test]
959 fn test_collision_hull_height() {
960 let hm = crate::terrain::heightmap::DiamondSquare::generate(8, 0.5, 7);
961 let hull = CollisionHull::generate(&hm, 64.0, 50.0, 8);
962 let h = hull.height_at_local(32.0, 32.0);
963 assert!(h >= 0.0 && h <= 50.0, "height out of range: {h}");
964 }
965
966 #[test]
967 fn test_chunk_generator_output() {
968 let gen = ChunkGenerator::new(ChunkGenConfig {
969 cells_per_chunk: 9,
970 erosion_iters: 0,
971 lod_levels: 2,
972 ..ChunkGenConfig::default()
973 });
974 let chunk = gen.generate(ChunkCoord::new(3, -2));
975 assert_eq!(chunk.coord, ChunkCoord::new(3, -2));
976 assert_eq!(chunk.state, ChunkState::Loaded);
977 assert!(!chunk.vertices.is_empty());
978 }
979
980 #[test]
981 fn test_streaming_manager_basic() {
982 let cfg = StreamingConfig {
983 load_radius: 2,
984 unload_radius: 4,
985 loads_per_frame: 50,
986 cache_capacity: 100,
987 gen_config: ChunkGenConfig {
988 cells_per_chunk: 9,
989 erosion_iters: 0,
990 lod_levels: 1,
991 ..ChunkGenConfig::default()
992 },
993 };
994 let mut mgr = ChunkStreamingManager::new(cfg);
995 mgr.update([0.0, 0.0, 0.0]);
996 mgr.update([0.0, 0.0, 0.0]);
997 assert!(mgr.stats.loaded_chunks > 0, "should have loaded some chunks");
998 }
999
1000 #[test]
1001 fn test_height_query_after_force_load() {
1002 let cfg = StreamingConfig {
1003 gen_config: ChunkGenConfig {
1004 cells_per_chunk: 9,
1005 erosion_iters: 0,
1006 lod_levels: 1,
1007 ..ChunkGenConfig::default()
1008 },
1009 ..StreamingConfig::default()
1010 };
1011 let mut mgr = ChunkStreamingManager::new(cfg);
1012 let coord = ChunkCoord::new(0, 0);
1013 mgr.force_load(coord);
1014 let h = mgr.height_at(64.0, 64.0);
1015 assert!(h.is_some(), "height query should succeed after force load");
1016 let h = h.unwrap();
1017 assert!(h >= 0.0, "height should be non-negative: {h}");
1018 }
1019
1020 #[test]
1021 fn test_serializer_roundtrip() {
1022 let hm = crate::terrain::heightmap::DiamondSquare::generate(16, 0.5, 42);
1023 let bytes = ChunkSerializer::serialize_heights(&hm);
1024 let hm2 = ChunkSerializer::deserialize_heights(&bytes).expect("deserialize failed");
1025 assert_eq!(hm.width, hm2.width);
1026 assert_eq!(hm.height, hm2.height);
1027 for (a, b) in hm.data.iter().zip(hm2.data.iter()) {
1028 assert!((a - b).abs() < 1e-6, "roundtrip mismatch: {a} vs {b}");
1029 }
1030 }
1031
1032 #[test]
1033 fn test_lod_scheduler() {
1034 let sched = LodScheduler::new(128.0, 3);
1035 assert_eq!(sched.select_lod(100.0), 0);
1036 assert_eq!(sched.select_lod(300.0), 1);
1037 assert_eq!(sched.select_lod(600.0), 2);
1038 assert_eq!(sched.select_lod(1200.0), 3);
1039 }
1040
1041 #[test]
1042 fn test_prefetcher() {
1043 let mut pf = Prefetcher::new(3);
1044 let c0 = pf.prefetch_coords(ChunkCoord::new(0, 0), 1);
1046 assert!(!c0.is_empty());
1047 let c1 = pf.prefetch_coords(ChunkCoord::new(1, 0), 1);
1049 assert_eq!(c1.len(), 9);
1051 assert!(c1.contains(&ChunkCoord::new(4, 0)));
1052 }
1053
1054 #[test]
1055 fn test_collision_hull_triangles() {
1056 let hm = crate::terrain::heightmap::DiamondSquare::generate(8, 0.4, 5);
1057 let hull = CollisionHull::generate(&hm, 64.0, 50.0, 4);
1058 let tris = hull.triangles();
1059 assert_eq!(tris.len(), 18);
1061 }
1062
1063 #[test]
1064 fn test_visibility_set() {
1065 let mut vis = VisibilitySet::new();
1066 vis.mark_visible(ChunkCoord::new(1, 2), 0);
1067 vis.mark_visible(ChunkCoord::new(3, 4), 2);
1068 assert!(vis.is_visible(ChunkCoord::new(1, 2)));
1069 assert!(!vis.is_visible(ChunkCoord::new(0, 0)));
1070 assert_eq!(vis.lod_for(ChunkCoord::new(3, 4)), Some(2));
1071 vis.clear();
1072 assert!(!vis.is_visible(ChunkCoord::new(1, 2)));
1073 }
1074
1075 #[test]
1076 fn test_aabb_intersects() {
1077 let a = Aabb { min: [0.0, 0.0, 0.0], max: [10.0, 10.0, 10.0] };
1078 let b = Aabb { min: [5.0, 5.0, 5.0], max: [15.0, 15.0, 15.0] };
1079 let c = Aabb { min: [20.0, 0.0, 0.0], max: [30.0, 10.0, 10.0] };
1080 assert!(a.intersects(&b));
1081 assert!(!a.intersects(&c));
1082 }
1083}