1#![allow(dead_code, unused_variables, unused_mut, unused_imports)]
2use glam::{Vec2, Vec3, Vec4, Quat, Mat4};
3use std::collections::{HashMap, HashSet, BTreeMap, VecDeque};
4
5const MAX_OCTREE_DEPTH: u32 = 8;
10const MAX_OBJECTS_PER_OCTREE_NODE: usize = 16;
11const BVH_MAX_LEAF_OBJECTS: usize = 4;
12const DEFAULT_CHUNK_SIZE: f32 = 256.0;
13const DEFAULT_STREAMING_RADIUS: f32 = 2048.0;
14const DEFAULT_MAX_LOADED_CHUNKS: usize = 256;
15const DEFAULT_MAX_MEMORY_MB: u64 = 4096;
16const LOD_DISTANCES: [f32; 5] = [64.0, 128.0, 256.0, 512.0, 1024.0];
17const FRUSTUM_NEAR_PLANE: usize = 0;
18const FRUSTUM_FAR_PLANE: usize = 1;
19const FRUSTUM_LEFT_PLANE: usize = 2;
20const FRUSTUM_RIGHT_PLANE: usize = 3;
21const FRUSTUM_TOP_PLANE: usize = 4;
22const FRUSTUM_BOTTOM_PLANE: usize = 5;
23const VIRTUAL_TEXTURE_TILE_SIZE: u32 = 128;
24const VIRTUAL_TEXTURE_ATLAS_SIZE: u32 = 4096;
25const IMPOSTOR_ATLAS_COLS: u32 = 8;
26const IMPOSTOR_ATLAS_ROWS: u32 = 8;
27const TERRAIN_PATCH_SIZE: usize = 65;
28const SCREEN_SPACE_ERROR_THRESHOLD: f32 = 2.0;
29const SAH_TRAVERSAL_COST: f32 = 1.0;
30const SAH_INTERSECTION_COST: f32 = 2.0;
31const LRU_MAX_AGE_FRAMES: u64 = 300;
32const STREAMING_PRIORITY_LEVELS: usize = 8;
33const MAX_ASYNC_LOAD_QUEUE: usize = 1024;
34const HLOD_CLUSTER_RADIUS: f32 = 512.0;
35const DATA_LAYER_MAX: usize = 32;
36const WORLD_PARTITION_CELL_SIZE: f32 = 512.0;
37const PROFILER_HISTORY_FRAMES: usize = 128;
38const NORMAL_SMOOTH_EPSILON: f32 = 1e-6;
39const BILINEAR_CLAMP_EPSILON: f32 = 1e-5;
40const CHUNK_DEPENDENCY_MAX_DEPTH: usize = 16;
41const FEEDBACK_BUFFER_MIPS: usize = 8;
42
43#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
48pub enum LodLevel {
49 Unloaded,
50 Impostor,
51 Low,
52 Medium,
53 High,
54 Ultra,
55}
56
57impl LodLevel {
58 pub fn index(&self) -> usize {
59 match self {
60 LodLevel::Unloaded => 0,
61 LodLevel::Impostor => 1,
62 LodLevel::Low => 2,
63 LodLevel::Medium => 3,
64 LodLevel::High => 4,
65 LodLevel::Ultra => 5,
66 }
67 }
68
69 pub fn from_index(idx: usize) -> Self {
70 match idx {
71 0 => LodLevel::Unloaded,
72 1 => LodLevel::Impostor,
73 2 => LodLevel::Low,
74 3 => LodLevel::Medium,
75 4 => LodLevel::High,
76 5 => LodLevel::Ultra,
77 _ => LodLevel::Unloaded,
78 }
79 }
80
81 pub fn memory_multiplier(&self) -> f32 {
82 match self {
83 LodLevel::Unloaded => 0.0,
84 LodLevel::Impostor => 0.02,
85 LodLevel::Low => 0.1,
86 LodLevel::Medium => 0.3,
87 LodLevel::High => 0.7,
88 LodLevel::Ultra => 1.0,
89 }
90 }
91
92 pub fn vertex_reduction_ratio(&self) -> f32 {
93 match self {
94 LodLevel::Unloaded => 0.0,
95 LodLevel::Impostor => 0.001,
96 LodLevel::Low => 0.05,
97 LodLevel::Medium => 0.2,
98 LodLevel::High => 0.6,
99 LodLevel::Ultra => 1.0,
100 }
101 }
102
103 pub fn next_higher(&self) -> LodLevel {
104 match self {
105 LodLevel::Unloaded => LodLevel::Impostor,
106 LodLevel::Impostor => LodLevel::Low,
107 LodLevel::Low => LodLevel::Medium,
108 LodLevel::Medium => LodLevel::High,
109 LodLevel::High => LodLevel::Ultra,
110 LodLevel::Ultra => LodLevel::Ultra,
111 }
112 }
113
114 pub fn next_lower(&self) -> LodLevel {
115 match self {
116 LodLevel::Unloaded => LodLevel::Unloaded,
117 LodLevel::Impostor => LodLevel::Unloaded,
118 LodLevel::Low => LodLevel::Impostor,
119 LodLevel::Medium => LodLevel::Low,
120 LodLevel::High => LodLevel::Medium,
121 LodLevel::Ultra => LodLevel::High,
122 }
123 }
124
125 pub fn is_loaded(&self) -> bool {
126 !matches!(self, LodLevel::Unloaded)
127 }
128
129 pub fn screen_space_error_threshold(&self) -> f32 {
130 match self {
131 LodLevel::Unloaded => f32::MAX,
132 LodLevel::Impostor => 64.0,
133 LodLevel::Low => 16.0,
134 LodLevel::Medium => 4.0,
135 LodLevel::High => 1.0,
136 LodLevel::Ultra => 0.0,
137 }
138 }
139}
140
141#[derive(Debug, Clone, PartialEq, Eq, Hash)]
142pub enum ChunkLoadState {
143 Unloaded,
144 Queued,
145 Loading,
146 Loaded,
147 Evicting,
148}
149
150impl ChunkLoadState {
151 pub fn can_evict(&self) -> bool {
152 matches!(self, ChunkLoadState::Loaded)
153 }
154
155 pub fn is_pending(&self) -> bool {
156 matches!(self, ChunkLoadState::Queued | ChunkLoadState::Loading)
157 }
158
159 pub fn is_active(&self) -> bool {
160 matches!(self, ChunkLoadState::Loaded | ChunkLoadState::Loading)
161 }
162
163 pub fn transition_to_loaded(&self) -> Option<ChunkLoadState> {
164 match self {
165 ChunkLoadState::Loading => Some(ChunkLoadState::Loaded),
166 _ => None,
167 }
168 }
169
170 pub fn transition_to_unloaded(&self) -> Option<ChunkLoadState> {
171 match self {
172 ChunkLoadState::Evicting => Some(ChunkLoadState::Unloaded),
173 _ => None,
174 }
175 }
176}
177
178#[derive(Debug, Clone, PartialEq, Eq, Hash)]
179pub enum BvhNodeKind {
180 Internal,
181 Leaf,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq, Hash)]
185pub enum EvictionPolicy {
186 Lru,
187 Lfu,
188 DistanceBased,
189 PriorityBased,
190}
191
192#[derive(Debug, Clone, PartialEq, Eq, Hash)]
193pub enum DataLayerMode {
194 Included,
195 Excluded,
196 Inherited,
197}
198
199#[derive(Debug, Clone, PartialEq, Eq, Hash)]
200pub enum StreamingLoadType {
201 Synchronous,
202 Asynchronous,
203 Prefetch,
204}
205
206#[derive(Debug, Clone, PartialEq)]
207pub enum ProfilerEventType {
208 ChunkLoad,
209 ChunkUnload,
210 LodSwitch,
211 FrustumCull,
212 OctreeQuery,
213 BvhQuery,
214 TerrainStitch,
215 ImpostorUpdate,
216 VirtualTextureUpdate,
217 HlodBuild,
218}
219
220#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Copy)]
225pub struct ChunkCoord {
226 pub x: i32,
227 pub y: i32,
228 pub z: i32,
229}
230
231impl ChunkCoord {
232 pub fn new(x: i32, y: i32, z: i32) -> Self {
233 Self { x, y, z }
234 }
235
236 pub fn from_world_pos(pos: Vec3, chunk_size: f32) -> Self {
237 let x = (pos.x / chunk_size).floor() as i32;
238 let y = (pos.y / chunk_size).floor() as i32;
239 let z = (pos.z / chunk_size).floor() as i32;
240 Self { x, y, z }
241 }
242
243 pub fn to_world_min(&self, chunk_size: f32) -> Vec3 {
244 Vec3::new(
245 self.x as f32 * chunk_size,
246 self.y as f32 * chunk_size,
247 self.z as f32 * chunk_size,
248 )
249 }
250
251 pub fn to_world_center(&self, chunk_size: f32) -> Vec3 {
252 let min = self.to_world_min(chunk_size);
253 min + Vec3::splat(chunk_size * 0.5)
254 }
255
256 pub fn manhattan_distance(&self, other: &ChunkCoord) -> i32 {
257 (self.x - other.x).abs() + (self.y - other.y).abs() + (self.z - other.z).abs()
258 }
259
260 pub fn chebyshev_distance(&self, other: &ChunkCoord) -> i32 {
261 let dx = (self.x - other.x).abs();
262 let dy = (self.y - other.y).abs();
263 let dz = (self.z - other.z).abs();
264 dx.max(dy).max(dz)
265 }
266
267 pub fn euclidean_distance_sq(&self, other: &ChunkCoord) -> i64 {
268 let dx = (self.x - other.x) as i64;
269 let dy = (self.y - other.y) as i64;
270 let dz = (self.z - other.z) as i64;
271 dx * dx + dy * dy + dz * dz
272 }
273
274 pub fn neighbors_6(&self) -> [ChunkCoord; 6] {
275 [
276 ChunkCoord::new(self.x + 1, self.y, self.z),
277 ChunkCoord::new(self.x - 1, self.y, self.z),
278 ChunkCoord::new(self.x, self.y + 1, self.z),
279 ChunkCoord::new(self.x, self.y - 1, self.z),
280 ChunkCoord::new(self.x, self.y, self.z + 1),
281 ChunkCoord::new(self.x, self.y, self.z - 1),
282 ]
283 }
284
285 pub fn neighbors_26(&self) -> Vec<ChunkCoord> {
286 let mut result = Vec::with_capacity(26);
287 for dx in -1i32..=1 {
288 for dy in -1i32..=1 {
289 for dz in -1i32..=1 {
290 if dx == 0 && dy == 0 && dz == 0 {
291 continue;
292 }
293 result.push(ChunkCoord::new(self.x + dx, self.y + dy, self.z + dz));
294 }
295 }
296 }
297 result
298 }
299
300 pub fn chunks_in_radius(center: &ChunkCoord, radius: i32) -> Vec<ChunkCoord> {
301 let mut result = Vec::new();
302 for dx in -radius..=radius {
303 for dy in -radius..=radius {
304 for dz in -radius..=radius {
305 let coord = ChunkCoord::new(center.x + dx, center.y + dy, center.z + dz);
306 if coord.chebyshev_distance(center) <= radius {
307 result.push(coord);
308 }
309 }
310 }
311 }
312 result
313 }
314
315 pub fn is_adjacent(&self, other: &ChunkCoord) -> bool {
316 self.chebyshev_distance(other) == 1
317 }
318
319 pub fn offset(&self, dx: i32, dy: i32, dz: i32) -> ChunkCoord {
320 ChunkCoord::new(self.x + dx, self.y + dy, self.z + dz)
321 }
322
323 pub fn pack_u64(&self) -> u64 {
324 let xi = (self.x as i64 + 0x0000_8000i64) as u64;
325 let yi = (self.y as i64 + 0x0000_8000i64) as u64;
326 let zi = (self.z as i64 + 0x0000_8000i64) as u64;
327 (xi & 0xFFFF) | ((yi & 0xFFFF) << 16) | ((zi & 0xFFFF) << 32)
328 }
329
330 pub fn unpack_u64(packed: u64) -> ChunkCoord {
331 let xi = ((packed & 0xFFFF) as i64 - 0x0000_8000i64) as i32;
332 let yi = (((packed >> 16) & 0xFFFF) as i64 - 0x0000_8000i64) as i32;
333 let zi = (((packed >> 32) & 0xFFFF) as i64 - 0x0000_8000i64) as i32;
334 ChunkCoord::new(xi, yi, zi)
335 }
336}
337
338impl std::fmt::Display for ChunkCoord {
339 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
340 write!(f, "({},{},{})", self.x, self.y, self.z)
341 }
342}
343
344#[derive(Debug, Clone, PartialEq)]
349pub struct ChunkBounds {
350 pub min: Vec3,
351 pub max: Vec3,
352}
353
354impl ChunkBounds {
355 pub fn new(min: Vec3, max: Vec3) -> Self {
356 Self { min, max }
357 }
358
359 pub fn from_center_size(center: Vec3, half_size: Vec3) -> Self {
360 Self {
361 min: center - half_size,
362 max: center + half_size,
363 }
364 }
365
366 pub fn from_chunk_coord(coord: &ChunkCoord, chunk_size: f32) -> Self {
367 let min = coord.to_world_min(chunk_size);
368 let max = min + Vec3::splat(chunk_size);
369 Self { min, max }
370 }
371
372 pub fn contains(&self, point: Vec3) -> bool {
373 point.x >= self.min.x && point.x <= self.max.x
374 && point.y >= self.min.y && point.y <= self.max.y
375 && point.z >= self.min.z && point.z <= self.max.z
376 }
377
378 pub fn intersects(&self, other: &ChunkBounds) -> bool {
379 self.min.x <= other.max.x && self.max.x >= other.min.x
380 && self.min.y <= other.max.y && self.max.y >= other.min.y
381 && self.min.z <= other.max.z && self.max.z >= other.min.z
382 }
383
384 pub fn expand(&self, amount: f32) -> ChunkBounds {
385 let delta = Vec3::splat(amount);
386 ChunkBounds {
387 min: self.min - delta,
388 max: self.max + delta,
389 }
390 }
391
392 pub fn volume(&self) -> f32 {
393 let size = self.size();
394 size.x * size.y * size.z
395 }
396
397 pub fn center(&self) -> Vec3 {
398 (self.min + self.max) * 0.5
399 }
400
401 pub fn size(&self) -> Vec3 {
402 self.max - self.min
403 }
404
405 pub fn surface_area(&self) -> f32 {
406 let s = self.size();
407 2.0 * (s.x * s.y + s.y * s.z + s.z * s.x)
408 }
409
410 pub fn half_size(&self) -> Vec3 {
411 self.size() * 0.5
412 }
413
414 pub fn merge(&self, other: &ChunkBounds) -> ChunkBounds {
415 ChunkBounds {
416 min: self.min.min(other.min),
417 max: self.max.max(other.max),
418 }
419 }
420
421 pub fn intersection(&self, other: &ChunkBounds) -> Option<ChunkBounds> {
422 let min = self.min.max(other.min);
423 let max = self.max.min(other.max);
424 if min.x <= max.x && min.y <= max.y && min.z <= max.z {
425 Some(ChunkBounds { min, max })
426 } else {
427 None
428 }
429 }
430
431 pub fn distance_sq_to_point(&self, point: Vec3) -> f32 {
432 let clamped = point.clamp(self.min, self.max);
433 (point - clamped).length_squared()
434 }
435
436 pub fn distance_to_point(&self, point: Vec3) -> f32 {
437 self.distance_sq_to_point(point).sqrt()
438 }
439
440 pub fn closest_point(&self, point: Vec3) -> Vec3 {
441 point.clamp(self.min, self.max)
442 }
443
444 pub fn farthest_point(&self, point: Vec3) -> Vec3 {
445 let center = self.center();
446 let half = self.half_size();
447 let dir = point - center;
448 let sign = Vec3::new(
449 if dir.x >= 0.0 { 1.0 } else { -1.0 },
450 if dir.y >= 0.0 { 1.0 } else { -1.0 },
451 if dir.z >= 0.0 { 1.0 } else { -1.0 },
452 );
453 center + half * sign
454 }
455
456 pub fn octant_bounds(&self, octant: usize) -> ChunkBounds {
457 let center = self.center();
458 let (min_x, max_x) = if octant & 1 == 0 { (self.min.x, center.x) } else { (center.x, self.max.x) };
459 let (min_y, max_y) = if octant & 2 == 0 { (self.min.y, center.y) } else { (center.y, self.max.y) };
460 let (min_z, max_z) = if octant & 4 == 0 { (self.min.z, center.z) } else { (center.z, self.max.z) };
461 ChunkBounds {
462 min: Vec3::new(min_x, min_y, min_z),
463 max: Vec3::new(max_x, max_y, max_z),
464 }
465 }
466
467 pub fn octant_for_point(&self, point: Vec3) -> usize {
468 let center = self.center();
469 let mut octant = 0usize;
470 if point.x > center.x { octant |= 1; }
471 if point.y > center.y { octant |= 2; }
472 if point.z > center.z { octant |= 4; }
473 octant
474 }
475
476 pub fn transformed_by(&self, transform: Mat4) -> ChunkBounds {
477 let corners = self.corners();
478 let mut new_min = Vec3::splat(f32::MAX);
479 let mut new_max = Vec3::splat(f32::MIN);
480 for corner in &corners {
481 let transformed = transform.transform_point3(*corner);
482 new_min = new_min.min(transformed);
483 new_max = new_max.max(transformed);
484 }
485 ChunkBounds { min: new_min, max: new_max }
486 }
487
488 pub fn corners(&self) -> [Vec3; 8] {
489 [
490 Vec3::new(self.min.x, self.min.y, self.min.z),
491 Vec3::new(self.max.x, self.min.y, self.min.z),
492 Vec3::new(self.min.x, self.max.y, self.min.z),
493 Vec3::new(self.max.x, self.max.y, self.min.z),
494 Vec3::new(self.min.x, self.min.y, self.max.z),
495 Vec3::new(self.max.x, self.min.y, self.max.z),
496 Vec3::new(self.min.x, self.max.y, self.max.z),
497 Vec3::new(self.max.x, self.max.y, self.max.z),
498 ]
499 }
500
501 pub fn is_degenerate(&self) -> bool {
502 let size = self.size();
503 size.x <= 0.0 || size.y <= 0.0 || size.z <= 0.0
504 }
505
506 pub fn scale(&self, factor: f32) -> ChunkBounds {
507 let center = self.center();
508 let half = self.half_size() * factor;
509 ChunkBounds {
510 min: center - half,
511 max: center + half,
512 }
513 }
514}
515
516#[derive(Debug, Clone)]
521pub struct StreamingConfig {
522 pub max_loaded_chunks: usize,
523 pub lod_distances: [f32; 5],
524 pub max_memory_mb: u64,
525 pub chunk_size: f32,
526 pub streaming_radius: f32,
527 pub vertical_streaming_radius: f32,
528 pub enable_frustum_culling: bool,
529 pub enable_occlusion_culling: bool,
530 pub enable_hlod: bool,
531 pub enable_virtual_textures: bool,
532 pub enable_impostor_billboards: bool,
533 pub max_concurrent_loads: usize,
534 pub max_concurrent_unloads: usize,
535 pub lod_bias: f32,
536 pub screen_height_pixels: u32,
537 pub fov_vertical_rad: f32,
538 pub eviction_policy: EvictionPolicy,
539 pub prefetch_distance: f32,
540 pub min_lod_retain_frames: u64,
541}
542
543impl Default for StreamingConfig {
544 fn default() -> Self {
545 Self {
546 max_loaded_chunks: DEFAULT_MAX_LOADED_CHUNKS,
547 lod_distances: LOD_DISTANCES,
548 max_memory_mb: DEFAULT_MAX_MEMORY_MB,
549 chunk_size: DEFAULT_CHUNK_SIZE,
550 streaming_radius: DEFAULT_STREAMING_RADIUS,
551 vertical_streaming_radius: DEFAULT_STREAMING_RADIUS * 0.5,
552 enable_frustum_culling: true,
553 enable_occlusion_culling: false,
554 enable_hlod: true,
555 enable_virtual_textures: true,
556 enable_impostor_billboards: true,
557 max_concurrent_loads: 4,
558 max_concurrent_unloads: 2,
559 lod_bias: 0.0,
560 screen_height_pixels: 1080,
561 fov_vertical_rad: std::f32::consts::FRAC_PI_4,
562 eviction_policy: EvictionPolicy::Lru,
563 prefetch_distance: 1.5,
564 min_lod_retain_frames: 10,
565 }
566 }
567}
568
569impl StreamingConfig {
570 pub fn new() -> Self {
571 Self::default()
572 }
573
574 pub fn with_quality_preset(preset: QualityPreset) -> Self {
575 let mut cfg = Self::default();
576 match preset {
577 QualityPreset::Low => {
578 cfg.max_loaded_chunks = 64;
579 cfg.lod_distances = [32.0, 64.0, 128.0, 256.0, 512.0];
580 cfg.max_memory_mb = 1024;
581 cfg.max_concurrent_loads = 2;
582 cfg.enable_hlod = false;
583 cfg.enable_virtual_textures = false;
584 }
585 QualityPreset::Medium => {
586 cfg.max_loaded_chunks = 128;
587 cfg.lod_distances = [48.0, 96.0, 192.0, 384.0, 768.0];
588 cfg.max_memory_mb = 2048;
589 cfg.max_concurrent_loads = 3;
590 }
591 QualityPreset::High => {
592 cfg.max_loaded_chunks = 256;
593 cfg.max_concurrent_loads = 6;
594 cfg.enable_occlusion_culling = true;
595 }
596 QualityPreset::Ultra => {
597 cfg.max_loaded_chunks = 512;
598 cfg.lod_distances = [96.0, 192.0, 384.0, 768.0, 1536.0];
599 cfg.max_memory_mb = 8192;
600 cfg.max_concurrent_loads = 8;
601 cfg.enable_occlusion_culling = true;
602 }
603 }
604 cfg
605 }
606
607 pub fn lod_for_distance(&self, dist: f32) -> LodLevel {
608 let biased = dist * (1.0 + self.lod_bias);
609 if biased < self.lod_distances[0] { LodLevel::Ultra }
610 else if biased < self.lod_distances[1] { LodLevel::High }
611 else if biased < self.lod_distances[2] { LodLevel::Medium }
612 else if biased < self.lod_distances[3] { LodLevel::Low }
613 else if biased < self.lod_distances[4] { LodLevel::Impostor }
614 else { LodLevel::Unloaded }
615 }
616
617 pub fn streaming_radius_chunks(&self) -> i32 {
618 (self.streaming_radius / self.chunk_size).ceil() as i32
619 }
620
621 pub fn memory_budget_per_lod(&self, lod: &LodLevel) -> u64 {
622 let ratio = lod.memory_multiplier();
623 (self.max_memory_mb as f32 * ratio * 0.3) as u64
624 }
625}
626
627#[derive(Debug, Clone)]
628pub enum QualityPreset {
629 Low,
630 Medium,
631 High,
632 Ultra,
633}
634
635#[derive(Debug, Clone)]
640pub struct StreamingCamera {
641 pub position: Vec3,
642 pub forward: Vec3,
643 pub up: Vec3,
644 pub right: Vec3,
645 pub fov_deg: f32,
646 pub near: f32,
647 pub far: f32,
648 pub aspect_ratio: f32,
649 pub frustum_planes: [Vec4; 6],
650 pub view_matrix: Mat4,
651 pub proj_matrix: Mat4,
652 pub view_proj_matrix: Mat4,
653}
654
655impl StreamingCamera {
656 pub fn new(position: Vec3, target: Vec3, up: Vec3, fov_deg: f32, aspect: f32, near: f32, far: f32) -> Self {
657 let forward = (target - position).normalize();
658 let right = forward.cross(up).normalize();
659 let up_corrected = right.cross(forward).normalize();
660 let view = Mat4::look_at_rh(position, target, up_corrected);
661 let proj = Mat4::perspective_rh(fov_deg.to_radians(), aspect, near, far);
662 let view_proj = proj * view;
663 let mut cam = Self {
664 position,
665 forward,
666 up: up_corrected,
667 right,
668 fov_deg,
669 near,
670 far,
671 aspect_ratio: aspect,
672 frustum_planes: [Vec4::ZERO; 6],
673 view_matrix: view,
674 proj_matrix: proj,
675 view_proj_matrix: view_proj,
676 };
677 cam.extract_frustum_planes();
678 cam
679 }
680
681 pub fn extract_frustum_planes(&mut self) {
682 let m = self.view_proj_matrix;
683 let rows = [
684 Vec4::new(m.col(0).x, m.col(1).x, m.col(2).x, m.col(3).x),
685 Vec4::new(m.col(0).y, m.col(1).y, m.col(2).y, m.col(3).y),
686 Vec4::new(m.col(0).z, m.col(1).z, m.col(2).z, m.col(3).z),
687 Vec4::new(m.col(0).w, m.col(1).w, m.col(2).w, m.col(3).w),
688 ];
689 self.frustum_planes[FRUSTUM_NEAR_PLANE] = rows[3] + rows[2];
691 self.frustum_planes[FRUSTUM_FAR_PLANE] = rows[3] - rows[2];
693 self.frustum_planes[FRUSTUM_LEFT_PLANE] = rows[3] + rows[0];
695 self.frustum_planes[FRUSTUM_RIGHT_PLANE] = rows[3] - rows[0];
697 self.frustum_planes[FRUSTUM_TOP_PLANE] = rows[3] - rows[1];
699 self.frustum_planes[FRUSTUM_BOTTOM_PLANE]= rows[3] + rows[1];
701 for plane in &mut self.frustum_planes {
703 let len = Vec3::new(plane.x, plane.y, plane.z).length();
704 if len > 1e-8 {
705 *plane /= len;
706 }
707 }
708 }
709
710 pub fn update_position(&mut self, new_pos: Vec3, new_target: Vec3) {
711 self.position = new_pos;
712 self.forward = (new_target - new_pos).normalize();
713 let world_up = Vec3::Y;
714 self.right = self.forward.cross(world_up).normalize();
715 self.up = self.right.cross(self.forward).normalize();
716 self.view_matrix = Mat4::look_at_rh(new_pos, new_target, self.up);
717 self.view_proj_matrix = self.proj_matrix * self.view_matrix;
718 self.extract_frustum_planes();
719 }
720
721 pub fn project_sphere_to_screen(&self, center: Vec3, radius: f32, screen_height: f32) -> f32 {
722 let dist = (center - self.position).length();
723 if dist <= radius { return screen_height; }
724 let fov_rad = self.fov_deg.to_radians();
725 let proj_radius = (radius / dist) / (fov_rad * 0.5).tan();
726 proj_radius * screen_height
727 }
728
729 pub fn compute_lod_screen_size(&self, bounds: &ChunkBounds, screen_height: f32) -> f32 {
730 let center = bounds.center();
731 let radius = bounds.half_size().length();
732 self.project_sphere_to_screen(center, radius, screen_height)
733 }
734
735 pub fn distance_to_bounds(&self, bounds: &ChunkBounds) -> f32 {
736 bounds.distance_to_point(self.position)
737 }
738}
739
740#[derive(Debug, Clone)]
745pub struct FrustumCulling {
746 pub planes: [Vec4; 6],
747}
748
749impl FrustumCulling {
750 pub fn new(planes: [Vec4; 6]) -> Self {
751 Self { planes }
752 }
753
754 pub fn from_view_proj(view_proj: Mat4) -> Self {
755 let mut planes = [Vec4::ZERO; 6];
756 let m = view_proj;
757 let rows = [
758 Vec4::new(m.col(0).x, m.col(1).x, m.col(2).x, m.col(3).x),
759 Vec4::new(m.col(0).y, m.col(1).y, m.col(2).y, m.col(3).y),
760 Vec4::new(m.col(0).z, m.col(1).z, m.col(2).z, m.col(3).z),
761 Vec4::new(m.col(0).w, m.col(1).w, m.col(2).w, m.col(3).w),
762 ];
763 planes[FRUSTUM_NEAR_PLANE] = rows[3] + rows[2];
764 planes[FRUSTUM_FAR_PLANE] = rows[3] - rows[2];
765 planes[FRUSTUM_LEFT_PLANE] = rows[3] + rows[0];
766 planes[FRUSTUM_RIGHT_PLANE] = rows[3] - rows[0];
767 planes[FRUSTUM_TOP_PLANE] = rows[3] - rows[1];
768 planes[FRUSTUM_BOTTOM_PLANE] = rows[3] + rows[1];
769 for plane in &mut planes {
770 let len = Vec3::new(plane.x, plane.y, plane.z).length();
771 if len > 1e-8 { *plane /= len; }
772 }
773 Self { planes }
774 }
775
776 pub fn test_aabb(&self, bounds: &ChunkBounds) -> FrustumResult {
777 let mut result = FrustumResult::Inside;
778 for plane in &self.planes {
779 let normal = Vec3::new(plane.x, plane.y, plane.z);
780 let d = plane.w;
781 let px = if normal.x >= 0.0 { bounds.max.x } else { bounds.min.x };
783 let py = if normal.y >= 0.0 { bounds.max.y } else { bounds.min.y };
784 let pz = if normal.z >= 0.0 { bounds.max.z } else { bounds.min.z };
785 let p_vert = Vec3::new(px, py, pz);
786 let nx_v = if normal.x >= 0.0 { bounds.min.x } else { bounds.max.x };
788 let ny_v = if normal.y >= 0.0 { bounds.min.y } else { bounds.max.y };
789 let nz_v = if normal.z >= 0.0 { bounds.min.z } else { bounds.max.z };
790 let n_vert = Vec3::new(nx_v, ny_v, nz_v);
791
792 if normal.dot(p_vert) + d < 0.0 {
793 return FrustumResult::Outside;
794 }
795 if normal.dot(n_vert) + d < 0.0 {
796 result = FrustumResult::Intersects;
797 }
798 }
799 result
800 }
801
802 pub fn test_sphere(&self, center: Vec3, radius: f32) -> FrustumResult {
803 let mut result = FrustumResult::Inside;
804 for plane in &self.planes {
805 let normal = Vec3::new(plane.x, plane.y, plane.z);
806 let dist = normal.dot(center) + plane.w;
807 if dist < -radius { return FrustumResult::Outside; }
808 if dist < radius { result = FrustumResult::Intersects; }
809 }
810 result
811 }
812
813 pub fn test_point(&self, point: Vec3) -> bool {
814 for plane in &self.planes {
815 let normal = Vec3::new(plane.x, plane.y, plane.z);
816 if normal.dot(point) + plane.w < 0.0 { return false; }
817 }
818 true
819 }
820
821 pub fn test_aabb_fast(&self, bounds: &ChunkBounds) -> bool {
822 for plane in &self.planes {
823 let normal = Vec3::new(plane.x, plane.y, plane.z);
824 let d = plane.w;
825 let px = if normal.x >= 0.0 { bounds.max.x } else { bounds.min.x };
826 let py = if normal.y >= 0.0 { bounds.max.y } else { bounds.min.y };
827 let pz = if normal.z >= 0.0 { bounds.max.z } else { bounds.min.z };
828 if normal.dot(Vec3::new(px, py, pz)) + d < 0.0 {
829 return false;
830 }
831 }
832 true
833 }
834
835 pub fn compute_visibility_mask(&self, bounds_list: &[ChunkBounds]) -> Vec<bool> {
836 bounds_list.iter().map(|b| self.test_aabb_fast(b)).collect()
837 }
838}
839
840#[derive(Debug, Clone, PartialEq)]
841pub enum FrustumResult {
842 Inside,
843 Outside,
844 Intersects,
845}
846
847#[derive(Debug, Clone)]
852pub struct OctreeNode {
853 pub bounds: ChunkBounds,
854 pub children: Option<Box<[OctreeNode; 8]>>,
855 pub objects: Vec<u32>,
856 pub depth: u32,
857}
858
859impl OctreeNode {
860 pub fn new(bounds: ChunkBounds, depth: u32) -> Self {
861 Self {
862 bounds,
863 children: None,
864 objects: Vec::new(),
865 depth,
866 }
867 }
868
869 pub fn is_leaf(&self) -> bool {
870 self.children.is_none()
871 }
872
873 pub fn object_count(&self) -> usize {
874 self.objects.len()
875 }
876
877 pub fn total_object_count(&self) -> usize {
878 let mut count = self.objects.len();
879 if let Some(children) = &self.children {
880 for child in children.iter() {
881 count += child.total_object_count();
882 }
883 }
884 count
885 }
886
887 pub fn depth(&self) -> u32 {
888 self.depth
889 }
890
891 pub fn max_depth(&self) -> u32 {
892 if let Some(children) = &self.children {
893 children.iter().map(|c| c.max_depth()).max().unwrap_or(self.depth)
894 } else {
895 self.depth
896 }
897 }
898
899 pub fn query_sphere(&self, center: Vec3, radius: f32, result: &mut Vec<u32>) {
900 let dist_sq = self.bounds.distance_sq_to_point(center);
901 if dist_sq > radius * radius { return; }
902 result.extend_from_slice(&self.objects);
903 if let Some(children) = &self.children {
904 for child in children.iter() {
905 child.query_sphere(center, radius, result);
906 }
907 }
908 }
909
910 pub fn query_aabb(&self, query: &ChunkBounds, result: &mut Vec<u32>) {
911 if !self.bounds.intersects(query) { return; }
912 result.extend_from_slice(&self.objects);
913 if let Some(children) = &self.children {
914 for child in children.iter() {
915 child.query_aabb(query, result);
916 }
917 }
918 }
919
920 pub fn query_frustum(&self, frustum: &FrustumCulling, result: &mut Vec<u32>) {
921 let fr = frustum.test_aabb(&self.bounds);
922 match fr {
923 FrustumResult::Outside => return,
924 FrustumResult::Inside => {
925 self.collect_all(result);
926 return;
927 }
928 FrustumResult::Intersects => {
929 result.extend_from_slice(&self.objects);
930 if let Some(children) = &self.children {
931 for child in children.iter() {
932 child.query_frustum(frustum, result);
933 }
934 }
935 }
936 }
937 }
938
939 pub fn collect_all(&self, result: &mut Vec<u32>) {
940 result.extend_from_slice(&self.objects);
941 if let Some(children) = &self.children {
942 for child in children.iter() {
943 child.collect_all(result);
944 }
945 }
946 }
947
948 pub fn node_count(&self) -> usize {
949 let mut count = 1;
950 if let Some(children) = &self.children {
951 for child in children.iter() {
952 count += child.node_count();
953 }
954 }
955 count
956 }
957}
958
959#[derive(Debug, Clone)]
964pub struct OctreeBuilder {
965 pub max_depth: u32,
966 pub max_per_node: usize,
967}
968
969impl OctreeBuilder {
970 pub fn new(max_depth: u32, max_per_node: usize) -> Self {
971 Self { max_depth, max_per_node }
972 }
973
974 pub fn build(&self, points: &[(u32, Vec3)]) -> OctreeNode {
975 if points.is_empty() {
976 return OctreeNode::new(
977 ChunkBounds::new(Vec3::ZERO, Vec3::ZERO),
978 0,
979 );
980 }
981 let bounds = self.compute_bounds(points);
982 let mut root = OctreeNode::new(bounds, 0);
983 for &(id, pos) in points {
984 self.insert(&mut root, id, pos);
985 }
986 root
987 }
988
989 fn compute_bounds(&self, points: &[(u32, Vec3)]) -> ChunkBounds {
990 let mut min = Vec3::splat(f32::MAX);
991 let mut max = Vec3::splat(f32::MIN);
992 for &(_, pos) in points {
993 min = min.min(pos);
994 max = max.max(pos);
995 }
996 let padding = Vec3::splat(0.001);
997 ChunkBounds { min: min - padding, max: max + padding }
998 }
999
1000 pub fn insert(&self, node: &mut OctreeNode, id: u32, pos: Vec3) {
1001 if !node.bounds.contains(pos) { return; }
1002
1003 if node.is_leaf() {
1004 if node.objects.len() < self.max_per_node || node.depth >= self.max_depth {
1005 node.objects.push(id);
1006 } else {
1007 self.subdivide(node);
1008 self.insert_into_children(node, id, pos);
1009 }
1010 } else {
1011 self.insert_into_children(node, id, pos);
1012 }
1013 }
1014
1015 fn insert_into_children(&self, node: &mut OctreeNode, id: u32, pos: Vec3) {
1016 let octant = node.bounds.octant_for_point(pos);
1017 if let Some(children) = &mut node.children {
1018 self.insert(&mut children[octant], id, pos);
1019 }
1020 }
1021
1022 fn subdivide(&self, node: &mut OctreeNode) {
1023 let child_depth = node.depth + 1;
1024 let children: [OctreeNode; 8] = std::array::from_fn(|i| {
1025 let bounds = node.bounds.octant_bounds(i);
1026 OctreeNode::new(bounds, child_depth)
1027 });
1028 node.children = Some(Box::new(children));
1029
1030 let existing = std::mem::take(&mut node.objects);
1031 for id in existing {
1032 node.objects.push(id);
1036 }
1037 }
1038
1039 pub fn build_with_positions(&self, points: &[(u32, Vec3)]) -> (OctreeNode, HashMap<u32, Vec3>) {
1040 let mut pos_map = HashMap::new();
1041 for &(id, pos) in points {
1042 pos_map.insert(id, pos);
1043 }
1044 let bounds = if points.is_empty() {
1045 ChunkBounds::new(Vec3::ZERO, Vec3::ONE)
1046 } else {
1047 self.compute_bounds(points)
1048 };
1049 let mut root = OctreeNode::new(bounds, 0);
1050 for &(id, pos) in points {
1051 self.insert_with_pos(&mut root, id, pos, &pos_map);
1052 }
1053 (root, pos_map)
1054 }
1055
1056 fn insert_with_pos(&self, node: &mut OctreeNode, id: u32, pos: Vec3, pos_map: &HashMap<u32, Vec3>) {
1057 if !node.bounds.contains(pos) { return; }
1058 if node.is_leaf() {
1059 if node.objects.len() < self.max_per_node || node.depth >= self.max_depth {
1060 node.objects.push(id);
1061 } else {
1062 self.subdivide_with_pos(node, pos_map);
1063 let octant = node.bounds.octant_for_point(pos);
1064 if let Some(children) = &mut node.children {
1065 self.insert_with_pos(&mut children[octant], id, pos, pos_map);
1066 }
1067 }
1068 } else {
1069 let octant = node.bounds.octant_for_point(pos);
1070 if let Some(children) = &mut node.children {
1071 self.insert_with_pos(&mut children[octant], id, pos, pos_map);
1072 }
1073 }
1074 }
1075
1076 fn subdivide_with_pos(&self, node: &mut OctreeNode, pos_map: &HashMap<u32, Vec3>) {
1077 let child_depth = node.depth + 1;
1078 let children: [OctreeNode; 8] = std::array::from_fn(|i| {
1079 OctreeNode::new(node.bounds.octant_bounds(i), child_depth)
1080 });
1081 node.children = Some(Box::new(children));
1082 let existing = std::mem::take(&mut node.objects);
1083 for id in existing {
1084 if let Some(&pos) = pos_map.get(&id) {
1085 let octant = node.bounds.octant_for_point(pos);
1086 if let Some(children) = &mut node.children {
1087 children[octant].objects.push(id);
1088 }
1089 } else {
1090 node.objects.push(id);
1091 }
1092 }
1093 }
1094}
1095
1096#[derive(Debug, Clone)]
1101pub struct BvhNode {
1102 pub bounds: ChunkBounds,
1103 pub kind: BvhNodeKind,
1104 pub left: Option<Box<BvhNode>>,
1105 pub right: Option<Box<BvhNode>>,
1106 pub objects: Vec<u32>,
1107 pub parent_index: Option<usize>,
1108}
1109
1110impl BvhNode {
1111 pub fn new_leaf(bounds: ChunkBounds, objects: Vec<u32>) -> Self {
1112 Self {
1113 bounds,
1114 kind: BvhNodeKind::Leaf,
1115 left: None,
1116 right: None,
1117 objects,
1118 parent_index: None,
1119 }
1120 }
1121
1122 pub fn new_internal(bounds: ChunkBounds, left: BvhNode, right: BvhNode) -> Self {
1123 Self {
1124 bounds,
1125 kind: BvhNodeKind::Internal,
1126 left: Some(Box::new(left)),
1127 right: Some(Box::new(right)),
1128 objects: Vec::new(),
1129 parent_index: None,
1130 }
1131 }
1132
1133 pub fn is_leaf(&self) -> bool {
1134 matches!(self.kind, BvhNodeKind::Leaf)
1135 }
1136
1137 pub fn depth(&self) -> usize {
1138 match (&self.left, &self.right) {
1139 (Some(l), Some(r)) => 1 + l.depth().max(r.depth()),
1140 (Some(l), None) => 1 + l.depth(),
1141 (None, Some(r)) => 1 + r.depth(),
1142 (None, None) => 0,
1143 }
1144 }
1145
1146 pub fn node_count(&self) -> usize {
1147 let mut count = 1;
1148 if let Some(l) = &self.left { count += l.node_count(); }
1149 if let Some(r) = &self.right { count += r.node_count(); }
1150 count
1151 }
1152
1153 pub fn query_ray(&self, origin: Vec3, dir: Vec3, t_min: f32, t_max: f32, result: &mut Vec<u32>) {
1154 if !ray_aabb_intersect(origin, dir, &self.bounds, t_min, t_max) { return; }
1155 if self.is_leaf() {
1156 result.extend_from_slice(&self.objects);
1157 return;
1158 }
1159 if let Some(l) = &self.left { l.query_ray(origin, dir, t_min, t_max, result); }
1160 if let Some(r) = &self.right { r.query_ray(origin, dir, t_min, t_max, result); }
1161 }
1162
1163 pub fn query_aabb(&self, query: &ChunkBounds, result: &mut Vec<u32>) {
1164 if !self.bounds.intersects(query) { return; }
1165 if self.is_leaf() {
1166 result.extend_from_slice(&self.objects);
1167 return;
1168 }
1169 if let Some(l) = &self.left { l.query_aabb(query, result); }
1170 if let Some(r) = &self.right { r.query_aabb(query, result); }
1171 }
1172
1173 pub fn query_frustum(&self, frustum: &FrustumCulling, result: &mut Vec<u32>) {
1174 match frustum.test_aabb(&self.bounds) {
1175 FrustumResult::Outside => {}
1176 FrustumResult::Inside => { self.collect_all(result); }
1177 FrustumResult::Intersects => {
1178 if self.is_leaf() {
1179 result.extend_from_slice(&self.objects);
1180 } else {
1181 if let Some(l) = &self.left { l.query_frustum(frustum, result); }
1182 if let Some(r) = &self.right { r.query_frustum(frustum, result); }
1183 }
1184 }
1185 }
1186 }
1187
1188 fn collect_all(&self, result: &mut Vec<u32>) {
1189 result.extend_from_slice(&self.objects);
1190 if let Some(l) = &self.left { l.collect_all(result); }
1191 if let Some(r) = &self.right { r.collect_all(result); }
1192 }
1193}
1194
1195#[derive(Debug, Clone)]
1200pub struct BvhBuilder {
1201 pub max_leaf_objects: usize,
1202 pub num_bins: usize,
1203}
1204
1205impl BvhBuilder {
1206 pub fn new(max_leaf_objects: usize, num_bins: usize) -> Self {
1207 Self { max_leaf_objects, num_bins }
1208 }
1209
1210 pub fn build(&self, objects: &[(u32, ChunkBounds)]) -> Option<BvhNode> {
1211 if objects.is_empty() { return None; }
1212 let indices: Vec<usize> = (0..objects.len()).collect();
1213 Some(self.build_recursive(objects, &indices))
1214 }
1215
1216 fn build_recursive(&self, objects: &[(u32, ChunkBounds)], indices: &[usize]) -> BvhNode {
1217 if indices.len() <= self.max_leaf_objects {
1218 return self.make_leaf(objects, indices);
1219 }
1220
1221 let node_bounds = self.compute_union_bounds(objects, indices);
1222 let (split_axis, split_pos, split_cost) = self.find_best_split(objects, indices, &node_bounds);
1223
1224 let leaf_cost = SAH_INTERSECTION_COST * indices.len() as f32;
1226 if split_cost >= leaf_cost {
1227 return self.make_leaf(objects, indices);
1228 }
1229
1230 let (left_indices, right_indices) = self.partition(objects, indices, split_axis, split_pos);
1231
1232 if left_indices.is_empty() || right_indices.is_empty() {
1233 return self.make_leaf(objects, indices);
1234 }
1235
1236 let left = self.build_recursive(objects, &left_indices);
1237 let right = self.build_recursive(objects, &right_indices);
1238 BvhNode::new_internal(node_bounds, left, right)
1239 }
1240
1241 fn find_best_split(
1242 &self,
1243 objects: &[(u32, ChunkBounds)],
1244 indices: &[usize],
1245 node_bounds: &ChunkBounds,
1246 ) -> (usize, f32, f32) {
1247 let node_sa = node_bounds.surface_area();
1248 let mut best_cost = f32::MAX;
1249 let mut best_axis = 0;
1250 let mut best_split = 0.0f32;
1251
1252 let size = node_bounds.size();
1253
1254 for axis in 0..3 {
1255 let axis_len = match axis { 0 => size.x, 1 => size.y, _ => size.z };
1256 if axis_len < 1e-8 { continue; }
1257
1258 let axis_min = match axis { 0 => node_bounds.min.x, 1 => node_bounds.min.y, _ => node_bounds.min.z };
1259
1260 for bin in 1..self.num_bins {
1261 let t = bin as f32 / self.num_bins as f32;
1262 let split_pos = axis_min + t * axis_len;
1263
1264 let mut left_bounds: Option<ChunkBounds> = None;
1265 let mut right_bounds: Option<ChunkBounds> = None;
1266 let mut left_count = 0usize;
1267 let mut right_count = 0usize;
1268
1269 for &idx in indices {
1270 let center = objects[idx].1.center();
1271 let coord = match axis { 0 => center.x, 1 => center.y, _ => center.z };
1272 if coord < split_pos {
1273 left_count += 1;
1274 left_bounds = Some(match left_bounds {
1275 Some(b) => b.merge(&objects[idx].1),
1276 None => objects[idx].1.clone(),
1277 });
1278 } else {
1279 right_count += 1;
1280 right_bounds = Some(match right_bounds {
1281 Some(b) => b.merge(&objects[idx].1),
1282 None => objects[idx].1.clone(),
1283 });
1284 }
1285 }
1286
1287 if left_count == 0 || right_count == 0 { continue; }
1288
1289 let left_sa = left_bounds.map(|b| b.surface_area()).unwrap_or(0.0);
1290 let right_sa = right_bounds.map(|b| b.surface_area()).unwrap_or(0.0);
1291
1292 let cost = SAH_TRAVERSAL_COST
1293 + SAH_INTERSECTION_COST * (
1294 left_sa / node_sa * left_count as f32
1295 + right_sa / node_sa * right_count as f32
1296 );
1297
1298 if cost < best_cost {
1299 best_cost = cost;
1300 best_axis = axis;
1301 best_split = split_pos;
1302 }
1303 }
1304 }
1305 (best_axis, best_split, best_cost)
1306 }
1307
1308 fn partition(
1309 &self,
1310 objects: &[(u32, ChunkBounds)],
1311 indices: &[usize],
1312 axis: usize,
1313 split: f32,
1314 ) -> (Vec<usize>, Vec<usize>) {
1315 let mut left = Vec::new();
1316 let mut right = Vec::new();
1317 for &idx in indices {
1318 let center = objects[idx].1.center();
1319 let coord = match axis { 0 => center.x, 1 => center.y, _ => center.z };
1320 if coord < split { left.push(idx); } else { right.push(idx); }
1321 }
1322 (left, right)
1323 }
1324
1325 fn make_leaf(&self, objects: &[(u32, ChunkBounds)], indices: &[usize]) -> BvhNode {
1326 let bounds = self.compute_union_bounds(objects, indices);
1327 let ids: Vec<u32> = indices.iter().map(|&i| objects[i].0).collect();
1328 BvhNode::new_leaf(bounds, ids)
1329 }
1330
1331 fn compute_union_bounds(&self, objects: &[(u32, ChunkBounds)], indices: &[usize]) -> ChunkBounds {
1332 let mut min = Vec3::splat(f32::MAX);
1333 let mut max = Vec3::splat(f32::MIN);
1334 for &idx in indices {
1335 min = min.min(objects[idx].1.min);
1336 max = max.max(objects[idx].1.max);
1337 }
1338 ChunkBounds { min, max }
1339 }
1340}
1341
1342pub fn ray_aabb_intersect(origin: Vec3, dir: Vec3, bounds: &ChunkBounds, t_min: f32, t_max: f32) -> bool {
1347 let inv_dir = Vec3::new(
1348 if dir.x.abs() > 1e-12 { 1.0 / dir.x } else { f32::MAX },
1349 if dir.y.abs() > 1e-12 { 1.0 / dir.y } else { f32::MAX },
1350 if dir.z.abs() > 1e-12 { 1.0 / dir.z } else { f32::MAX },
1351 );
1352 let t1 = (bounds.min - origin) * inv_dir;
1353 let t2 = (bounds.max - origin) * inv_dir;
1354 let tmin = t1.min(t2);
1355 let tmax = t1.max(t2);
1356 let enter = tmin.x.max(tmin.y).max(tmin.z).max(t_min);
1357 let exit = tmax.x.min(tmax.y).min(tmax.z).min(t_max);
1358 enter <= exit
1359}
1360
1361#[derive(Debug, Clone)]
1366pub struct StreamingChunk {
1367 pub coord: ChunkCoord,
1368 pub bounds: ChunkBounds,
1369 pub lod_level: LodLevel,
1370 pub load_state: ChunkLoadState,
1371 pub resident_objects: Vec<u32>,
1372 pub memory_bytes: u64,
1373 pub last_visible_frame: u64,
1374 pub last_loaded_frame: u64,
1375 pub load_priority: f32,
1376 pub distance_to_viewer: f32,
1377 pub screen_space_size: f32,
1378 pub is_visible: bool,
1379 pub dependencies: Vec<ChunkCoord>,
1380 pub hlod_cluster_id: Option<u32>,
1381 pub data_layer_mask: u32,
1382 pub version: u32,
1383 pub flags: ChunkFlags,
1384}
1385
1386#[derive(Debug, Clone, Default)]
1387pub struct ChunkFlags {
1388 pub dirty: bool,
1389 pub needs_lod_update: bool,
1390 pub needs_terrain_stitch: bool,
1391 pub impostor_valid: bool,
1392 pub heightmap_loaded: bool,
1393 pub collision_loaded: bool,
1394 pub nav_mesh_loaded: bool,
1395}
1396
1397impl StreamingChunk {
1398 pub fn new(coord: ChunkCoord, chunk_size: f32) -> Self {
1399 let bounds = ChunkBounds::from_chunk_coord(&coord, chunk_size);
1400 Self {
1401 coord,
1402 bounds,
1403 lod_level: LodLevel::Unloaded,
1404 load_state: ChunkLoadState::Unloaded,
1405 resident_objects: Vec::new(),
1406 memory_bytes: 0,
1407 last_visible_frame: 0,
1408 last_loaded_frame: 0,
1409 load_priority: 0.0,
1410 distance_to_viewer: f32::MAX,
1411 screen_space_size: 0.0,
1412 is_visible: false,
1413 dependencies: Vec::new(),
1414 hlod_cluster_id: None,
1415 data_layer_mask: 0xFFFF_FFFF,
1416 version: 0,
1417 flags: ChunkFlags::default(),
1418 }
1419 }
1420
1421 pub fn update_distance(&mut self, viewer_pos: Vec3) {
1422 self.distance_to_viewer = self.bounds.distance_to_point(viewer_pos);
1423 }
1424
1425 pub fn compute_load_priority(&mut self, viewer_pos: Vec3, viewer_forward: Vec3) -> f32 {
1426 let center = self.bounds.center();
1427 let to_chunk = (center - viewer_pos).normalize_or_zero();
1428 let dot = viewer_forward.dot(to_chunk).max(0.0);
1429 let dist_factor = 1.0 / (1.0 + self.distance_to_viewer * 0.01);
1430 let facing_factor = 0.5 + 0.5 * dot;
1431 let priority = dist_factor * facing_factor * (if self.is_visible { 2.0 } else { 1.0 });
1432 self.load_priority = priority;
1433 priority
1434 }
1435
1436 pub fn estimate_memory_for_lod(&self, lod: &LodLevel) -> u64 {
1437 let base_mb = 8u64;
1438 let object_mb = self.resident_objects.len() as u64 * 2;
1439 let total_mb = (base_mb + object_mb) as f32 * lod.memory_multiplier();
1440 (total_mb * 1024.0 * 1024.0) as u64
1441 }
1442
1443 pub fn can_load(&self) -> bool {
1444 matches!(self.load_state, ChunkLoadState::Unloaded | ChunkLoadState::Queued)
1445 }
1446
1447 pub fn can_evict(&self) -> bool {
1448 self.load_state.can_evict() && self.lod_level.is_loaded()
1449 }
1450
1451 pub fn age_frames(&self, current_frame: u64) -> u64 {
1452 current_frame.saturating_sub(self.last_visible_frame)
1453 }
1454
1455 pub fn needs_lod_upgrade(&self, desired_lod: &LodLevel) -> bool {
1456 self.lod_level < *desired_lod
1457 }
1458
1459 pub fn needs_lod_downgrade(&self, desired_lod: &LodLevel) -> bool {
1460 self.lod_level > *desired_lod && self.lod_level != LodLevel::Unloaded
1461 }
1462
1463 pub fn mark_visible(&mut self, frame: u64) {
1464 self.is_visible = true;
1465 self.last_visible_frame = frame;
1466 }
1467
1468 pub fn mark_not_visible(&mut self) {
1469 self.is_visible = false;
1470 }
1471
1472 pub fn add_object(&mut self, object_id: u32) {
1473 if !self.resident_objects.contains(&object_id) {
1474 self.resident_objects.push(object_id);
1475 }
1476 }
1477
1478 pub fn remove_object(&mut self, object_id: u32) {
1479 self.resident_objects.retain(|&id| id != object_id);
1480 }
1481
1482 pub fn is_dependency_satisfied(&self, loaded_chunks: &HashSet<ChunkCoord>) -> bool {
1483 self.dependencies.iter().all(|dep| loaded_chunks.contains(dep))
1484 }
1485}
1486
1487#[derive(Debug, Clone)]
1492pub struct StreamingPriority {
1493 pub queue: BTreeMap<OrderedFloat, ChunkCoord>,
1494 pub coord_to_priority: HashMap<ChunkCoord, f32>,
1495 pub max_size: usize,
1496}
1497
1498#[derive(Debug, Clone, PartialEq)]
1499struct OrderedFloat(f32);
1500
1501impl Eq for OrderedFloat {}
1502
1503impl PartialOrd for OrderedFloat {
1504 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1505 Some(self.cmp(other))
1506 }
1507}
1508
1509impl Ord for OrderedFloat {
1510 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1511 self.0.partial_cmp(&other.0).unwrap_or(std::cmp::Ordering::Equal)
1512 }
1513}
1514
1515impl StreamingPriority {
1516 pub fn new(max_size: usize) -> Self {
1517 Self {
1518 queue: BTreeMap::new(),
1519 coord_to_priority: HashMap::new(),
1520 max_size,
1521 }
1522 }
1523
1524 pub fn push(&mut self, coord: ChunkCoord, priority: f32) {
1525 if let Some(&old_priority) = self.coord_to_priority.get(&coord) {
1526 self.queue.remove(&OrderedFloat(old_priority));
1527 }
1528 self.queue.insert(OrderedFloat(-priority), coord.clone());
1530 self.coord_to_priority.insert(coord, priority);
1531 }
1532
1533 pub fn pop_highest(&mut self) -> Option<(ChunkCoord, f32)> {
1534 if let Some((key, coord)) = self.queue.pop_first() {
1535 let priority = -key.0;
1536 self.coord_to_priority.remove(&coord);
1537 Some((coord, priority))
1538 } else {
1539 None
1540 }
1541 }
1542
1543 pub fn peek_highest(&self) -> Option<(&ChunkCoord, f32)> {
1544 self.queue.iter().next().map(|(k, v)| (v, -k.0))
1545 }
1546
1547 pub fn contains(&self, coord: &ChunkCoord) -> bool {
1548 self.coord_to_priority.contains_key(coord)
1549 }
1550
1551 pub fn remove(&mut self, coord: &ChunkCoord) {
1552 if let Some(priority) = self.coord_to_priority.remove(coord) {
1553 self.queue.remove(&OrderedFloat(-priority));
1554 }
1555 }
1556
1557 pub fn len(&self) -> usize {
1558 self.queue.len()
1559 }
1560
1561 pub fn is_empty(&self) -> bool {
1562 self.queue.is_empty()
1563 }
1564
1565 pub fn update_priorities(&mut self, viewer_pos: Vec3, chunks: &HashMap<ChunkCoord, StreamingChunk>) {
1566 let coords: Vec<ChunkCoord> = self.coord_to_priority.keys().cloned().collect();
1567 for coord in coords {
1568 if let Some(chunk) = chunks.get(&coord) {
1569 let priority = 1.0 / (1.0 + chunk.distance_to_viewer);
1570 self.push(coord, priority);
1571 }
1572 }
1573 }
1574
1575 pub fn drain_up_to(&mut self, n: usize) -> Vec<(ChunkCoord, f32)> {
1576 let mut result = Vec::with_capacity(n);
1577 for _ in 0..n {
1578 if let Some(item) = self.pop_highest() {
1579 result.push(item);
1580 } else {
1581 break;
1582 }
1583 }
1584 result
1585 }
1586
1587 pub fn recompute_all(&mut self, viewer_pos: Vec3, chunk_size: f32) {
1588 let coords: Vec<ChunkCoord> = self.coord_to_priority.keys().cloned().collect();
1589 let old_queue = std::mem::take(&mut self.queue);
1590 self.coord_to_priority.clear();
1591 for coord in coords {
1592 let center = coord.to_world_center(chunk_size);
1593 let dist = (center - viewer_pos).length();
1594 let priority = 1.0 / (1.0 + dist * 0.01);
1595 self.push(coord, priority);
1596 }
1597 }
1598}
1599
1600#[derive(Debug, Clone)]
1605pub struct MemoryBudget {
1606 pub max_bytes: u64,
1607 pub used_bytes: u64,
1608 pub lod_usage: HashMap<LodLevel, u64>,
1609 pub chunk_memory: HashMap<ChunkCoord, u64>,
1610 pub lru_order: VecDeque<ChunkCoord>,
1611 pub access_count: HashMap<ChunkCoord, u64>,
1612 pub last_access_frame: HashMap<ChunkCoord, u64>,
1613 pub policy: EvictionPolicy,
1614}
1615
1616impl MemoryBudget {
1617 pub fn new(max_mb: u64, policy: EvictionPolicy) -> Self {
1618 Self {
1619 max_bytes: max_mb * 1024 * 1024,
1620 used_bytes: 0,
1621 lod_usage: HashMap::new(),
1622 chunk_memory: HashMap::new(),
1623 lru_order: VecDeque::new(),
1624 access_count: HashMap::new(),
1625 last_access_frame: HashMap::new(),
1626 policy,
1627 }
1628 }
1629
1630 pub fn available_bytes(&self) -> u64 {
1631 self.max_bytes.saturating_sub(self.used_bytes)
1632 }
1633
1634 pub fn usage_ratio(&self) -> f32 {
1635 self.used_bytes as f32 / self.max_bytes as f32
1636 }
1637
1638 pub fn can_allocate(&self, bytes: u64) -> bool {
1639 self.used_bytes + bytes <= self.max_bytes
1640 }
1641
1642 pub fn allocate(&mut self, coord: ChunkCoord, lod: LodLevel, bytes: u64, frame: u64) -> bool {
1643 if !self.can_allocate(bytes) { return false; }
1644 self.used_bytes += bytes;
1645 *self.lod_usage.entry(lod).or_insert(0) += bytes;
1646 self.chunk_memory.insert(coord.clone(), bytes);
1647 self.touch(coord, frame);
1648 true
1649 }
1650
1651 pub fn free(&mut self, coord: &ChunkCoord, lod: &LodLevel) {
1652 if let Some(bytes) = self.chunk_memory.remove(coord) {
1653 self.used_bytes = self.used_bytes.saturating_sub(bytes);
1654 if let Some(usage) = self.lod_usage.get_mut(lod) {
1655 *usage = usage.saturating_sub(bytes);
1656 }
1657 }
1658 self.lru_order.retain(|c| c != coord);
1659 self.access_count.remove(coord);
1660 self.last_access_frame.remove(coord);
1661 }
1662
1663 pub fn touch(&mut self, coord: ChunkCoord, frame: u64) {
1664 self.lru_order.retain(|c| c != &coord);
1665 self.lru_order.push_back(coord.clone());
1666 *self.access_count.entry(coord.clone()).or_insert(0) += 1;
1667 self.last_access_frame.insert(coord, frame);
1668 }
1669
1670 pub fn eviction_candidates(&self, num: usize, current_frame: u64) -> Vec<ChunkCoord> {
1671 match self.policy {
1672 EvictionPolicy::Lru => {
1673 self.lru_order.iter().take(num).cloned().collect()
1674 }
1675 EvictionPolicy::Lfu => {
1676 let mut by_count: Vec<_> = self.access_count.iter().collect();
1677 by_count.sort_by_key(|(_, &c)| c);
1678 by_count.iter().take(num).map(|(c, _)| (*c).clone()).collect()
1679 }
1680 EvictionPolicy::DistanceBased => {
1681 let mut by_frame: Vec<_> = self.last_access_frame.iter().collect();
1683 by_frame.sort_by_key(|(_, &f)| f);
1684 by_frame.iter().take(num).map(|(c, _)| (*c).clone()).collect()
1685 }
1686 EvictionPolicy::PriorityBased => {
1687 let mut aged: Vec<_> = self.last_access_frame
1688 .iter()
1689 .filter(|(_, &f)| current_frame.saturating_sub(f) > LRU_MAX_AGE_FRAMES)
1690 .collect();
1691 aged.sort_by_key(|(_, &f)| f);
1692 aged.iter().take(num).map(|(c, _)| (*c).clone()).collect()
1693 }
1694 }
1695 }
1696
1697 pub fn needs_eviction(&self) -> bool {
1698 self.usage_ratio() > 0.95
1699 }
1700
1701 pub fn memory_for_coord(&self, coord: &ChunkCoord) -> u64 {
1702 *self.chunk_memory.get(coord).unwrap_or(&0)
1703 }
1704
1705 pub fn lod_usage_mb(&self, lod: &LodLevel) -> f32 {
1706 *self.lod_usage.get(lod).unwrap_or(&0) as f32 / (1024.0 * 1024.0)
1707 }
1708
1709 pub fn total_used_mb(&self) -> f32 {
1710 self.used_bytes as f32 / (1024.0 * 1024.0)
1711 }
1712
1713 pub fn chunk_count(&self) -> usize {
1714 self.chunk_memory.len()
1715 }
1716}
1717
1718#[derive(Debug, Clone)]
1723pub struct ChunkMeshLod {
1724 pub base_vertex_count: u32,
1725 pub lod_vertex_counts: [u32; 6],
1726 pub simplification_ratios: [f32; 6],
1727 pub screen_space_error_thresholds: [f32; 6],
1728 pub index_buffer_sizes: [u32; 6],
1729 pub memory_sizes_bytes: [u64; 6],
1730 pub transition_distances: [f32; 5],
1731}
1732
1733impl ChunkMeshLod {
1734 pub fn new(base_vertex_count: u32, chunk_size: f32) -> Self {
1735 let ratios: [f32; 6] = [0.0, 0.001, 0.05, 0.2, 0.6, 1.0];
1736 let sse_thresholds: [f32; 6] = [f32::MAX, 64.0, 16.0, 4.0, 1.0, 0.0];
1737 let mut lod_vertex_counts = [0u32; 6];
1738 let mut index_buffer_sizes = [0u32; 6];
1739 let mut memory_sizes = [0u64; 6];
1740 for i in 0..6 {
1741 lod_vertex_counts[i] = (base_vertex_count as f32 * ratios[i]) as u32;
1742 index_buffer_sizes[i] = lod_vertex_counts[i] * 3; memory_sizes[i] = lod_vertex_counts[i] as u64 * 12 + index_buffer_sizes[i] as u64 * 4;
1745 }
1746 Self {
1747 base_vertex_count,
1748 lod_vertex_counts,
1749 simplification_ratios: ratios,
1750 screen_space_error_thresholds: sse_thresholds,
1751 index_buffer_sizes,
1752 memory_sizes_bytes: memory_sizes,
1753 transition_distances: LOD_DISTANCES,
1754 }
1755 }
1756
1757 pub fn vertex_count_for_lod(&self, lod: &LodLevel) -> u32 {
1758 self.lod_vertex_counts[lod.index()]
1759 }
1760
1761 pub fn memory_for_lod(&self, lod: &LodLevel) -> u64 {
1762 self.memory_sizes_bytes[lod.index()]
1763 }
1764
1765 pub fn select_lod_for_screen_size(&self, screen_size_pixels: f32) -> LodLevel {
1766 for i in (0..6).rev() {
1767 if screen_size_pixels >= self.screen_space_error_thresholds[i] {
1768 return LodLevel::from_index(i);
1769 }
1770 }
1771 LodLevel::Unloaded
1772 }
1773
1774 pub fn select_lod_for_distance(&self, dist: f32) -> LodLevel {
1775 if dist < self.transition_distances[0] { LodLevel::Ultra }
1776 else if dist < self.transition_distances[1] { LodLevel::High }
1777 else if dist < self.transition_distances[2] { LodLevel::Medium }
1778 else if dist < self.transition_distances[3] { LodLevel::Low }
1779 else if dist < self.transition_distances[4] { LodLevel::Impostor }
1780 else { LodLevel::Unloaded }
1781 }
1782
1783 pub fn blend_factor(&self, lod: &LodLevel, dist: f32) -> f32 {
1784 let idx = lod.index();
1785 if idx == 0 || idx >= 5 { return 1.0; }
1786 let near = self.transition_distances[idx - 1];
1787 let far = self.transition_distances[idx];
1788 if far <= near { return 1.0; }
1789 ((dist - near) / (far - near)).clamp(0.0, 1.0)
1790 }
1791
1792 pub fn total_triangle_count(&self, lod: &LodLevel) -> u32 {
1793 self.index_buffer_sizes[lod.index()] / 3
1794 }
1795
1796 pub fn reduction_percentage(&self, lod: &LodLevel) -> f32 {
1797 (1.0 - self.simplification_ratios[lod.index()]) * 100.0
1798 }
1799}
1800
1801#[derive(Debug, Clone)]
1806pub struct ImpostorBillboard {
1807 pub atlas_uv_min: Vec2,
1808 pub atlas_uv_max: Vec2,
1809 pub world_position: Vec3,
1810 pub scale: Vec2,
1811 pub pivot_offset: Vec3,
1812 pub facing_angle_rad: f32,
1813 pub num_views: u32,
1814 pub current_view_index: u32,
1815 pub last_update_frame: u64,
1816 pub is_dirty: bool,
1817 pub depth_prepass_enabled: bool,
1818}
1819
1820impl ImpostorBillboard {
1821 pub fn new(world_position: Vec3, scale: Vec2, num_views: u32) -> Self {
1822 let tile_w = 1.0 / IMPOSTOR_ATLAS_COLS as f32;
1823 let tile_h = 1.0 / IMPOSTOR_ATLAS_ROWS as f32;
1824 Self {
1825 atlas_uv_min: Vec2::ZERO,
1826 atlas_uv_max: Vec2::new(tile_w, tile_h),
1827 world_position,
1828 scale,
1829 pivot_offset: Vec3::ZERO,
1830 facing_angle_rad: 0.0,
1831 num_views,
1832 current_view_index: 0,
1833 last_update_frame: 0,
1834 is_dirty: true,
1835 depth_prepass_enabled: false,
1836 }
1837 }
1838
1839 pub fn update_view_index(&mut self, camera_pos: Vec3) {
1840 let to_cam = (camera_pos - self.world_position).normalize_or_zero();
1841 let angle = to_cam.x.atan2(to_cam.z);
1842 let normalized = (angle + std::f32::consts::PI) / (2.0 * std::f32::consts::PI);
1843 self.current_view_index = (normalized * self.num_views as f32) as u32 % self.num_views;
1844 self.facing_angle_rad = angle;
1845 self.update_atlas_uvs();
1846 }
1847
1848 fn update_atlas_uvs(&mut self) {
1849 let cols = IMPOSTOR_ATLAS_COLS;
1850 let rows = IMPOSTOR_ATLAS_ROWS;
1851 let tile_w = 1.0 / cols as f32;
1852 let tile_h = 1.0 / rows as f32;
1853 let col = (self.current_view_index % cols) as f32;
1854 let row = (self.current_view_index / cols) as f32;
1855 self.atlas_uv_min = Vec2::new(col * tile_w, row * tile_h);
1856 self.atlas_uv_max = Vec2::new((col + 1.0) * tile_w, (row + 1.0) * tile_h);
1857 }
1858
1859 pub fn compute_billboard_matrix(&self, camera_pos: Vec3, camera_up: Vec3) -> Mat4 {
1860 let to_cam = (camera_pos - self.world_position).normalize_or_zero();
1861 let right = to_cam.cross(camera_up).normalize_or_zero();
1862 let up = right.cross(to_cam).normalize_or_zero();
1863 let scaled_right = right * self.scale.x;
1864 let scaled_up = up * self.scale.y;
1865 let pos = self.world_position + self.pivot_offset;
1866 Mat4::from_cols(
1867 scaled_right.extend(0.0),
1868 scaled_up.extend(0.0),
1869 to_cam.extend(0.0),
1870 pos.extend(1.0),
1871 )
1872 }
1873
1874 pub fn screen_space_bounds(&self, camera: &StreamingCamera) -> (Vec2, Vec2) {
1875 let proj_pos = camera.proj_matrix * camera.view_matrix * self.world_position.extend(1.0);
1876 if proj_pos.w.abs() < 1e-8 {
1877 return (Vec2::ZERO, Vec2::ZERO);
1878 }
1879 let ndc = proj_pos.truncate() / proj_pos.w;
1880 let half_scale = self.scale * 0.5 / proj_pos.w;
1881 let center_2d = Vec2::new(ndc.x, ndc.y);
1882 (center_2d - half_scale, center_2d + half_scale)
1883 }
1884
1885 pub fn mark_dirty(&mut self) {
1886 self.is_dirty = true;
1887 }
1888
1889 pub fn clear_dirty(&mut self, frame: u64) {
1890 self.is_dirty = false;
1891 self.last_update_frame = frame;
1892 }
1893
1894 pub fn should_update(&self, camera_pos: Vec3, angle_threshold_deg: f32) -> bool {
1895 if self.is_dirty { return true; }
1896 let to_cam = (camera_pos - self.world_position).normalize_or_zero();
1897 let current_angle = to_cam.x.atan2(to_cam.z);
1898 let diff = (current_angle - self.facing_angle_rad).abs();
1899 let wrap = if diff > std::f32::consts::PI { 2.0 * std::f32::consts::PI - diff } else { diff };
1900 let step = 2.0 * std::f32::consts::PI / self.num_views as f32;
1901 wrap > step * 0.5 + angle_threshold_deg.to_radians()
1902 }
1903}
1904
1905#[derive(Debug, Clone)]
1910pub struct TerrainHeightmap {
1911 pub width: usize,
1912 pub height: usize,
1913 pub heights: Vec<f32>,
1914 pub cell_size: f32,
1915 pub origin: Vec2,
1916 pub min_height: f32,
1917 pub max_height: f32,
1918 pub scale_y: f32,
1919}
1920
1921impl TerrainHeightmap {
1922 pub fn new(width: usize, height: usize, cell_size: f32, origin: Vec2, scale_y: f32) -> Self {
1923 let size = width * height;
1924 Self {
1925 width,
1926 height,
1927 heights: vec![0.0; size],
1928 cell_size,
1929 origin,
1930 min_height: 0.0,
1931 max_height: 0.0,
1932 scale_y,
1933 }
1934 }
1935
1936 pub fn set_height(&mut self, x: usize, z: usize, h: f32) {
1937 if x < self.width && z < self.height {
1938 self.heights[z * self.width + x] = h;
1939 self.min_height = self.min_height.min(h);
1940 self.max_height = self.max_height.max(h);
1941 }
1942 }
1943
1944 pub fn get_height(&self, x: usize, z: usize) -> f32 {
1945 if x < self.width && z < self.height {
1946 self.heights[z * self.width + x]
1947 } else {
1948 0.0
1949 }
1950 }
1951
1952 pub fn sample_bilinear(&self, world_x: f32, world_z: f32) -> f32 {
1953 let local_x = (world_x - self.origin.x) / self.cell_size;
1954 let local_z = (world_z - self.origin.y) / self.cell_size;
1955
1956 let ix = local_x.floor() as isize;
1957 let iz = local_z.floor() as isize;
1958 let fx = local_x - ix as f32;
1959 let fz = local_z - iz as f32;
1960
1961 let h00 = self.get_clamped(ix, iz );
1962 let h10 = self.get_clamped(ix + 1, iz );
1963 let h01 = self.get_clamped(ix, iz + 1);
1964 let h11 = self.get_clamped(ix + 1, iz + 1);
1965
1966 let h0 = h00 * (1.0 - fx) + h10 * fx;
1967 let h1 = h01 * (1.0 - fx) + h11 * fx;
1968 (h0 * (1.0 - fz) + h1 * fz) * self.scale_y
1969 }
1970
1971 fn get_clamped(&self, x: isize, z: isize) -> f32 {
1972 let cx = x.clamp(0, self.width as isize - 1) as usize;
1973 let cz = z.clamp(0, self.height as isize - 1) as usize;
1974 self.heights[cz * self.width + cx]
1975 }
1976
1977 pub fn compute_normal(&self, x: usize, z: usize) -> Vec3 {
1978 let left = self.get_height(x.saturating_sub(1), z);
1979 let right = if x + 1 < self.width { self.get_height(x + 1, z) } else { self.get_height(x, z) };
1980 let down = self.get_height(x, z.saturating_sub(1));
1981 let up = if z + 1 < self.height { self.get_height(x, z + 1) } else { self.get_height(x, z) };
1982
1983 let dx = (right - left) * self.scale_y / (2.0 * self.cell_size);
1984 let dz = (up - down ) * self.scale_y / (2.0 * self.cell_size);
1985 Vec3::new(-dx, 1.0, -dz).normalize()
1986 }
1987
1988 pub fn compute_normal_bilinear(&self, world_x: f32, world_z: f32) -> Vec3 {
1989 let epsilon = self.cell_size * 0.5;
1990 let h_px = self.sample_bilinear(world_x + epsilon, world_z);
1991 let h_nx = self.sample_bilinear(world_x - epsilon, world_z);
1992 let h_pz = self.sample_bilinear(world_x, world_z + epsilon);
1993 let h_nz = self.sample_bilinear(world_x, world_z - epsilon);
1994
1995 let dx = (h_px - h_nx) / (2.0 * epsilon);
1996 let dz = (h_pz - h_nz) / (2.0 * epsilon);
1997 Vec3::new(-dx, 1.0, -dz).normalize()
1998 }
1999
2000 pub fn slope_at(&self, world_x: f32, world_z: f32) -> f32 {
2001 let normal = self.compute_normal_bilinear(world_x, world_z);
2002 normal.dot(Vec3::Y).acos().to_degrees()
2003 }
2004
2005 pub fn curvature_at(&self, x: usize, z: usize) -> f32 {
2006 if x == 0 || x >= self.width - 1 || z == 0 || z >= self.height - 1 {
2007 return 0.0;
2008 }
2009 let h = self.get_height(x, z);
2010 let h_l = self.get_height(x - 1, z);
2011 let h_r = self.get_height(x + 1, z);
2012 let h_u = self.get_height(x, z + 1);
2013 let h_d = self.get_height(x, z - 1);
2014
2015 let d2x = (h_l - 2.0 * h + h_r) / (self.cell_size * self.cell_size);
2016 let d2z = (h_d - 2.0 * h + h_u) / (self.cell_size * self.cell_size);
2017 d2x + d2z
2018 }
2019
2020 pub fn bounds(&self) -> ChunkBounds {
2021 let world_width = (self.width - 1) as f32 * self.cell_size;
2022 let world_height = (self.height - 1) as f32 * self.cell_size;
2023 ChunkBounds {
2024 min: Vec3::new(self.origin.x, self.min_height * self.scale_y, self.origin.y),
2025 max: Vec3::new(self.origin.x + world_width, self.max_height * self.scale_y, self.origin.y + world_height),
2026 }
2027 }
2028
2029 pub fn generate_flat(width: usize, height: usize, cell_size: f32, origin: Vec2) -> Self {
2030 TerrainHeightmap::new(width, height, cell_size, origin, 1.0)
2031 }
2032
2033 pub fn generate_sinusoidal(width: usize, height: usize, cell_size: f32, origin: Vec2, amplitude: f32, frequency: f32) -> Self {
2034 let mut hm = TerrainHeightmap::new(width, height, cell_size, origin, 1.0);
2035 for z in 0..height {
2036 for x in 0..width {
2037 let wx = origin.x + x as f32 * cell_size;
2038 let wz = origin.y + z as f32 * cell_size;
2039 let h = amplitude * (wx * frequency).sin() * (wz * frequency).cos();
2040 hm.set_height(x, z, h);
2041 }
2042 }
2043 hm
2044 }
2045}
2046
2047#[derive(Debug, Clone)]
2052pub struct TerrainPatch {
2053 pub coord: ChunkCoord,
2054 pub heightmap: TerrainHeightmap,
2055 pub lod_level: LodLevel,
2056 pub neighbor_lods: [Option<LodLevel>; 4], pub seam_data: [Vec<f32>; 4],
2058 pub needs_stitch: bool,
2059 pub error_metric: f32,
2060}
2061
2062impl TerrainPatch {
2063 pub fn new(coord: ChunkCoord, size: usize, cell_size: f32) -> Self {
2064 let origin = Vec2::new(
2065 coord.x as f32 * (size as f32 - 1.0) * cell_size,
2066 coord.z as f32 * (size as f32 - 1.0) * cell_size,
2067 );
2068 let heightmap = TerrainHeightmap::new(size, size, cell_size, origin, 1.0);
2069 let seam_data = [Vec::new(), Vec::new(), Vec::new(), Vec::new()];
2070 Self {
2071 coord,
2072 heightmap,
2073 lod_level: LodLevel::Unloaded,
2074 neighbor_lods: [None, None, None, None],
2075 seam_data,
2076 needs_stitch: false,
2077 error_metric: 0.0,
2078 }
2079 }
2080
2081 pub fn compute_seam_data(&mut self, side: usize) {
2082 let size = self.heightmap.width;
2083 let mut seam = Vec::with_capacity(size);
2084 match side {
2085 0 => { for z in 0..self.heightmap.height {
2087 seam.push(self.heightmap.get_height(size - 1, z));
2088 }
2089 }
2090 1 => { for z in 0..self.heightmap.height {
2092 seam.push(self.heightmap.get_height(0, z));
2093 }
2094 }
2095 2 => { for x in 0..self.heightmap.width {
2097 seam.push(self.heightmap.get_height(x, size - 1));
2098 }
2099 }
2100 3 => { for x in 0..self.heightmap.width {
2102 seam.push(self.heightmap.get_height(x, 0));
2103 }
2104 }
2105 _ => {}
2106 }
2107 self.seam_data[side] = seam;
2108 }
2109
2110 pub fn stitch_edge(&mut self, side: usize, neighbor_seam: &[f32], neighbor_lod: &LodLevel) {
2111 let my_lod_idx = self.lod_level.index();
2112 let neighbor_lod_idx = neighbor_lod.index();
2113
2114 if my_lod_idx <= neighbor_lod_idx {
2115 return; }
2117
2118 let ratio = (1 << (my_lod_idx - neighbor_lod_idx)) as usize;
2119 let my_size = self.heightmap.width;
2120
2121 match side {
2122 0 => { for z in 0..my_size {
2124 if z % ratio != 0 {
2125 let z0 = (z / ratio) * ratio;
2126 let z1 = (z0 + ratio).min(my_size - 1);
2127 let t = (z - z0) as f32 / ratio as f32;
2128 let h0 = if z0 < neighbor_seam.len() { neighbor_seam[z0 / ratio] } else { 0.0 };
2129 let h1 = if z1 / ratio < neighbor_seam.len() { neighbor_seam[z1 / ratio] } else { h0 };
2130 let blended = h0 * (1.0 - t) + h1 * t;
2131 self.heightmap.set_height(my_size - 1, z, blended);
2132 }
2133 }
2134 }
2135 1 => { for z in 0..my_size {
2137 if z % ratio != 0 {
2138 let z0 = (z / ratio) * ratio;
2139 let z1 = (z0 + ratio).min(my_size - 1);
2140 let t = (z - z0) as f32 / ratio as f32;
2141 let h0 = if z0 < neighbor_seam.len() { neighbor_seam[z0 / ratio] } else { 0.0 };
2142 let h1 = if z1 / ratio < neighbor_seam.len() { neighbor_seam[z1 / ratio] } else { h0 };
2143 let blended = h0 * (1.0 - t) + h1 * t;
2144 self.heightmap.set_height(0, z, blended);
2145 }
2146 }
2147 }
2148 2 => { for x in 0..my_size {
2150 if x % ratio != 0 {
2151 let x0 = (x / ratio) * ratio;
2152 let x1 = (x0 + ratio).min(my_size - 1);
2153 let t = (x - x0) as f32 / ratio as f32;
2154 let h0 = if x0 < neighbor_seam.len() { neighbor_seam[x0 / ratio] } else { 0.0 };
2155 let h1 = if x1 / ratio < neighbor_seam.len() { neighbor_seam[x1 / ratio] } else { h0 };
2156 let blended = h0 * (1.0 - t) + h1 * t;
2157 self.heightmap.set_height(x, my_size - 1, blended);
2158 }
2159 }
2160 }
2161 3 => { for x in 0..my_size {
2163 if x % ratio != 0 {
2164 let x0 = (x / ratio) * ratio;
2165 let x1 = (x0 + ratio).min(my_size - 1);
2166 let t = (x - x0) as f32 / ratio as f32;
2167 let h0 = if x0 < neighbor_seam.len() { neighbor_seam[x0 / ratio] } else { 0.0 };
2168 let h1 = if x1 / ratio < neighbor_seam.len() { neighbor_seam[x1 / ratio] } else { h0 };
2169 let blended = h0 * (1.0 - t) + h1 * t;
2170 self.heightmap.set_height(x, 0, blended);
2171 }
2172 }
2173 }
2174 _ => {}
2175 }
2176 self.needs_stitch = false;
2177 }
2178
2179 pub fn compute_error_metric(&mut self) -> f32 {
2180 let size = self.heightmap.width;
2181 if size < 3 { self.error_metric = 0.0; return 0.0; }
2182 let mut max_error = 0.0f32;
2183 for z in 1..size - 1 {
2184 for x in 1..size - 1 {
2185 let h = self.heightmap.get_height(x, z);
2186 let avg = (
2187 self.heightmap.get_height(x - 1, z) +
2188 self.heightmap.get_height(x + 1, z) +
2189 self.heightmap.get_height(x, z - 1) +
2190 self.heightmap.get_height(x, z + 1)
2191 ) * 0.25;
2192 max_error = max_error.max((h - avg).abs());
2193 }
2194 }
2195 self.error_metric = max_error;
2196 max_error
2197 }
2198
2199 pub fn lod_for_screen_size(&self, screen_pixels: f32) -> LodLevel {
2200 if screen_pixels > 512.0 { LodLevel::Ultra }
2201 else if screen_pixels > 256.0 { LodLevel::High }
2202 else if screen_pixels > 128.0 { LodLevel::Medium }
2203 else if screen_pixels > 64.0 { LodLevel::Low }
2204 else if screen_pixels > 16.0 { LodLevel::Impostor}
2205 else { LodLevel::Unloaded}
2206 }
2207
2208 pub fn update_neighbor_lods(&mut self, neighbors: [Option<LodLevel>; 4]) {
2209 let changed = self.neighbor_lods != neighbors;
2210 self.neighbor_lods = neighbors;
2211 if changed { self.needs_stitch = true; }
2212 }
2213}
2214
2215#[derive(Debug, Clone)]
2220pub struct VirtualTextureTile {
2221 pub mip: u32,
2222 pub tile_x: u32,
2223 pub tile_y: u32,
2224 pub atlas_slot: u32,
2225 pub last_requested_frame: u64,
2226 pub is_resident: bool,
2227 pub priority: f32,
2228}
2229
2230impl VirtualTextureTile {
2231 pub fn new(mip: u32, tile_x: u32, tile_y: u32) -> Self {
2232 Self {
2233 mip,
2234 tile_x,
2235 tile_y,
2236 atlas_slot: u32::MAX,
2237 last_requested_frame: 0,
2238 is_resident: false,
2239 priority: 0.0,
2240 }
2241 }
2242
2243 pub fn tile_id(&self) -> u64 {
2244 (self.mip as u64) | ((self.tile_x as u64) << 8) | ((self.tile_y as u64) << 24)
2245 }
2246}
2247
2248#[derive(Debug, Clone)]
2249pub struct VirtualTexture {
2250 pub page_table: Vec<Vec<u32>>, pub tile_cache: HashMap<u64, VirtualTextureTile>,
2252 pub atlas_size: u32,
2253 pub tile_size: u32,
2254 pub num_mips: u32,
2255 pub max_resident_tiles: usize,
2256 pub resident_count: usize,
2257 pub free_slots: VecDeque<u32>,
2258 pub feedback_buffer: Vec<u32>,
2259 pub current_frame: u64,
2260}
2261
2262impl VirtualTexture {
2263 pub fn new(atlas_size: u32, tile_size: u32, num_mips: u32) -> Self {
2264 let tiles_per_row = atlas_size / tile_size;
2265 let max_tiles = (tiles_per_row * tiles_per_row) as usize;
2266 let mut free_slots = VecDeque::with_capacity(max_tiles);
2267 for i in 0..max_tiles as u32 {
2268 free_slots.push_back(i);
2269 }
2270 let page_table: Vec<Vec<u32>> = (0..num_mips)
2271 .map(|mip| {
2272 let tiles_at_mip = (tiles_per_row >> mip).max(1);
2273 vec![u32::MAX; (tiles_at_mip * tiles_at_mip) as usize]
2274 })
2275 .collect();
2276
2277 Self {
2278 page_table,
2279 tile_cache: HashMap::new(),
2280 atlas_size,
2281 tile_size,
2282 num_mips,
2283 max_resident_tiles: max_tiles,
2284 resident_count: 0,
2285 free_slots,
2286 feedback_buffer: vec![0u32; FEEDBACK_BUFFER_MIPS],
2287 current_frame: 0,
2288 }
2289 }
2290
2291 pub fn request_tile(&mut self, mip: u32, tile_x: u32, tile_y: u32, frame: u64) {
2292 let tile_id = (mip as u64) | ((tile_x as u64) << 8) | ((tile_y as u64) << 24);
2293 if let Some(tile) = self.tile_cache.get_mut(&tile_id) {
2294 tile.last_requested_frame = frame;
2295 return;
2296 }
2297 let mut tile = VirtualTextureTile::new(mip, tile_x, tile_y);
2298 tile.last_requested_frame = frame;
2299 self.tile_cache.insert(tile_id, tile);
2300 }
2301
2302 pub fn load_tile(&mut self, mip: u32, tile_x: u32, tile_y: u32) -> Option<u32> {
2303 let tile_id = (mip as u64) | ((tile_x as u64) << 8) | ((tile_y as u64) << 24);
2304 let slot = self.free_slots.pop_front()?;
2305
2306 if let Some(tile) = self.tile_cache.get_mut(&tile_id) {
2307 tile.atlas_slot = slot;
2308 tile.is_resident = true;
2309 }
2310
2311 let tiles_at_mip = ((self.atlas_size / self.tile_size) >> mip).max(1) as usize;
2312 let index = tile_y as usize * tiles_at_mip + tile_x as usize;
2313 if (mip as usize) < self.page_table.len() && index < self.page_table[mip as usize].len() {
2314 self.page_table[mip as usize][index] = slot;
2315 }
2316
2317 self.resident_count += 1;
2318 Some(slot)
2319 }
2320
2321 pub fn evict_tile(&mut self, mip: u32, tile_x: u32, tile_y: u32) {
2322 let tile_id = (mip as u64) | ((tile_x as u64) << 8) | ((tile_y as u64) << 24);
2323 if let Some(tile) = self.tile_cache.get_mut(&tile_id) {
2324 if tile.is_resident {
2325 let slot = tile.atlas_slot;
2326 tile.is_resident = false;
2327 tile.atlas_slot = u32::MAX;
2328 self.free_slots.push_back(slot);
2329 self.resident_count -= 1;
2330
2331 let tiles_at_mip = ((self.atlas_size / self.tile_size) >> mip).max(1) as usize;
2332 let index = tile_y as usize * tiles_at_mip + tile_x as usize;
2333 if (mip as usize) < self.page_table.len() && index < self.page_table[mip as usize].len() {
2334 self.page_table[mip as usize][index] = u32::MAX;
2335 }
2336 }
2337 }
2338 }
2339
2340 pub fn analyze_feedback_buffer(&self) -> Vec<(u32, u32, u32, f32)> {
2341 let mut requests = Vec::new();
2342 for (mip_idx, &count) in self.feedback_buffer.iter().enumerate() {
2343 if count > 0 {
2344 let mip = mip_idx as u32;
2345 let tiles = ((self.atlas_size / self.tile_size) >> mip).max(1);
2346 let priority = count as f32 / (tiles * tiles) as f32;
2347 for y in 0..tiles {
2349 for x in 0..tiles {
2350 requests.push((mip, x, y, priority));
2351 }
2352 }
2353 }
2354 }
2355 requests.sort_by(|a, b| b.3.partial_cmp(&a.3).unwrap_or(std::cmp::Ordering::Equal));
2356 requests
2357 }
2358
2359 pub fn record_feedback(&mut self, mip: u32) {
2360 let idx = (mip as usize).min(self.feedback_buffer.len() - 1);
2361 self.feedback_buffer[idx] += 1;
2362 }
2363
2364 pub fn clear_feedback(&mut self) {
2365 for v in &mut self.feedback_buffer { *v = 0; }
2366 }
2367
2368 pub fn tile_atlas_uv(&self, slot: u32) -> (Vec2, Vec2) {
2369 let tiles_per_row = self.atlas_size / self.tile_size;
2370 let col = slot % tiles_per_row;
2371 let row = slot / tiles_per_row;
2372 let uv_tile_size = self.tile_size as f32 / self.atlas_size as f32;
2373 let uv_min = Vec2::new(col as f32 * uv_tile_size, row as f32 * uv_tile_size);
2374 let uv_max = uv_min + Vec2::splat(uv_tile_size);
2375 (uv_min, uv_max)
2376 }
2377
2378 pub fn evict_lru_tiles(&mut self, target_free: usize) {
2379 if self.free_slots.len() >= target_free { return; }
2380 let mut by_age: Vec<_> = self.tile_cache.values()
2381 .filter(|t| t.is_resident)
2382 .map(|t| (t.tile_id(), t.last_requested_frame, t.mip, t.tile_x, t.tile_y))
2383 .collect();
2384 by_age.sort_by_key(|&(_, frame, _, _, _)| frame);
2385
2386 let to_evict = (target_free - self.free_slots.len()).min(by_age.len());
2387 for i in 0..to_evict {
2388 let (_, _, mip, tx, ty) = by_age[i];
2389 self.evict_tile(mip, tx, ty);
2390 }
2391 }
2392}
2393
2394#[derive(Debug, Clone, Default)]
2399pub struct StreamingStats {
2400 pub frame_number: u64,
2401 pub chunks_loaded_this_frame: u32,
2402 pub chunks_unloaded_this_frame: u32,
2403 pub chunks_lod_switched_this_frame: u32,
2404 pub total_chunks_loaded: u32,
2405 pub total_chunks_unloaded: u32,
2406 pub total_draw_calls_saved: u64,
2407 pub memory_used_bytes: u64,
2408 pub memory_budget_bytes: u64,
2409 pub chunks_in_frustum: u32,
2410 pub chunks_frustum_culled: u32,
2411 pub chunks_distance_culled: u32,
2412 pub active_chunks: u32,
2413 pub queued_loads: u32,
2414 pub active_loads: u32,
2415 pub impostor_draw_calls: u32,
2416 pub lod_low_draw_calls: u32,
2417 pub lod_medium_draw_calls: u32,
2418 pub lod_high_draw_calls: u32,
2419 pub lod_ultra_draw_calls: u32,
2420 pub terrain_stitch_ops: u32,
2421 pub virtual_texture_uploads: u32,
2422 pub hlod_merges: u32,
2423 pub frame_load_time_us: u64,
2424 pub frame_cull_time_us: u64,
2425 pub frame_lod_time_us: u64,
2426 pub peak_memory_bytes: u64,
2427 pub avg_chunk_load_time_us: f64,
2428 pub total_visible_objects: u32,
2429 pub total_culled_objects: u32,
2430}
2431
2432impl StreamingStats {
2433 pub fn new() -> Self {
2434 Self::default()
2435 }
2436
2437 pub fn reset_frame_counters(&mut self) {
2438 self.chunks_loaded_this_frame = 0;
2439 self.chunks_unloaded_this_frame = 0;
2440 self.chunks_lod_switched_this_frame = 0;
2441 self.impostor_draw_calls = 0;
2442 self.lod_low_draw_calls = 0;
2443 self.lod_medium_draw_calls = 0;
2444 self.lod_high_draw_calls = 0;
2445 self.lod_ultra_draw_calls = 0;
2446 self.terrain_stitch_ops = 0;
2447 self.virtual_texture_uploads = 0;
2448 self.frame_load_time_us = 0;
2449 self.frame_cull_time_us = 0;
2450 self.frame_lod_time_us = 0;
2451 self.chunks_in_frustum = 0;
2452 self.chunks_frustum_culled = 0;
2453 self.chunks_distance_culled = 0;
2454 }
2455
2456 pub fn memory_usage_ratio(&self) -> f32 {
2457 if self.memory_budget_bytes == 0 { return 0.0; }
2458 self.memory_used_bytes as f32 / self.memory_budget_bytes as f32
2459 }
2460
2461 pub fn draw_calls_saved_ratio(&self) -> f32 {
2462 let total = self.impostor_draw_calls + self.lod_low_draw_calls
2463 + self.lod_medium_draw_calls + self.lod_high_draw_calls + self.lod_ultra_draw_calls;
2464 if total == 0 { return 0.0; }
2465 self.total_draw_calls_saved as f32 / total as f32
2466 }
2467
2468 pub fn advance_frame(&mut self) {
2469 self.frame_number += 1;
2470 self.total_chunks_loaded += self.chunks_loaded_this_frame;
2471 self.total_chunks_unloaded += self.chunks_unloaded_this_frame;
2472 if self.memory_used_bytes > self.peak_memory_bytes {
2473 self.peak_memory_bytes = self.memory_used_bytes;
2474 }
2475 if self.chunks_loaded_this_frame > 0 {
2476 let load_time = self.frame_load_time_us as f64;
2477 let count = self.chunks_loaded_this_frame as f64;
2478 let alpha = 0.1;
2479 self.avg_chunk_load_time_us = self.avg_chunk_load_time_us * (1.0 - alpha)
2480 + (load_time / count) * alpha;
2481 }
2482 self.reset_frame_counters();
2483 }
2484
2485 pub fn record_lod_draw_call(&mut self, lod: &LodLevel) {
2486 match lod {
2487 LodLevel::Impostor => self.impostor_draw_calls += 1,
2488 LodLevel::Low => self.lod_low_draw_calls += 1,
2489 LodLevel::Medium => self.lod_medium_draw_calls += 1,
2490 LodLevel::High => self.lod_high_draw_calls += 1,
2491 LodLevel::Ultra => self.lod_ultra_draw_calls += 1,
2492 LodLevel::Unloaded => {}
2493 }
2494 }
2495}
2496
2497#[derive(Debug, Clone)]
2502pub struct WorldPartitionCell {
2503 pub coord: ChunkCoord,
2504 pub bounds: ChunkBounds,
2505 pub actors: Vec<u32>,
2506 pub is_loaded: bool,
2507 pub streaming_source_count: u32,
2508}
2509
2510impl WorldPartitionCell {
2511 pub fn new(coord: ChunkCoord, cell_size: f32) -> Self {
2512 let bounds = ChunkBounds::from_chunk_coord(&coord, cell_size);
2513 Self {
2514 coord,
2515 bounds,
2516 actors: Vec::new(),
2517 is_loaded: false,
2518 streaming_source_count: 0,
2519 }
2520 }
2521
2522 pub fn add_actor(&mut self, actor_id: u32) {
2523 if !self.actors.contains(&actor_id) {
2524 self.actors.push(actor_id);
2525 }
2526 }
2527
2528 pub fn remove_actor(&mut self, actor_id: u32) {
2529 self.actors.retain(|&id| id != actor_id);
2530 }
2531}
2532
2533#[derive(Debug, Clone)]
2534pub struct WorldPartition {
2535 pub cells: HashMap<ChunkCoord, WorldPartitionCell>,
2536 pub cell_size: f32,
2537 pub actor_to_cell: HashMap<u32, ChunkCoord>,
2538 pub loaded_cells: HashSet<ChunkCoord>,
2539 pub streaming_sources: Vec<Vec3>,
2540 pub bounds: ChunkBounds,
2541}
2542
2543impl WorldPartition {
2544 pub fn new(cell_size: f32) -> Self {
2545 Self {
2546 cells: HashMap::new(),
2547 cell_size,
2548 actor_to_cell: HashMap::new(),
2549 loaded_cells: HashSet::new(),
2550 streaming_sources: Vec::new(),
2551 bounds: ChunkBounds::new(Vec3::ZERO, Vec3::ZERO),
2552 }
2553 }
2554
2555 pub fn register_actor(&mut self, actor_id: u32, world_pos: Vec3) {
2556 let coord = ChunkCoord::from_world_pos(world_pos, self.cell_size);
2557 let cell = self.cells.entry(coord.clone()).or_insert_with(|| {
2558 WorldPartitionCell::new(coord.clone(), self.cell_size)
2559 });
2560 cell.add_actor(actor_id);
2561 self.actor_to_cell.insert(actor_id, coord);
2562 self.recompute_bounds();
2563 }
2564
2565 pub fn unregister_actor(&mut self, actor_id: u32) {
2566 if let Some(coord) = self.actor_to_cell.remove(&actor_id) {
2567 if let Some(cell) = self.cells.get_mut(&coord) {
2568 cell.remove_actor(actor_id);
2569 }
2570 }
2571 }
2572
2573 pub fn move_actor(&mut self, actor_id: u32, new_pos: Vec3) {
2574 let new_coord = ChunkCoord::from_world_pos(new_pos, self.cell_size);
2575 if let Some(old_coord) = self.actor_to_cell.get(&actor_id).cloned() {
2576 if old_coord == new_coord { return; }
2577 if let Some(old_cell) = self.cells.get_mut(&old_coord) {
2578 old_cell.remove_actor(actor_id);
2579 }
2580 }
2581 let cell = self.cells.entry(new_coord.clone()).or_insert_with(|| {
2582 WorldPartitionCell::new(new_coord.clone(), self.cell_size)
2583 });
2584 cell.add_actor(actor_id);
2585 self.actor_to_cell.insert(actor_id, new_coord);
2586 }
2587
2588 pub fn add_streaming_source(&mut self, pos: Vec3) {
2589 self.streaming_sources.push(pos);
2590 }
2591
2592 pub fn clear_streaming_sources(&mut self) {
2593 self.streaming_sources.clear();
2594 }
2595
2596 pub fn compute_cells_to_load(&self, load_radius: f32) -> HashSet<ChunkCoord> {
2597 let mut to_load = HashSet::new();
2598 let radius_cells = (load_radius / self.cell_size).ceil() as i32;
2599 for &source in &self.streaming_sources {
2600 let center = ChunkCoord::from_world_pos(source, self.cell_size);
2601 let candidates = ChunkCoord::chunks_in_radius(¢er, radius_cells);
2602 for coord in candidates {
2603 if let Some(cell) = self.cells.get(&coord) {
2604 let dist = cell.bounds.distance_to_point(source);
2605 if dist <= load_radius {
2606 to_load.insert(coord);
2607 }
2608 }
2609 }
2610 }
2611 to_load
2612 }
2613
2614 pub fn get_actors_in_bounds(&self, bounds: &ChunkBounds) -> Vec<u32> {
2615 let mut result = Vec::new();
2616 for (_, cell) in &self.cells {
2617 if cell.bounds.intersects(bounds) {
2618 result.extend_from_slice(&cell.actors);
2619 }
2620 }
2621 result
2622 }
2623
2624 pub fn get_actors_in_radius(&self, center: Vec3, radius: f32) -> Vec<u32> {
2625 let query_bounds = ChunkBounds::from_center_size(center, Vec3::splat(radius));
2626 let mut result = Vec::new();
2627 for (_, cell) in &self.cells {
2628 if cell.bounds.intersects(&query_bounds) {
2629 for &actor in &cell.actors {
2630 result.push(actor);
2631 }
2632 }
2633 }
2634 result
2635 }
2636
2637 fn recompute_bounds(&mut self) {
2638 let mut min = Vec3::splat(f32::MAX);
2639 let mut max = Vec3::splat(f32::MIN);
2640 for (_, cell) in &self.cells {
2641 if !cell.actors.is_empty() {
2642 min = min.min(cell.bounds.min);
2643 max = max.max(cell.bounds.max);
2644 }
2645 }
2646 if min.x <= max.x {
2647 self.bounds = ChunkBounds { min, max };
2648 }
2649 }
2650
2651 pub fn cell_count(&self) -> usize {
2652 self.cells.len()
2653 }
2654
2655 pub fn actor_count(&self) -> usize {
2656 self.actor_to_cell.len()
2657 }
2658}
2659
2660#[derive(Debug, Clone)]
2665pub struct ActorStreamingProxy {
2666 pub actor_id: u32,
2667 pub world_position: Vec3,
2668 pub world_rotation: Quat,
2669 pub world_scale: Vec3,
2670 pub bounds: ChunkBounds,
2671 pub streaming_distance: f32,
2672 pub lod_level: LodLevel,
2673 pub is_loaded: bool,
2674 pub data_layer_mask: u32,
2675 pub hlod_cluster_id: Option<u32>,
2676 pub last_frame_visible: u64,
2677 pub importance: f32,
2678}
2679
2680impl ActorStreamingProxy {
2681 pub fn new(actor_id: u32, position: Vec3, bounds: ChunkBounds) -> Self {
2682 Self {
2683 actor_id,
2684 world_position: position,
2685 world_rotation: Quat::IDENTITY,
2686 world_scale: Vec3::ONE,
2687 bounds,
2688 streaming_distance: 1000.0,
2689 lod_level: LodLevel::Unloaded,
2690 is_loaded: false,
2691 data_layer_mask: 0xFFFF_FFFF,
2692 hlod_cluster_id: None,
2693 last_frame_visible: 0,
2694 importance: 1.0,
2695 }
2696 }
2697
2698 pub fn world_transform(&self) -> Mat4 {
2699 Mat4::from_scale_rotation_translation(
2700 self.world_scale,
2701 self.world_rotation,
2702 self.world_position,
2703 )
2704 }
2705
2706 pub fn distance_to_viewer(&self, viewer_pos: Vec3) -> f32 {
2707 (self.world_position - viewer_pos).length()
2708 }
2709
2710 pub fn should_load(&self, viewer_pos: Vec3) -> bool {
2711 let dist = self.distance_to_viewer(viewer_pos);
2712 dist <= self.streaming_distance
2713 }
2714
2715 pub fn desired_lod(&self, viewer_pos: Vec3, config: &StreamingConfig) -> LodLevel {
2716 let dist = self.distance_to_viewer(viewer_pos);
2717 config.lod_for_distance(dist * self.importance.recip())
2718 }
2719
2720 pub fn screen_size_at_distance(&self, viewer_pos: Vec3, camera: &StreamingCamera, screen_height: f32) -> f32 {
2721 let radius = self.bounds.half_size().length();
2722 camera.project_sphere_to_screen(self.world_position, radius, screen_height)
2723 }
2724
2725 pub fn is_in_data_layer(&self, layer_mask: u32) -> bool {
2726 (self.data_layer_mask & layer_mask) != 0
2727 }
2728
2729 pub fn update_transform(&mut self, pos: Vec3, rot: Quat, scale: Vec3) {
2730 self.world_position = pos;
2731 self.world_rotation = rot;
2732 self.world_scale = scale;
2733 let extent = self.bounds.size() * scale * 0.5;
2734 self.bounds = ChunkBounds::from_center_size(pos, extent);
2735 }
2736}
2737
2738#[derive(Debug, Clone)]
2743pub struct DataLayer {
2744 pub id: u32,
2745 pub name: String,
2746 pub mode: DataLayerMode,
2747 pub parent_id: Option<u32>,
2748 pub child_ids: Vec<u32>,
2749 pub is_visible: bool,
2750 pub is_loaded: bool,
2751 pub actor_ids: Vec<u32>,
2752 pub spatial_bounds: Option<ChunkBounds>,
2753 pub load_state: ChunkLoadState,
2754 pub debug_color: Vec3,
2755}
2756
2757impl DataLayer {
2758 pub fn new(id: u32, name: String) -> Self {
2759 Self {
2760 id,
2761 name,
2762 mode: DataLayerMode::Inherited,
2763 parent_id: None,
2764 child_ids: Vec::new(),
2765 is_visible: true,
2766 is_loaded: false,
2767 actor_ids: Vec::new(),
2768 spatial_bounds: None,
2769 load_state: ChunkLoadState::Unloaded,
2770 debug_color: Vec3::ONE,
2771 }
2772 }
2773
2774 pub fn add_actor(&mut self, actor_id: u32) {
2775 if !self.actor_ids.contains(&actor_id) {
2776 self.actor_ids.push(actor_id);
2777 }
2778 }
2779
2780 pub fn remove_actor(&mut self, actor_id: u32) {
2781 self.actor_ids.retain(|&id| id != actor_id);
2782 }
2783
2784 pub fn is_active(&self) -> bool {
2785 self.is_visible && self.is_loaded
2786 }
2787
2788 pub fn effective_mode(&self) -> DataLayerMode {
2789 match &self.mode {
2790 DataLayerMode::Inherited => DataLayerMode::Included,
2791 other => other.clone(),
2792 }
2793 }
2794}
2795
2796#[derive(Debug, Clone)]
2797pub struct DataLayerSystem {
2798 pub layers: HashMap<u32, DataLayer>,
2799 pub next_id: u32,
2800 pub active_layers: HashSet<u32>,
2801 pub actor_layer_map: HashMap<u32, Vec<u32>>,
2802}
2803
2804impl DataLayerSystem {
2805 pub fn new() -> Self {
2806 Self {
2807 layers: HashMap::new(),
2808 next_id: 1,
2809 active_layers: HashSet::new(),
2810 actor_layer_map: HashMap::new(),
2811 }
2812 }
2813
2814 pub fn create_layer(&mut self, name: String) -> u32 {
2815 let id = self.next_id;
2816 self.next_id += 1;
2817 self.layers.insert(id, DataLayer::new(id, name));
2818 id
2819 }
2820
2821 pub fn delete_layer(&mut self, id: u32) {
2822 if let Some(layer) = self.layers.remove(&id) {
2823 for actor_id in &layer.actor_ids {
2824 if let Some(layers) = self.actor_layer_map.get_mut(actor_id) {
2825 layers.retain(|&lid| lid != id);
2826 }
2827 }
2828 }
2829 self.active_layers.remove(&id);
2830 }
2831
2832 pub fn add_actor_to_layer(&mut self, layer_id: u32, actor_id: u32) {
2833 if let Some(layer) = self.layers.get_mut(&layer_id) {
2834 layer.add_actor(actor_id);
2835 }
2836 self.actor_layer_map.entry(actor_id).or_insert_with(Vec::new).push(layer_id);
2837 }
2838
2839 pub fn activate_layer(&mut self, id: u32) {
2840 self.active_layers.insert(id);
2841 if let Some(layer) = self.layers.get_mut(&id) {
2842 layer.is_loaded = true;
2843 layer.is_visible = true;
2844 }
2845 }
2846
2847 pub fn deactivate_layer(&mut self, id: u32) {
2848 self.active_layers.remove(&id);
2849 if let Some(layer) = self.layers.get_mut(&id) {
2850 layer.is_loaded = false;
2851 layer.is_visible = false;
2852 }
2853 }
2854
2855 pub fn is_actor_visible(&self, actor_id: u32) -> bool {
2856 match self.actor_layer_map.get(&actor_id) {
2857 None => true,
2858 Some(layer_ids) => {
2859 layer_ids.iter().all(|lid| {
2860 self.layers.get(lid).map_or(true, |l| {
2861 match l.effective_mode() {
2862 DataLayerMode::Included => self.active_layers.contains(lid),
2863 DataLayerMode::Excluded => !self.active_layers.contains(lid),
2864 DataLayerMode::Inherited => true,
2865 }
2866 })
2867 })
2868 }
2869 }
2870 }
2871
2872 pub fn actors_in_active_layers(&self) -> Vec<u32> {
2873 let mut actors = Vec::new();
2874 for lid in &self.active_layers {
2875 if let Some(layer) = self.layers.get(lid) {
2876 actors.extend_from_slice(&layer.actor_ids);
2877 }
2878 }
2879 actors.sort_unstable();
2880 actors.dedup();
2881 actors
2882 }
2883
2884 pub fn compute_layer_bounds(&mut self, layer_id: u32, actor_positions: &HashMap<u32, Vec3>) {
2885 if let Some(layer) = self.layers.get_mut(&layer_id) {
2886 let mut min = Vec3::splat(f32::MAX);
2887 let mut max = Vec3::splat(f32::MIN);
2888 for &actor_id in &layer.actor_ids {
2889 if let Some(&pos) = actor_positions.get(&actor_id) {
2890 min = min.min(pos);
2891 max = max.max(pos);
2892 }
2893 }
2894 if min.x <= max.x {
2895 layer.spatial_bounds = Some(ChunkBounds {
2896 min: min - Vec3::splat(1.0),
2897 max: max + Vec3::splat(1.0),
2898 });
2899 }
2900 }
2901 }
2902
2903 pub fn layer_count(&self) -> usize {
2904 self.layers.len()
2905 }
2906}
2907
2908#[derive(Debug, Clone)]
2913pub struct HlodCluster {
2914 pub cluster_id: u32,
2915 pub actor_ids: Vec<u32>,
2916 pub merged_bounds: ChunkBounds,
2917 pub center: Vec3,
2918 pub radius: f32,
2919 pub lod_level: LodLevel,
2920 pub simplified_vertex_count: u32,
2921 pub simplified_triangle_count: u32,
2922 pub memory_bytes: u64,
2923 pub is_built: bool,
2924 pub sub_clusters: Vec<u32>,
2925 pub parent_cluster_id: Option<u32>,
2926 pub depth: u32,
2927 pub importance: f32,
2928}
2929
2930impl HlodCluster {
2931 pub fn new(cluster_id: u32, depth: u32) -> Self {
2932 Self {
2933 cluster_id,
2934 actor_ids: Vec::new(),
2935 merged_bounds: ChunkBounds::new(Vec3::ZERO, Vec3::ZERO),
2936 center: Vec3::ZERO,
2937 radius: 0.0,
2938 lod_level: LodLevel::Low,
2939 simplified_vertex_count: 0,
2940 simplified_triangle_count: 0,
2941 memory_bytes: 0,
2942 is_built: false,
2943 sub_clusters: Vec::new(),
2944 parent_cluster_id: None,
2945 depth,
2946 importance: 1.0,
2947 }
2948 }
2949
2950 pub fn add_actor(&mut self, actor_id: u32, bounds: &ChunkBounds) {
2951 self.actor_ids.push(actor_id);
2952 if self.actor_ids.len() == 1 {
2953 self.merged_bounds = bounds.clone();
2954 } else {
2955 self.merged_bounds = self.merged_bounds.merge(bounds);
2956 }
2957 self.center = self.merged_bounds.center();
2958 let half = self.merged_bounds.half_size();
2959 self.radius = half.length();
2960 }
2961
2962 pub fn compute_simplified_geometry(&mut self) {
2963 let tris_per_actor = 1000u32;
2965 let total_tris = tris_per_actor * self.actor_ids.len() as u32;
2966 let ratio = match self.depth {
2967 0 => 0.1,
2968 1 => 0.2,
2969 2 => 0.4,
2970 _ => 0.5,
2971 };
2972 self.simplified_triangle_count = (total_tris as f32 * ratio) as u32;
2973 self.simplified_vertex_count = (self.simplified_triangle_count as f32 * 0.6) as u32;
2974 self.memory_bytes = self.simplified_vertex_count as u64 * 32 + self.simplified_triangle_count as u64 * 12; }
2977
2978 pub fn compute_screen_size(&self, camera_pos: Vec3, screen_height: f32, fov_rad: f32) -> f32 {
2979 let dist = (self.center - camera_pos).length();
2980 if dist < 1e-6 { return screen_height; }
2981 let angular = self.radius / dist;
2982 let half_fov_tan = (fov_rad * 0.5).tan();
2983 (angular / half_fov_tan) * screen_height
2984 }
2985
2986 pub fn should_use_hlod(&self, camera_pos: Vec3, screen_height: f32, fov_rad: f32, threshold: f32) -> bool {
2987 self.compute_screen_size(camera_pos, screen_height, fov_rad) < threshold
2988 }
2989
2990 pub fn overlap_with(&self, other: &HlodCluster) -> bool {
2991 self.merged_bounds.intersects(&other.merged_bounds)
2992 }
2993
2994 pub fn merge_cluster(&mut self, other: &HlodCluster) {
2995 self.actor_ids.extend_from_slice(&other.actor_ids);
2996 self.merged_bounds = self.merged_bounds.merge(&other.merged_bounds);
2997 self.center = self.merged_bounds.center();
2998 let half = self.merged_bounds.half_size();
2999 self.radius = half.length();
3000 self.simplified_vertex_count += other.simplified_vertex_count;
3001 self.simplified_triangle_count += other.simplified_triangle_count;
3002 self.memory_bytes += other.memory_bytes;
3003 }
3004}
3005
3006#[derive(Debug, Clone)]
3011pub struct HlodBuilder {
3012 pub cluster_radius: f32,
3013 pub max_actors_per_cluster: usize,
3014 pub max_depth: u32,
3015 pub simplification_ratio: f32,
3016 pub next_cluster_id: u32,
3017}
3018
3019impl HlodBuilder {
3020 pub fn new(cluster_radius: f32, max_actors: usize, max_depth: u32) -> Self {
3021 Self {
3022 cluster_radius,
3023 max_actors_per_cluster: max_actors,
3024 max_depth,
3025 simplification_ratio: 0.1,
3026 next_cluster_id: 1,
3027 }
3028 }
3029
3030 pub fn build_clusters(&mut self, actors: &[(u32, Vec3, ChunkBounds)]) -> Vec<HlodCluster> {
3031 if actors.is_empty() { return Vec::new(); }
3032 self.build_level(actors, 0)
3033 }
3034
3035 fn build_level(&mut self, actors: &[(u32, Vec3, ChunkBounds)], depth: u32) -> Vec<HlodCluster> {
3036 let mut clusters: Vec<HlodCluster> = Vec::new();
3037 let mut assigned: Vec<bool> = vec![false; actors.len()];
3038
3039 for i in 0..actors.len() {
3040 if assigned[i] { continue; }
3041 let mut cluster = HlodCluster::new(self.next_cluster_id, depth);
3042 self.next_cluster_id += 1;
3043 let seed_pos = actors[i].1;
3044 cluster.add_actor(actors[i].0, &actors[i].2);
3045 assigned[i] = true;
3046
3047 for j in (i + 1)..actors.len() {
3048 if assigned[j] { continue; }
3049 if cluster.actor_ids.len() >= self.max_actors_per_cluster { break; }
3050 let dist = (actors[j].1 - seed_pos).length();
3051 if dist <= self.cluster_radius {
3052 cluster.add_actor(actors[j].0, &actors[j].2);
3053 assigned[j] = true;
3054 }
3055 }
3056
3057 cluster.compute_simplified_geometry();
3058 clusters.push(cluster);
3059 }
3060
3061 clusters
3062 }
3063
3064 pub fn build_hierarchical(&mut self, actors: &[(u32, Vec3, ChunkBounds)]) -> Vec<Vec<HlodCluster>> {
3065 let mut hierarchy: Vec<Vec<HlodCluster>> = Vec::new();
3066 let leaf_clusters = self.build_clusters(actors);
3067 hierarchy.push(leaf_clusters);
3068
3069 let mut depth = 1u32;
3070 while depth <= self.max_depth {
3071 let prev_clusters = hierarchy.last().unwrap();
3072 if prev_clusters.len() <= 1 { break; }
3073
3074 let cluster_actors: Vec<(u32, Vec3, ChunkBounds)> = prev_clusters.iter()
3075 .map(|c| (c.cluster_id, c.center, c.merged_bounds.clone()))
3076 .collect();
3077
3078 let new_radius = self.cluster_radius * (1 << depth) as f32;
3079 let mut builder = HlodBuilder::new(new_radius, self.max_actors_per_cluster * 4, self.max_depth);
3080 builder.next_cluster_id = self.next_cluster_id;
3081 let next_level = builder.build_clusters(&cluster_actors);
3082 self.next_cluster_id = builder.next_cluster_id;
3083 hierarchy.push(next_level);
3084 depth += 1;
3085 }
3086 hierarchy
3087 }
3088
3089 pub fn spatially_sort_actors(&self, actors: &mut [(u32, Vec3, ChunkBounds)]) {
3090 actors.sort_by(|a, b| {
3092 let za = morton_encode_2d(a.1.x as u32, a.1.z as u32);
3093 let zb = morton_encode_2d(b.1.x as u32, b.1.z as u32);
3094 za.cmp(&zb)
3095 });
3096 }
3097
3098 pub fn compute_cluster_bounds(cluster: &HlodCluster) -> ChunkBounds {
3099 cluster.merged_bounds.clone()
3100 }
3101
3102 pub fn merge_small_clusters(&self, clusters: &mut Vec<HlodCluster>, min_actors: usize) {
3103 let small_ids: Vec<usize> = clusters.iter().enumerate()
3104 .filter(|(_, c)| c.actor_ids.len() < min_actors)
3105 .map(|(i, _)| i)
3106 .collect();
3107
3108 for i in small_ids.iter().rev() {
3109 if *i >= clusters.len() { continue; }
3110 let small = clusters.remove(*i);
3111 let mut best = 0;
3113 let mut best_dist = f32::MAX;
3114 for (j, c) in clusters.iter().enumerate() {
3115 let dist = (c.center - small.center).length();
3116 if dist < best_dist { best_dist = dist; best = j; }
3117 }
3118 if !clusters.is_empty() {
3119 clusters[best].merge_cluster(&small);
3120 } else {
3121 clusters.push(small);
3122 }
3123 }
3124 }
3125}
3126
3127fn morton_encode_2d(x: u32, y: u32) -> u64 {
3129 let mut result = 0u64;
3130 let x = x as u64;
3131 let y = y as u64;
3132 for i in 0..32u64 {
3133 result |= ((x >> i) & 1) << (2 * i);
3134 result |= ((y >> i) & 1) << (2 * i + 1);
3135 }
3136 result
3137}
3138
3139#[derive(Debug, Clone)]
3144pub struct StreamingDistanceCalculator {
3145 pub screen_height: f32,
3146 pub fov_vertical_rad: f32,
3147 pub min_screen_size_fraction: f32,
3148 pub max_streaming_distance: f32,
3149 pub lod_bias: f32,
3150}
3151
3152impl StreamingDistanceCalculator {
3153 pub fn new(screen_height: f32, fov_rad: f32) -> Self {
3154 Self {
3155 screen_height,
3156 fov_vertical_rad: fov_rad,
3157 min_screen_size_fraction: 0.01,
3158 max_streaming_distance: 10000.0,
3159 lod_bias: 0.0,
3160 }
3161 }
3162
3163 pub fn compute_streaming_distance(&self, bounding_radius: f32, min_screen_fraction: f32) -> f32 {
3164 let target_screen = self.screen_height * min_screen_fraction;
3165 let half_fov_tan = (self.fov_vertical_rad * 0.5).tan();
3166 if target_screen < 1e-8 || half_fov_tan < 1e-8 {
3167 return self.max_streaming_distance;
3168 }
3169 let dist = bounding_radius * self.screen_height / (target_screen * half_fov_tan);
3170 (dist * (1.0 + self.lod_bias)).min(self.max_streaming_distance)
3171 }
3172
3173 pub fn compute_lod_transition_distances(&self, bounding_radius: f32) -> [f32; 5] {
3174 let fractions = [0.8, 0.4, 0.2, 0.1, 0.05];
3175 let mut dists = [0.0f32; 5];
3176 for (i, &frac) in fractions.iter().enumerate() {
3177 dists[i] = self.compute_streaming_distance(bounding_radius, frac);
3178 }
3179 dists
3180 }
3181
3182 pub fn screen_size_at_distance(&self, bounding_radius: f32, distance: f32) -> f32 {
3183 if distance < 1e-6 { return self.screen_height; }
3184 let half_fov_tan = (self.fov_vertical_rad * 0.5).tan();
3185 let angular_size = bounding_radius / distance;
3186 (angular_size / half_fov_tan) * self.screen_height
3187 }
3188
3189 pub fn desired_lod_at_distance(&self, distance: f32, bounding_radius: f32) -> LodLevel {
3190 let screen_size = self.screen_size_at_distance(bounding_radius, distance);
3191 let fraction = screen_size / self.screen_height;
3192 if fraction > 0.5 { LodLevel::Ultra }
3193 else if fraction > 0.2 { LodLevel::High }
3194 else if fraction > 0.08 { LodLevel::Medium }
3195 else if fraction > 0.03 { LodLevel::Low }
3196 else if fraction > 0.01 { LodLevel::Impostor}
3197 else { LodLevel::Unloaded}
3198 }
3199
3200 pub fn compute_cull_distance(&self, bounding_radius: f32) -> f32 {
3201 self.compute_streaming_distance(bounding_radius, self.min_screen_size_fraction)
3202 }
3203
3204 pub fn importance_adjusted_distance(&self, distance: f32, importance: f32) -> f32 {
3205 distance / importance.max(0.01)
3206 }
3207
3208 pub fn compute_lod_blend_alpha(&self, distance: f32, near_dist: f32, far_dist: f32) -> f32 {
3209 if distance <= near_dist { return 0.0; }
3210 if distance >= far_dist { return 1.0; }
3211 let range = far_dist - near_dist;
3212 if range < 1e-6 { return 1.0; }
3213 (distance - near_dist) / range
3214 }
3215}
3216
3217#[derive(Debug, Clone)]
3222pub struct ChunkDependencyEdge {
3223 pub from: ChunkCoord,
3224 pub to: ChunkCoord,
3225 pub is_hard: bool,
3226 pub weight: f32,
3227}
3228
3229#[derive(Debug, Clone)]
3230pub struct ChunkDependency {
3231 pub dependency_graph: HashMap<ChunkCoord, Vec<ChunkCoord>>,
3232 pub reverse_graph: HashMap<ChunkCoord, Vec<ChunkCoord>>,
3233 pub edges: Vec<ChunkDependencyEdge>,
3234 pub topological_order: Vec<ChunkCoord>,
3235 pub is_dirty: bool,
3236}
3237
3238impl ChunkDependency {
3239 pub fn new() -> Self {
3240 Self {
3241 dependency_graph: HashMap::new(),
3242 reverse_graph: HashMap::new(),
3243 edges: Vec::new(),
3244 topological_order: Vec::new(),
3245 is_dirty: true,
3246 }
3247 }
3248
3249 pub fn add_dependency(&mut self, from: ChunkCoord, to: ChunkCoord, is_hard: bool) {
3250 self.dependency_graph.entry(from.clone()).or_insert_with(Vec::new).push(to.clone());
3251 self.reverse_graph.entry(to.clone()).or_insert_with(Vec::new).push(from.clone());
3252 self.edges.push(ChunkDependencyEdge {
3253 from,
3254 to,
3255 is_hard,
3256 weight: if is_hard { 1.0 } else { 0.5 },
3257 });
3258 self.is_dirty = true;
3259 }
3260
3261 pub fn remove_dependency(&mut self, from: &ChunkCoord, to: &ChunkCoord) {
3262 if let Some(deps) = self.dependency_graph.get_mut(from) {
3263 deps.retain(|c| c != to);
3264 }
3265 if let Some(revs) = self.reverse_graph.get_mut(to) {
3266 revs.retain(|c| c != from);
3267 }
3268 self.edges.retain(|e| !(&e.from == from && &e.to == to));
3269 self.is_dirty = true;
3270 }
3271
3272 pub fn dependencies_of(&self, coord: &ChunkCoord) -> Vec<&ChunkCoord> {
3273 self.dependency_graph.get(coord).map(|v| v.iter().collect()).unwrap_or_default()
3274 }
3275
3276 pub fn dependents_of(&self, coord: &ChunkCoord) -> Vec<&ChunkCoord> {
3277 self.reverse_graph.get(coord).map(|v| v.iter().collect()).unwrap_or_default()
3278 }
3279
3280 pub fn topological_sort(&mut self) -> bool {
3281 if !self.is_dirty { return true; }
3282 let mut in_degree: HashMap<ChunkCoord, usize> = HashMap::new();
3284 let all_nodes: HashSet<ChunkCoord> = self.dependency_graph.keys()
3285 .chain(self.reverse_graph.keys())
3286 .cloned()
3287 .collect();
3288
3289 for node in &all_nodes {
3290 in_degree.entry(node.clone()).or_insert(0);
3291 }
3292 for edge in &self.edges {
3293 *in_degree.entry(edge.from.clone()).or_insert(0) += 1;
3294 }
3295
3296 let mut queue: VecDeque<ChunkCoord> = in_degree.iter()
3297 .filter(|(_, °)| deg == 0)
3298 .map(|(c, _)| c.clone())
3299 .collect();
3300
3301 let mut order = Vec::new();
3302 while let Some(node) = queue.pop_front() {
3303 order.push(node.clone());
3304 if let Some(dependents) = self.reverse_graph.get(&node) {
3305 for dep in dependents.clone() {
3306 let deg = in_degree.entry(dep.clone()).or_insert(0);
3307 if *deg > 0 { *deg -= 1; }
3308 if *deg == 0 { queue.push_back(dep); }
3309 }
3310 }
3311 }
3312
3313 if order.len() == all_nodes.len() {
3314 self.topological_order = order;
3315 self.is_dirty = false;
3316 true
3317 } else {
3318 false }
3320 }
3321
3322 pub fn transitive_dependencies(&self, coord: &ChunkCoord, max_depth: usize) -> HashSet<ChunkCoord> {
3323 let mut visited = HashSet::new();
3324 let mut stack = vec![(coord.clone(), 0)];
3325 while let Some((current, depth)) = stack.pop() {
3326 if depth >= max_depth || !visited.insert(current.clone()) { continue; }
3327 if let Some(deps) = self.dependency_graph.get(¤t) {
3328 for dep in deps {
3329 stack.push((dep.clone(), depth + 1));
3330 }
3331 }
3332 }
3333 visited.remove(coord);
3334 visited
3335 }
3336
3337 pub fn has_cycle(&self) -> bool {
3338 let all_nodes: HashSet<ChunkCoord> = self.dependency_graph.keys()
3339 .chain(self.reverse_graph.keys())
3340 .cloned()
3341 .collect();
3342 let mut color: HashMap<ChunkCoord, u8> = HashMap::new(); for node in &all_nodes {
3344 if *color.get(node).unwrap_or(&0) == 0 {
3345 if self.dfs_has_cycle(node, &mut color) { return true; }
3346 }
3347 }
3348 false
3349 }
3350
3351 fn dfs_has_cycle(&self, node: &ChunkCoord, color: &mut HashMap<ChunkCoord, u8>) -> bool {
3352 color.insert(node.clone(), 1);
3353 if let Some(neighbors) = self.dependency_graph.get(node) {
3354 for neighbor in neighbors {
3355 let c = *color.get(neighbor).unwrap_or(&0);
3356 if c == 1 { return true; }
3357 if c == 0 && self.dfs_has_cycle(neighbor, color) { return true; }
3358 }
3359 }
3360 color.insert(node.clone(), 2);
3361 false
3362 }
3363
3364 pub fn can_load(&self, coord: &ChunkCoord, loaded: &HashSet<ChunkCoord>) -> bool {
3365 if let Some(deps) = self.dependency_graph.get(coord) {
3366 deps.iter().all(|dep| loaded.contains(dep))
3367 } else {
3368 true
3369 }
3370 }
3371}
3372
3373#[derive(Debug, Clone)]
3378pub struct AsyncLoadRequest {
3379 pub request_id: u64,
3380 pub coord: ChunkCoord,
3381 pub target_lod: LodLevel,
3382 pub priority: f32,
3383 pub frame_queued: u64,
3384 pub load_type: StreamingLoadType,
3385 pub is_cancelled: bool,
3386 pub retry_count: u32,
3387}
3388
3389impl AsyncLoadRequest {
3390 pub fn new(id: u64, coord: ChunkCoord, lod: LodLevel, priority: f32, frame: u64) -> Self {
3391 Self {
3392 request_id: id,
3393 coord,
3394 target_lod: lod,
3395 priority,
3396 frame_queued: frame,
3397 load_type: StreamingLoadType::Asynchronous,
3398 is_cancelled: false,
3399 retry_count: 0,
3400 }
3401 }
3402}
3403
3404#[derive(Debug, Clone)]
3405pub struct AsyncLoadQueue {
3406 pub pending: BTreeMap<u64, AsyncLoadRequest>, pub in_flight: HashMap<u64, AsyncLoadRequest>,
3408 pub completed: VecDeque<(u64, bool)>, pub cancelled: HashSet<u64>,
3410 pub next_id: u64,
3411 pub max_in_flight: usize,
3412 pub max_pending: usize,
3413}
3414
3415impl AsyncLoadQueue {
3416 pub fn new(max_in_flight: usize, max_pending: usize) -> Self {
3417 Self {
3418 pending: BTreeMap::new(),
3419 in_flight: HashMap::new(),
3420 completed: VecDeque::new(),
3421 cancelled: HashSet::new(),
3422 next_id: 1,
3423 max_in_flight,
3424 max_pending,
3425 }
3426 }
3427
3428 pub fn enqueue(&mut self, coord: ChunkCoord, lod: LodLevel, priority: f32, frame: u64) -> u64 {
3429 let id = self.next_id;
3430 self.next_id += 1;
3431 let req = AsyncLoadRequest::new(id, coord, lod, priority, frame);
3432 let key = ((-priority * 1_000_000.0) as i64 as u64).wrapping_add(id);
3434 self.pending.insert(key, req);
3435 if self.pending.len() > self.max_pending {
3436 self.pending.pop_last();
3437 }
3438 id
3439 }
3440
3441 pub fn cancel(&mut self, request_id: u64) {
3442 self.cancelled.insert(request_id);
3443 if let Some(req) = self.in_flight.get_mut(&request_id) {
3444 req.is_cancelled = true;
3445 }
3446 self.pending.retain(|_, req| req.request_id != request_id);
3447 }
3448
3449 pub fn cancel_coord(&mut self, coord: &ChunkCoord) {
3450 let ids_to_cancel: Vec<u64> = self.in_flight.values()
3451 .filter(|r| &r.coord == coord)
3452 .map(|r| r.request_id)
3453 .chain(
3454 self.pending.values()
3455 .filter(|r| &r.coord == coord)
3456 .map(|r| r.request_id)
3457 )
3458 .collect();
3459 for id in ids_to_cancel { self.cancel(id); }
3460 }
3461
3462 pub fn dispatch_available(&mut self, frame: u64) -> Vec<AsyncLoadRequest> {
3463 let to_dispatch = self.max_in_flight - self.in_flight.len().min(self.max_in_flight);
3464 let mut dispatched = Vec::new();
3465 let mut keys_to_remove = Vec::new();
3466
3467 for (&key, req) in &self.pending {
3468 if dispatched.len() >= to_dispatch { break; }
3469 if self.cancelled.contains(&req.request_id) {
3470 keys_to_remove.push(key);
3471 continue;
3472 }
3473 keys_to_remove.push(key);
3474 dispatched.push(req.clone());
3475 }
3476
3477 for key in keys_to_remove {
3478 if let Some(req) = self.pending.remove(&key) {
3479 if !self.cancelled.contains(&req.request_id) {
3480 self.in_flight.insert(req.request_id, req);
3481 }
3482 }
3483 }
3484
3485 dispatched
3486 }
3487
3488 pub fn complete_request(&mut self, request_id: u64, success: bool) {
3489 self.in_flight.remove(&request_id);
3490 self.cancelled.remove(&request_id);
3491 self.completed.push_back((request_id, success));
3492 while self.completed.len() > 256 {
3493 self.completed.pop_front();
3494 }
3495 }
3496
3497 pub fn drain_completed(&mut self) -> Vec<(u64, bool)> {
3498 self.completed.drain(..).collect()
3499 }
3500
3501 pub fn is_pending(&self, coord: &ChunkCoord) -> bool {
3502 self.pending.values().any(|r| &r.coord == coord && !self.cancelled.contains(&r.request_id))
3503 }
3504
3505 pub fn is_in_flight(&self, coord: &ChunkCoord) -> bool {
3506 self.in_flight.values().any(|r| &r.coord == coord && !r.is_cancelled)
3507 }
3508
3509 pub fn pending_count(&self) -> usize {
3510 self.pending.len()
3511 }
3512
3513 pub fn in_flight_count(&self) -> usize {
3514 self.in_flight.len()
3515 }
3516
3517 pub fn update_priority(&mut self, coord: &ChunkCoord, new_priority: f32) {
3518 let to_update: Vec<(u64, u64)> = self.pending.iter()
3519 .filter(|(_, r)| &r.coord == coord)
3520 .map(|(&k, r)| (k, r.request_id))
3521 .collect();
3522 for (old_key, req_id) in to_update {
3523 if let Some(mut req) = self.pending.remove(&old_key) {
3524 req.priority = new_priority;
3525 let new_key = ((-new_priority * 1_000_000.0) as i64 as u64).wrapping_add(req_id);
3526 self.pending.insert(new_key, req);
3527 }
3528 }
3529 }
3530
3531 pub fn stale_requests(&self, current_frame: u64, max_age_frames: u64) -> Vec<u64> {
3532 self.pending.values()
3533 .filter(|r| current_frame.saturating_sub(r.frame_queued) > max_age_frames)
3534 .map(|r| r.request_id)
3535 .collect()
3536 }
3537}
3538
3539#[derive(Debug, Clone)]
3544pub struct ProfilerEvent {
3545 pub event_type: ProfilerEventType,
3546 pub start_time_us: u64,
3547 pub duration_us: u64,
3548 pub frame: u64,
3549 pub metadata: u32,
3550}
3551
3552#[derive(Debug, Clone)]
3553pub struct FrameProfileData {
3554 pub frame: u64,
3555 pub events: Vec<ProfilerEvent>,
3556 pub total_load_us: u64,
3557 pub total_unload_us: u64,
3558 pub total_cull_us: u64,
3559 pub total_lod_us: u64,
3560 pub chunks_loaded: u32,
3561 pub chunks_unloaded: u32,
3562}
3563
3564impl FrameProfileData {
3565 pub fn new(frame: u64) -> Self {
3566 Self {
3567 frame,
3568 events: Vec::new(),
3569 total_load_us: 0,
3570 total_unload_us: 0,
3571 total_cull_us: 0,
3572 total_lod_us: 0,
3573 chunks_loaded: 0,
3574 chunks_unloaded: 0,
3575 }
3576 }
3577}
3578
3579#[derive(Debug, Clone)]
3580pub struct StreamingProfiler {
3581 pub history: VecDeque<FrameProfileData>,
3582 pub current_frame_data: FrameProfileData,
3583 pub current_frame: u64,
3584 pub active_timers: HashMap<String, u64>,
3585 pub max_history: usize,
3586 pub total_events_recorded: u64,
3587 pub peak_load_time_us: u64,
3588 pub peak_frame_time_us: u64,
3589}
3590
3591impl StreamingProfiler {
3592 pub fn new(max_history: usize) -> Self {
3593 Self {
3594 history: VecDeque::with_capacity(max_history),
3595 current_frame_data: FrameProfileData::new(0),
3596 current_frame: 0,
3597 active_timers: HashMap::new(),
3598 max_history,
3599 total_events_recorded: 0,
3600 peak_load_time_us: 0,
3601 peak_frame_time_us: 0,
3602 }
3603 }
3604
3605 pub fn begin_frame(&mut self, frame: u64) {
3606 self.current_frame = frame;
3607 self.current_frame_data = FrameProfileData::new(frame);
3608 }
3609
3610 pub fn end_frame(&mut self) {
3611 let data = self.current_frame_data.clone();
3612 let frame_total = data.total_load_us + data.total_unload_us
3613 + data.total_cull_us + data.total_lod_us;
3614 if frame_total > self.peak_frame_time_us {
3615 self.peak_frame_time_us = frame_total;
3616 }
3617 if data.total_load_us > self.peak_load_time_us {
3618 self.peak_load_time_us = data.total_load_us;
3619 }
3620 self.history.push_back(data);
3621 if self.history.len() > self.max_history {
3622 self.history.pop_front();
3623 }
3624 }
3625
3626 pub fn record_event(&mut self, event_type: ProfilerEventType, start_us: u64, duration_us: u64, meta: u32) {
3627 let event = ProfilerEvent {
3628 event_type: event_type.clone(),
3629 start_time_us: start_us,
3630 duration_us,
3631 frame: self.current_frame,
3632 metadata: meta,
3633 };
3634 match event_type {
3635 ProfilerEventType::ChunkLoad => {
3636 self.current_frame_data.total_load_us += duration_us;
3637 self.current_frame_data.chunks_loaded += 1;
3638 }
3639 ProfilerEventType::ChunkUnload => {
3640 self.current_frame_data.total_unload_us += duration_us;
3641 self.current_frame_data.chunks_unloaded += 1;
3642 }
3643 ProfilerEventType::FrustumCull => {
3644 self.current_frame_data.total_cull_us += duration_us;
3645 }
3646 ProfilerEventType::LodSwitch => {
3647 self.current_frame_data.total_lod_us += duration_us;
3648 }
3649 _ => {}
3650 }
3651 self.current_frame_data.events.push(event);
3652 self.total_events_recorded += 1;
3653 }
3654
3655 pub fn average_load_time_us(&self) -> f64 {
3656 if self.history.is_empty() { return 0.0; }
3657 let total: u64 = self.history.iter().map(|f| f.total_load_us).sum();
3658 total as f64 / self.history.len() as f64
3659 }
3660
3661 pub fn average_chunks_per_frame(&self) -> f64 {
3662 if self.history.is_empty() { return 0.0; }
3663 let total: u32 = self.history.iter().map(|f| f.chunks_loaded).sum();
3664 total as f64 / self.history.len() as f64
3665 }
3666
3667 pub fn compute_percentile_load_time(&self, pct: f64) -> u64 {
3668 let mut times: Vec<u64> = self.history.iter().map(|f| f.total_load_us).collect();
3669 times.sort_unstable();
3670 if times.is_empty() { return 0; }
3671 let idx = ((pct / 100.0) * (times.len() - 1) as f64) as usize;
3672 times[idx.min(times.len() - 1)]
3673 }
3674
3675 pub fn frame_time_ms(&self, frame_idx: usize) -> f64 {
3676 if let Some(data) = self.history.get(frame_idx) {
3677 (data.total_load_us + data.total_unload_us + data.total_cull_us + data.total_lod_us) as f64 / 1000.0
3678 } else {
3679 0.0
3680 }
3681 }
3682
3683 pub fn event_count_by_type(&self, event_type: &ProfilerEventType) -> usize {
3684 self.history.iter()
3685 .flat_map(|f| f.events.iter())
3686 .filter(|e| std::mem::discriminant(&e.event_type) == std::mem::discriminant(event_type))
3687 .count()
3688 }
3689
3690 pub fn history_len(&self) -> usize {
3691 self.history.len()
3692 }
3693
3694 pub fn report_summary(&self) -> ProfilerSummary {
3695 ProfilerSummary {
3696 frames_recorded: self.history.len(),
3697 avg_load_time_us: self.average_load_time_us(),
3698 avg_chunks_per_frame: self.average_chunks_per_frame(),
3699 peak_load_time_us: self.peak_load_time_us,
3700 peak_frame_time_us: self.peak_frame_time_us,
3701 p99_load_time_us: self.compute_percentile_load_time(99.0),
3702 total_events: self.total_events_recorded,
3703 }
3704 }
3705}
3706
3707#[derive(Debug, Clone)]
3708pub struct ProfilerSummary {
3709 pub frames_recorded: usize,
3710 pub avg_load_time_us: f64,
3711 pub avg_chunks_per_frame: f64,
3712 pub peak_load_time_us: u64,
3713 pub peak_frame_time_us: u64,
3714 pub p99_load_time_us: u64,
3715 pub total_events: u64,
3716}
3717
3718#[derive(Debug, Clone)]
3723pub struct WorldStreamingEditor {
3724 pub config: StreamingConfig,
3725 pub chunks: HashMap<ChunkCoord, StreamingChunk>,
3726 pub viewer_position: Vec3,
3727 pub viewer_forward: Vec3,
3728 pub camera: Option<StreamingCamera>,
3729 pub frustum: Option<FrustumCulling>,
3730 pub load_queue: AsyncLoadQueue,
3731 pub unload_queue: AsyncLoadQueue,
3732 pub memory_budget: MemoryBudget,
3733 pub streaming_stats: StreamingStats,
3734 pub profiler: StreamingProfiler,
3735 pub octree: Option<OctreeNode>,
3736 pub bvh: Option<BvhNode>,
3737 pub world_partition: WorldPartition,
3738 pub data_layers: DataLayerSystem,
3739 pub hlod_hierarchy: Vec<Vec<HlodCluster>>,
3740 pub virtual_texture: Option<VirtualTexture>,
3741 pub terrain_patches: HashMap<ChunkCoord, TerrainPatch>,
3742 pub actor_proxies: HashMap<u32, ActorStreamingProxy>,
3743 pub impostors: HashMap<u32, ImpostorBillboard>,
3744 pub chunk_dependencies: ChunkDependency,
3745 pub loaded_chunk_set: HashSet<ChunkCoord>,
3746 pub frame_number: u64,
3747 pub current_time_us: u64,
3748 pub pending_lod_transitions: Vec<(ChunkCoord, LodLevel, LodLevel)>,
3749 pub mesh_lods: HashMap<u32, ChunkMeshLod>,
3750 pub streaming_distance_calculator: StreamingDistanceCalculator,
3751 pub hlod_builder: HlodBuilder,
3752}
3753
3754impl WorldStreamingEditor {
3755 pub fn new(config: StreamingConfig) -> Self {
3756 let memory_budget = MemoryBudget::new(config.max_memory_mb, config.eviction_policy.clone());
3757 let max_in_flight = config.max_concurrent_loads;
3758 let max_pending = MAX_ASYNC_LOAD_QUEUE;
3759 let screen_h = config.screen_height_pixels as f32;
3760 let fov_rad = config.fov_vertical_rad;
3761 let cell_size = WORLD_PARTITION_CELL_SIZE;
3762 let vt = if config.enable_virtual_textures {
3763 Some(VirtualTexture::new(VIRTUAL_TEXTURE_ATLAS_SIZE, VIRTUAL_TEXTURE_TILE_SIZE, FEEDBACK_BUFFER_MIPS as u32))
3764 } else {
3765 None
3766 };
3767 Self {
3768 config: config.clone(),
3769 chunks: HashMap::new(),
3770 viewer_position: Vec3::ZERO,
3771 viewer_forward: Vec3::NEG_Z,
3772 camera: None,
3773 frustum: None,
3774 load_queue: AsyncLoadQueue::new(max_in_flight, max_pending),
3775 unload_queue: AsyncLoadQueue::new(config.max_concurrent_unloads, max_pending / 4),
3776 memory_budget,
3777 streaming_stats: StreamingStats::new(),
3778 profiler: StreamingProfiler::new(PROFILER_HISTORY_FRAMES),
3779 octree: None,
3780 bvh: None,
3781 world_partition: WorldPartition::new(cell_size),
3782 data_layers: DataLayerSystem::new(),
3783 hlod_hierarchy: Vec::new(),
3784 virtual_texture: vt,
3785 terrain_patches: HashMap::new(),
3786 actor_proxies: HashMap::new(),
3787 impostors: HashMap::new(),
3788 chunk_dependencies: ChunkDependency::new(),
3789 loaded_chunk_set: HashSet::new(),
3790 frame_number: 0,
3791 current_time_us: 0,
3792 pending_lod_transitions: Vec::new(),
3793 mesh_lods: HashMap::new(),
3794 streaming_distance_calculator: StreamingDistanceCalculator::new(screen_h, fov_rad),
3795 hlod_builder: HlodBuilder::new(HLOD_CLUSTER_RADIUS, 32, 3),
3796 }
3797 }
3798
3799 pub fn set_viewer(&mut self, position: Vec3, forward: Vec3) {
3800 self.viewer_position = position;
3801 self.viewer_forward = forward.normalize_or_zero();
3802 self.world_partition.clear_streaming_sources();
3803 self.world_partition.add_streaming_source(position);
3804 }
3805
3806 pub fn set_camera(&mut self, camera: StreamingCamera) {
3807 let planes = camera.frustum_planes;
3808 self.camera = Some(camera);
3809 self.frustum = Some(FrustumCulling::new(planes));
3810 }
3811
3812 pub fn tick(&mut self, delta_time_s: f32, time_us: u64) {
3813 self.current_time_us = time_us;
3814 self.profiler.begin_frame(self.frame_number);
3815 self.streaming_stats.advance_frame();
3816
3817 self.update_chunk_visibility();
3818 self.update_chunk_lods();
3819 self.process_load_queue();
3820 self.process_unload_queue();
3821 self.evict_memory_if_needed();
3822 self.update_terrain_stitching();
3823 self.update_impostors();
3824 self.update_virtual_textures();
3825
3826 self.profiler.end_frame();
3827 self.frame_number += 1;
3828 }
3829
3830 fn update_chunk_visibility(&mut self) {
3831 let viewer = self.viewer_position;
3832 let frame = self.frame_number;
3833 let streaming_radius = self.config.streaming_radius;
3834 let vertical_radius = self.config.vertical_streaming_radius;
3835
3836 let viewer_coord = ChunkCoord::from_world_pos(viewer, self.config.chunk_size);
3837 let radius_chunks = self.config.streaming_radius_chunks();
3838
3839 let candidates = ChunkCoord::chunks_in_radius(&viewer_coord, radius_chunks);
3840
3841 for coord in &candidates {
3843 self.chunks.entry(coord.clone()).or_insert_with(|| {
3844 StreamingChunk::new(coord.clone(), self.config.chunk_size)
3845 });
3846 }
3847
3848 let frustum = self.frustum.clone();
3849 let cull_enabled = self.config.enable_frustum_culling;
3850
3851 for (coord, chunk) in self.chunks.iter_mut() {
3852 chunk.update_distance(viewer);
3853
3854 let vert_dist = (chunk.bounds.center().y - viewer.y).abs();
3855 if vert_dist > vertical_radius {
3856 chunk.mark_not_visible();
3857 continue;
3858 }
3859 if chunk.distance_to_viewer > streaming_radius {
3860 chunk.mark_not_visible();
3861 continue;
3862 }
3863 if cull_enabled {
3864 if let Some(ref frust) = frustum {
3865 if !frust.test_aabb_fast(&chunk.bounds) {
3866 chunk.mark_not_visible();
3867 continue;
3868 }
3869 }
3870 }
3871 chunk.mark_visible(frame);
3872 chunk.compute_load_priority(viewer, self.viewer_forward);
3873 }
3874 }
3875
3876 fn update_chunk_lods(&mut self) {
3877 let viewer = self.viewer_position;
3878 let screen_h = self.config.screen_height_pixels as f32;
3879
3880 let camera_opt = self.camera.clone();
3881 let config = self.config.clone();
3882 let frame = self.frame_number;
3883
3884 let mut transitions = Vec::new();
3885
3886 for (coord, chunk) in self.chunks.iter_mut() {
3887 if !chunk.is_visible || chunk.load_state == ChunkLoadState::Unloaded {
3888 continue;
3889 }
3890 let desired_lod = if let Some(ref cam) = camera_opt {
3891 let screen_size = cam.compute_lod_screen_size(&chunk.bounds, screen_h);
3892 ChunkMeshLod::new(10000, config.chunk_size).select_lod_for_screen_size(screen_size)
3893 } else {
3894 config.lod_for_distance(chunk.distance_to_viewer)
3895 };
3896
3897 if desired_lod != chunk.lod_level && chunk.last_loaded_frame + config.min_lod_retain_frames <= frame {
3898 transitions.push((coord.clone(), chunk.lod_level.clone(), desired_lod));
3899 }
3900 }
3901 self.pending_lod_transitions = transitions;
3902 for (coord, old_lod, new_lod) in &self.pending_lod_transitions {
3903 if let Some(chunk) = self.chunks.get_mut(coord) {
3904 chunk.lod_level = new_lod.clone();
3905 chunk.flags.needs_lod_update = true;
3906 }
3907 self.streaming_stats.chunks_lod_switched_this_frame += 1;
3908 }
3909 }
3910
3911 fn process_load_queue(&mut self) {
3912 let frame = self.frame_number;
3913 let viewer = self.viewer_position;
3914
3915 let chunk_size = self.config.chunk_size;
3917 self.load_queue.update_priority(&ChunkCoord::new(0,0,0), 0.0); let mut to_enqueue: Vec<(ChunkCoord, LodLevel, f32)> = Vec::new();
3921 for (coord, chunk) in &self.chunks {
3922 if !chunk.is_visible { continue; }
3923 if chunk.load_state != ChunkLoadState::Unloaded { continue; }
3924 if self.load_queue.is_pending(coord) || self.load_queue.is_in_flight(coord) { continue; }
3925 let desired_lod = self.config.lod_for_distance(chunk.distance_to_viewer);
3926 if desired_lod == LodLevel::Unloaded { continue; }
3927 if !self.chunk_dependencies.can_load(coord, &self.loaded_chunk_set) { continue; }
3928 to_enqueue.push((coord.clone(), desired_lod, chunk.load_priority));
3929 }
3930
3931 for (coord, lod, priority) in to_enqueue {
3932 let id = self.load_queue.enqueue(coord.clone(), lod, priority, frame);
3933 if let Some(chunk) = self.chunks.get_mut(&coord) {
3934 chunk.load_state = ChunkLoadState::Queued;
3935 }
3936 }
3937
3938 let dispatched = self.load_queue.dispatch_available(frame);
3940 for req in dispatched {
3941 if let Some(chunk) = self.chunks.get_mut(&req.coord) {
3942 chunk.load_state = ChunkLoadState::Loading;
3943 }
3944 let est_bytes = self.estimate_chunk_memory(&req.coord, &req.target_lod);
3946 let can_alloc = self.memory_budget.can_allocate(est_bytes);
3947 let success = can_alloc;
3948 if success {
3949 self.memory_budget.allocate(req.coord.clone(), req.target_lod.clone(), est_bytes, frame);
3950 if let Some(chunk) = self.chunks.get_mut(&req.coord) {
3951 chunk.load_state = ChunkLoadState::Loaded;
3952 chunk.lod_level = req.target_lod.clone();
3953 chunk.memory_bytes = est_bytes;
3954 chunk.last_loaded_frame = frame;
3955 }
3956 self.loaded_chunk_set.insert(req.coord.clone());
3957 self.streaming_stats.chunks_loaded_this_frame += 1;
3958 }
3959 self.load_queue.complete_request(req.request_id, success);
3960 }
3961 }
3962
3963 fn process_unload_queue(&mut self) {
3964 let viewer = self.viewer_position;
3965 let streaming_radius = self.config.streaming_radius;
3966 let frame = self.frame_number;
3967
3968 let mut to_evict: Vec<ChunkCoord> = Vec::new();
3970 for (coord, chunk) in &self.chunks {
3971 if !chunk.can_evict() { continue; }
3972 if chunk.distance_to_viewer > streaming_radius * 1.1 {
3973 to_evict.push(coord.clone());
3974 }
3975 }
3976
3977 for coord in &to_evict {
3978 if !self.unload_queue.is_pending(coord) && !self.unload_queue.is_in_flight(coord) {
3979 let priority = if let Some(chunk) = self.chunks.get(coord) { chunk.distance_to_viewer } else { 0.0 };
3980 self.unload_queue.enqueue(coord.clone(), LodLevel::Unloaded, priority, frame);
3981 if let Some(chunk) = self.chunks.get_mut(coord) {
3982 chunk.load_state = ChunkLoadState::Evicting;
3983 }
3984 }
3985 }
3986
3987 let dispatched = self.unload_queue.dispatch_available(frame);
3988 for req in dispatched {
3989 let old_lod = self.chunks.get(&req.coord).map(|c| c.lod_level.clone()).unwrap_or(LodLevel::Unloaded);
3990 self.memory_budget.free(&req.coord, &old_lod);
3991 if let Some(chunk) = self.chunks.get_mut(&req.coord) {
3992 chunk.load_state = ChunkLoadState::Unloaded;
3993 chunk.lod_level = LodLevel::Unloaded;
3994 chunk.memory_bytes = 0;
3995 }
3996 self.loaded_chunk_set.remove(&req.coord);
3997 self.streaming_stats.chunks_unloaded_this_frame += 1;
3998 self.unload_queue.complete_request(req.request_id, true);
3999 }
4000 }
4001
4002 fn evict_memory_if_needed(&mut self) {
4003 if !self.memory_budget.needs_eviction() { return; }
4004 let frame = self.frame_number;
4005 let candidates = self.memory_budget.eviction_candidates(8, frame);
4006 for coord in candidates {
4007 if let Some(chunk) = self.chunks.get(&coord) {
4008 if chunk.can_evict() && !chunk.is_visible {
4009 let lod = chunk.lod_level.clone();
4010 self.memory_budget.free(&coord, &lod);
4011 if let Some(c) = self.chunks.get_mut(&coord) {
4012 c.load_state = ChunkLoadState::Unloaded;
4013 c.lod_level = LodLevel::Unloaded;
4014 c.memory_bytes = 0;
4015 }
4016 self.loaded_chunk_set.remove(&coord);
4017 }
4018 }
4019 }
4020 }
4021
4022 fn update_terrain_stitching(&mut self) {
4023 let needs_stitch: Vec<ChunkCoord> = self.terrain_patches
4024 .iter()
4025 .filter(|(_, p)| p.needs_stitch)
4026 .map(|(c, _)| c.clone())
4027 .collect();
4028
4029 for coord in needs_stitch {
4030 let neighbors_6 = coord.neighbors_6();
4031 let neighbor_seams_and_lods: Vec<_> = [0usize, 1, 2, 3].iter().map(|&side| {
4032 let ncoord = neighbors_6[side * 2];
4033 let seam = self.terrain_patches.get(&ncoord).map(|p| p.seam_data[1 - side % 2].clone()).unwrap_or_default();
4034 let lod = self.terrain_patches.get(&ncoord).map(|p| p.lod_level.clone());
4035 (seam, lod)
4036 }).collect();
4037
4038 if let Some(patch) = self.terrain_patches.get_mut(&coord) {
4039 for side in 0..4 {
4040 if let (seam, Some(neighbor_lod)) = &neighbor_seams_and_lods[side] {
4041 if !seam.is_empty() {
4042 patch.stitch_edge(side, seam, neighbor_lod);
4043 }
4044 }
4045 }
4046 self.streaming_stats.terrain_stitch_ops += 1;
4047 }
4048 }
4049 }
4050
4051 fn update_impostors(&mut self) {
4052 if !self.config.enable_impostor_billboards { return; }
4053 let viewer = self.viewer_position;
4054 let frame = self.frame_number;
4055 for (_, impostor) in self.impostors.iter_mut() {
4056 if impostor.should_update(viewer, 5.0) {
4057 impostor.update_view_index(viewer);
4058 impostor.clear_dirty(frame);
4059 }
4060 }
4061 }
4062
4063 fn update_virtual_textures(&mut self) {
4064 if !self.config.enable_virtual_textures { return; }
4065 if let Some(ref mut vt) = self.virtual_texture {
4066 let requests = vt.analyze_feedback_buffer();
4067 let to_load: Vec<_> = requests.iter().take(8).filter(|r| r.3 > 0.01).cloned().collect();
4068 for (mip, tx, ty, _) in to_load {
4069 vt.load_tile(mip, tx, ty);
4070 self.streaming_stats.virtual_texture_uploads += 1;
4071 }
4072 vt.evict_lru_tiles(vt.max_resident_tiles / 4);
4073 vt.clear_feedback();
4074 }
4075 }
4076
4077 pub fn estimate_chunk_memory(&self, coord: &ChunkCoord, lod: &LodLevel) -> u64 {
4078 let base = 4 * 1024 * 1024u64; (base as f32 * lod.memory_multiplier()) as u64
4080 }
4081
4082 pub fn register_actor(&mut self, actor_id: u32, position: Vec3, bounds: ChunkBounds, streaming_dist: f32) {
4083 let mut proxy = ActorStreamingProxy::new(actor_id, position, bounds);
4084 proxy.streaming_distance = streaming_dist;
4085 self.actor_proxies.insert(actor_id, proxy);
4086 self.world_partition.register_actor(actor_id, position);
4087 }
4088
4089 pub fn unregister_actor(&mut self, actor_id: u32) {
4090 self.actor_proxies.remove(&actor_id);
4091 self.world_partition.unregister_actor(actor_id);
4092 }
4093
4094 pub fn add_terrain_patch(&mut self, coord: ChunkCoord, size: usize, cell_size: f32) {
4095 let patch = TerrainPatch::new(coord.clone(), size, cell_size);
4096 self.terrain_patches.insert(coord, patch);
4097 }
4098
4099 pub fn build_octree(&mut self) {
4100 let points: Vec<(u32, Vec3)> = self.actor_proxies.iter()
4101 .map(|(&id, proxy)| (id, proxy.world_position))
4102 .collect();
4103 let builder = OctreeBuilder::new(MAX_OCTREE_DEPTH, MAX_OBJECTS_PER_OCTREE_NODE);
4104 self.octree = Some(builder.build(&points));
4105 }
4106
4107 pub fn build_bvh(&mut self) {
4108 let objects: Vec<(u32, ChunkBounds)> = self.actor_proxies.iter()
4109 .map(|(&id, proxy)| (id, proxy.bounds.clone()))
4110 .collect();
4111 let builder = BvhBuilder::new(BVH_MAX_LEAF_OBJECTS, 16);
4112 self.bvh = builder.build(&objects);
4113 }
4114
4115 pub fn build_hlod(&mut self) {
4116 let actors: Vec<(u32, Vec3, ChunkBounds)> = self.actor_proxies.iter()
4117 .map(|(&id, proxy)| (id, proxy.world_position, proxy.bounds.clone()))
4118 .collect();
4119 self.hlod_hierarchy = self.hlod_builder.build_hierarchical(&actors);
4120 self.streaming_stats.hlod_merges += 1;
4121 }
4122
4123 pub fn query_visible_objects(&self) -> Vec<u32> {
4124 let mut result = Vec::new();
4125 if let Some(ref frustum) = self.frustum {
4126 if let Some(ref bvh) = self.bvh {
4127 bvh.query_frustum(frustum, &mut result);
4128 }
4129 }
4130 result
4131 }
4132
4133 pub fn query_objects_in_radius(&self, center: Vec3, radius: f32) -> Vec<u32> {
4134 let mut result = Vec::new();
4135 if let Some(ref octree) = self.octree {
4136 octree.query_sphere(center, radius, &mut result);
4137 }
4138 result
4139 }
4140
4141 pub fn query_objects_in_bounds(&self, bounds: &ChunkBounds) -> Vec<u32> {
4142 let mut result = Vec::new();
4143 if let Some(ref bvh) = self.bvh {
4144 bvh.query_aabb(bounds, &mut result);
4145 }
4146 result
4147 }
4148
4149 pub fn get_chunk_lod(&self, coord: &ChunkCoord) -> LodLevel {
4150 self.chunks.get(coord).map(|c| c.lod_level.clone()).unwrap_or(LodLevel::Unloaded)
4151 }
4152
4153 pub fn get_chunk_state(&self, coord: &ChunkCoord) -> ChunkLoadState {
4154 self.chunks.get(coord).map(|c| c.load_state.clone()).unwrap_or(ChunkLoadState::Unloaded)
4155 }
4156
4157 pub fn chunk_count(&self) -> usize {
4158 self.chunks.len()
4159 }
4160
4161 pub fn loaded_chunk_count(&self) -> usize {
4162 self.loaded_chunk_set.len()
4163 }
4164
4165 pub fn memory_usage_mb(&self) -> f32 {
4166 self.memory_budget.total_used_mb()
4167 }
4168
4169 pub fn stats(&self) -> &StreamingStats {
4170 &self.streaming_stats
4171 }
4172
4173 pub fn profiler_summary(&self) -> ProfilerSummary {
4174 self.profiler.report_summary()
4175 }
4176
4177 pub fn set_lod_bias(&mut self, bias: f32) {
4178 self.config.lod_bias = bias;
4179 self.streaming_distance_calculator.lod_bias = bias;
4180 }
4181
4182 pub fn force_lod(&mut self, coord: &ChunkCoord, lod: LodLevel) {
4183 if let Some(chunk) = self.chunks.get_mut(coord) {
4184 let old = chunk.lod_level.clone();
4185 chunk.lod_level = lod.clone();
4186 chunk.flags.needs_lod_update = true;
4187 self.pending_lod_transitions.push((coord.clone(), old, lod));
4188 }
4189 }
4190
4191 pub fn get_terrain_height(&self, world_x: f32, world_z: f32) -> f32 {
4192 let coord = ChunkCoord::from_world_pos(
4193 Vec3::new(world_x, 0.0, world_z),
4194 self.config.chunk_size,
4195 );
4196 if let Some(patch) = self.terrain_patches.get(&coord) {
4197 patch.heightmap.sample_bilinear(world_x, world_z)
4198 } else {
4199 0.0
4200 }
4201 }
4202
4203 pub fn get_terrain_normal(&self, world_x: f32, world_z: f32) -> Vec3 {
4204 let coord = ChunkCoord::from_world_pos(
4205 Vec3::new(world_x, 0.0, world_z),
4206 self.config.chunk_size,
4207 );
4208 if let Some(patch) = self.terrain_patches.get(&coord) {
4209 patch.heightmap.compute_normal_bilinear(world_x, world_z)
4210 } else {
4211 Vec3::Y
4212 }
4213 }
4214
4215 pub fn compute_streaming_bounds(&self) -> ChunkBounds {
4216 let half_r = Vec3::new(
4217 self.config.streaming_radius,
4218 self.config.vertical_streaming_radius,
4219 self.config.streaming_radius,
4220 );
4221 ChunkBounds::from_center_size(self.viewer_position, half_r)
4222 }
4223
4224 pub fn debug_draw_chunks(&self) -> Vec<(ChunkBounds, Vec3, LodLevel)> {
4225 self.chunks.values()
4226 .filter(|c| c.load_state != ChunkLoadState::Unloaded)
4227 .map(|c| {
4228 let color = match c.lod_level {
4229 LodLevel::Ultra => Vec3::new(0.0, 1.0, 0.0),
4230 LodLevel::High => Vec3::new(0.5, 1.0, 0.0),
4231 LodLevel::Medium => Vec3::new(1.0, 1.0, 0.0),
4232 LodLevel::Low => Vec3::new(1.0, 0.5, 0.0),
4233 LodLevel::Impostor => Vec3::new(1.0, 0.0, 0.0),
4234 LodLevel::Unloaded => Vec3::new(0.3, 0.3, 0.3),
4235 };
4236 (c.bounds.clone(), color, c.lod_level.clone())
4237 })
4238 .collect()
4239 }
4240}
4241
4242pub fn compute_view_matrix(eye: Vec3, target: Vec3, up: Vec3) -> Mat4 {
4247 Mat4::look_at_rh(eye, target, up)
4248}
4249
4250pub fn compute_projection_matrix(fov_y_deg: f32, aspect: f32, near: f32, far: f32) -> Mat4 {
4251 Mat4::perspective_rh(fov_y_deg.to_radians(), aspect, near, far)
4252}
4253
4254pub fn compute_ortho_matrix(left: f32, right: f32, bottom: f32, top: f32, near: f32, far: f32) -> Mat4 {
4255 Mat4::orthographic_rh(left, right, bottom, top, near, far)
4256}
4257
4258pub fn screen_to_world_ray(
4259 screen_x: f32,
4260 screen_y: f32,
4261 screen_w: f32,
4262 screen_h: f32,
4263 inv_view_proj: Mat4,
4264) -> (Vec3, Vec3) {
4265 let ndc_x = (screen_x / screen_w) * 2.0 - 1.0;
4266 let ndc_y = 1.0 - (screen_y / screen_h) * 2.0;
4267 let near_point = inv_view_proj.project_point3(Vec3::new(ndc_x, ndc_y, -1.0));
4268 let far_point = inv_view_proj.project_point3(Vec3::new(ndc_x, ndc_y, 1.0));
4269 let dir = (far_point - near_point).normalize();
4270 (near_point, dir)
4271}
4272
4273pub fn compute_sphere_screen_radius(center: Vec3, radius: f32, proj_matrix: Mat4, screen_height: f32) -> f32 {
4274 let proj_center = proj_matrix.project_point3(center);
4275 let proj_edge = proj_matrix.project_point3(center + Vec3::new(radius, 0.0, 0.0));
4276 let screen_radius = (proj_center - proj_edge).length() * screen_height * 0.5;
4277 screen_radius.abs()
4278}
4279
4280pub fn lerp_vec3(a: Vec3, b: Vec3, t: f32) -> Vec3 {
4281 a + (b - a) * t
4282}
4283
4284pub fn smooth_step(edge0: f32, edge1: f32, x: f32) -> f32 {
4285 let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
4286 t * t * (3.0 - 2.0 * t)
4287}
4288
4289pub fn smoother_step(edge0: f32, edge1: f32, x: f32) -> f32 {
4290 let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
4291 t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
4292}
4293
4294pub fn project_point_to_plane(point: Vec3, plane_normal: Vec3, plane_d: f32) -> Vec3 {
4295 let dist = plane_normal.dot(point) + plane_d;
4296 point - plane_normal * dist
4297}
4298
4299pub fn distance_point_to_line(point: Vec3, line_origin: Vec3, line_dir: Vec3) -> f32 {
4300 let v = point - line_origin;
4301 let d = v - line_dir * v.dot(line_dir);
4302 d.length()
4303}
4304
4305pub fn closest_point_on_segment(point: Vec3, a: Vec3, b: Vec3) -> Vec3 {
4306 let ab = b - a;
4307 let len_sq = ab.length_squared();
4308 if len_sq < 1e-12 { return a; }
4309 let t = ((point - a).dot(ab) / len_sq).clamp(0.0, 1.0);
4310 a + ab * t
4311}
4312
4313pub fn triangle_area(a: Vec3, b: Vec3, c: Vec3) -> f32 {
4314 (b - a).cross(c - a).length() * 0.5
4315}
4316
4317pub fn triangle_normal(a: Vec3, b: Vec3, c: Vec3) -> Vec3 {
4318 (b - a).cross(c - a).normalize()
4319}
4320
4321pub fn barycentric_coords(point: Vec3, a: Vec3, b: Vec3, c: Vec3) -> Vec3 {
4322 let v0 = b - a;
4323 let v1 = c - a;
4324 let v2 = point - a;
4325 let d00 = v0.dot(v0);
4326 let d01 = v0.dot(v1);
4327 let d11 = v1.dot(v1);
4328 let d20 = v2.dot(v0);
4329 let d21 = v2.dot(v1);
4330 let denom = d00 * d11 - d01 * d01;
4331 if denom.abs() < 1e-12 { return Vec3::new(1.0, 0.0, 0.0); }
4332 let v = (d11 * d20 - d01 * d21) / denom;
4333 let w = (d00 * d21 - d01 * d20) / denom;
4334 Vec3::new(1.0 - v - w, v, w)
4335}
4336
4337pub fn point_in_triangle(point: Vec3, a: Vec3, b: Vec3, c: Vec3) -> bool {
4338 let bary = barycentric_coords(point, a, b, c);
4339 bary.x >= 0.0 && bary.y >= 0.0 && bary.z >= 0.0
4340}
4341
4342pub fn compute_tangent_space(pos0: Vec3, pos1: Vec3, pos2: Vec3, uv0: Vec2, uv1: Vec2, uv2: Vec2) -> (Vec3, Vec3) {
4343 let edge1 = pos1 - pos0;
4344 let edge2 = pos2 - pos0;
4345 let duv1 = uv1 - uv0;
4346 let duv2 = uv2 - uv0;
4347 let denom = duv1.x * duv2.y - duv2.x * duv1.y;
4348 if denom.abs() < 1e-12 {
4349 return (Vec3::X, Vec3::Y);
4350 }
4351 let inv = 1.0 / denom;
4352 let tangent = (edge1 * duv2.y - edge2 * duv1.y) * inv;
4353 let bitangent = (edge2 * duv1.x - edge1 * duv2.x) * inv;
4354 (tangent.normalize(), bitangent.normalize())
4355}
4356
4357pub fn compute_lod_bias_from_mip(mip: u32, base_mip: u32) -> f32 {
4358 if mip <= base_mip { 0.0 } else { (mip - base_mip) as f32 }
4359}
4360
4361pub fn hash_2d(x: i32, y: i32) -> u32 {
4366 let mut h = x.wrapping_mul(1234567891i32).wrapping_add(y.wrapping_mul(987654321i32)) as u32;
4367 h ^= h >> 16;
4368 h = h.wrapping_mul(0x45d9f3b);
4369 h ^= h >> 16;
4370 h
4371}
4372
4373pub fn hash_3d(x: i32, y: i32, z: i32) -> u32 {
4374 let mut h = x.wrapping_mul(1234567891i32)
4375 .wrapping_add(y.wrapping_mul(987654321i32))
4376 .wrapping_add(z.wrapping_mul(741852963i32)) as u32;
4377 h ^= h >> 16;
4378 h = h.wrapping_mul(0x45d9f3b);
4379 h ^= h >> 16;
4380 h
4381}
4382
4383pub fn value_noise_2d(x: f32, y: f32) -> f32 {
4384 let ix = x.floor() as i32;
4385 let iy = y.floor() as i32;
4386 let fx = x - ix as f32;
4387 let fy = y - iy as f32;
4388 let ux = smooth_step(0.0, 1.0, fx);
4389 let uy = smooth_step(0.0, 1.0, fy);
4390 let v00 = (hash_2d(ix, iy ) as f32) / u32::MAX as f32;
4391 let v10 = (hash_2d(ix + 1, iy ) as f32) / u32::MAX as f32;
4392 let v01 = (hash_2d(ix, iy + 1) as f32) / u32::MAX as f32;
4393 let v11 = (hash_2d(ix + 1, iy + 1) as f32) / u32::MAX as f32;
4394 let h0 = v00 * (1.0 - ux) + v10 * ux;
4395 let h1 = v01 * (1.0 - ux) + v11 * ux;
4396 h0 * (1.0 - uy) + h1 * uy
4397}
4398
4399pub fn fbm_noise_2d(x: f32, y: f32, octaves: u32, lacunarity: f32, gain: f32) -> f32 {
4400 let mut sum = 0.0f32;
4401 let mut amplitude = 1.0f32;
4402 let mut frequency = 1.0f32;
4403 let mut max_amp = 0.0f32;
4404 for _ in 0..octaves {
4405 sum += value_noise_2d(x * frequency, y * frequency) * amplitude;
4406 max_amp += amplitude;
4407 amplitude *= gain;
4408 frequency *= lacunarity;
4409 }
4410 if max_amp > 0.0 { sum / max_amp } else { 0.0 }
4411}
4412
4413pub fn generate_heightmap_fbm(
4414 width: usize,
4415 height: usize,
4416 cell_size: f32,
4417 origin: Vec2,
4418 octaves: u32,
4419 scale: f32,
4420 amplitude: f32,
4421) -> TerrainHeightmap {
4422 let mut hm = TerrainHeightmap::new(width, height, cell_size, origin, 1.0);
4423 for z in 0..height {
4424 for x in 0..width {
4425 let wx = (origin.x + x as f32 * cell_size) * scale;
4426 let wz = (origin.y + z as f32 * cell_size) * scale;
4427 let h = fbm_noise_2d(wx, wz, octaves, 2.0, 0.5) * amplitude;
4428 hm.set_height(x, z, h);
4429 }
4430 }
4431 hm
4432}
4433
4434#[derive(Debug, Clone)]
4439pub struct LodTransitionState {
4440 pub coord: ChunkCoord,
4441 pub from_lod: LodLevel,
4442 pub to_lod: LodLevel,
4443 pub blend_alpha: f32,
4444 pub transition_duration_frames: u32,
4445 pub frames_elapsed: u32,
4446}
4447
4448impl LodTransitionState {
4449 pub fn new(coord: ChunkCoord, from: LodLevel, to: LodLevel, duration_frames: u32) -> Self {
4450 Self {
4451 coord,
4452 from_lod: from,
4453 to_lod: to,
4454 blend_alpha: 0.0,
4455 transition_duration_frames: duration_frames,
4456 frames_elapsed: 0,
4457 }
4458 }
4459
4460 pub fn advance(&mut self) -> bool {
4461 self.frames_elapsed += 1;
4462 self.blend_alpha = if self.transition_duration_frames > 0 {
4463 (self.frames_elapsed as f32 / self.transition_duration_frames as f32).clamp(0.0, 1.0)
4464 } else {
4465 1.0
4466 };
4467 self.blend_alpha >= 1.0
4468 }
4469
4470 pub fn is_complete(&self) -> bool {
4471 self.blend_alpha >= 1.0
4472 }
4473
4474 pub fn smooth_alpha(&self) -> f32 {
4475 smooth_step(0.0, 1.0, self.blend_alpha)
4476 }
4477}
4478
4479#[derive(Debug, Clone)]
4480pub struct LodTransitionManager {
4481 pub active_transitions: HashMap<ChunkCoord, LodTransitionState>,
4482 pub transition_duration_frames: u32,
4483 pub enable_smooth_transitions: bool,
4484}
4485
4486impl LodTransitionManager {
4487 pub fn new(duration_frames: u32, smooth: bool) -> Self {
4488 Self {
4489 active_transitions: HashMap::new(),
4490 transition_duration_frames: duration_frames,
4491 enable_smooth_transitions: smooth,
4492 }
4493 }
4494
4495 pub fn begin_transition(&mut self, coord: ChunkCoord, from: LodLevel, to: LodLevel) {
4496 let duration = if self.enable_smooth_transitions { self.transition_duration_frames } else { 0 };
4497 let state = LodTransitionState::new(coord.clone(), from, to, duration);
4498 self.active_transitions.insert(coord, state);
4499 }
4500
4501 pub fn tick(&mut self) -> Vec<ChunkCoord> {
4502 let mut completed = Vec::new();
4503 for (coord, state) in self.active_transitions.iter_mut() {
4504 if state.advance() {
4505 completed.push(coord.clone());
4506 }
4507 }
4508 for c in &completed {
4509 self.active_transitions.remove(c);
4510 }
4511 completed
4512 }
4513
4514 pub fn get_blend_alpha(&self, coord: &ChunkCoord) -> f32 {
4515 self.active_transitions.get(coord).map(|s| s.smooth_alpha()).unwrap_or(1.0)
4516 }
4517
4518 pub fn is_transitioning(&self, coord: &ChunkCoord) -> bool {
4519 self.active_transitions.contains_key(coord)
4520 }
4521
4522 pub fn active_count(&self) -> usize {
4523 self.active_transitions.len()
4524 }
4525
4526 pub fn cancel_transition(&mut self, coord: &ChunkCoord) {
4527 self.active_transitions.remove(coord);
4528 }
4529}
4530
4531#[derive(Debug, Clone)]
4536pub struct VisibilityGrid {
4537 pub cell_size: f32,
4538 pub cells: HashMap<(i32, i32, i32), Vec<u32>>,
4539 pub object_cells: HashMap<u32, (i32, i32, i32)>,
4540}
4541
4542impl VisibilityGrid {
4543 pub fn new(cell_size: f32) -> Self {
4544 Self {
4545 cell_size,
4546 cells: HashMap::new(),
4547 object_cells: HashMap::new(),
4548 }
4549 }
4550
4551 pub fn insert(&mut self, id: u32, pos: Vec3) {
4552 let cell = self.pos_to_cell(pos);
4553 self.cells.entry(cell).or_insert_with(Vec::new).push(id);
4554 self.object_cells.insert(id, cell);
4555 }
4556
4557 pub fn remove(&mut self, id: u32) {
4558 if let Some(cell) = self.object_cells.remove(&id) {
4559 if let Some(ids) = self.cells.get_mut(&cell) {
4560 ids.retain(|&i| i != id);
4561 }
4562 }
4563 }
4564
4565 pub fn update(&mut self, id: u32, new_pos: Vec3) {
4566 let new_cell = self.pos_to_cell(new_pos);
4567 if let Some(old_cell) = self.object_cells.get(&id).cloned() {
4568 if old_cell == new_cell { return; }
4569 if let Some(ids) = self.cells.get_mut(&old_cell) {
4570 ids.retain(|&i| i != id);
4571 }
4572 }
4573 self.cells.entry(new_cell).or_insert_with(Vec::new).push(id);
4574 self.object_cells.insert(id, new_cell);
4575 }
4576
4577 pub fn query_radius(&self, center: Vec3, radius: f32) -> Vec<u32> {
4578 let cells = self.cells_in_radius(center, radius);
4579 let mut result = Vec::new();
4580 for cell in cells {
4581 if let Some(ids) = self.cells.get(&cell) {
4582 result.extend_from_slice(ids);
4583 }
4584 }
4585 result
4586 }
4587
4588 pub fn query_aabb(&self, bounds: &ChunkBounds) -> Vec<u32> {
4589 let cells = self.cells_in_aabb(bounds);
4590 let mut result = Vec::new();
4591 for cell in cells {
4592 if let Some(ids) = self.cells.get(&cell) {
4593 result.extend_from_slice(ids);
4594 }
4595 }
4596 result
4597 }
4598
4599 fn pos_to_cell(&self, pos: Vec3) -> (i32, i32, i32) {
4600 (
4601 (pos.x / self.cell_size).floor() as i32,
4602 (pos.y / self.cell_size).floor() as i32,
4603 (pos.z / self.cell_size).floor() as i32,
4604 )
4605 }
4606
4607 fn cells_in_radius(&self, center: Vec3, radius: f32) -> Vec<(i32, i32, i32)> {
4608 let cr = (radius / self.cell_size).ceil() as i32;
4609 let cc = self.pos_to_cell(center);
4610 let mut cells = Vec::new();
4611 for dx in -cr..=cr {
4612 for dy in -cr..=cr {
4613 for dz in -cr..=cr {
4614 cells.push((cc.0 + dx, cc.1 + dy, cc.2 + dz));
4615 }
4616 }
4617 }
4618 cells
4619 }
4620
4621 fn cells_in_aabb(&self, bounds: &ChunkBounds) -> Vec<(i32, i32, i32)> {
4622 let min_c = self.pos_to_cell(bounds.min);
4623 let max_c = self.pos_to_cell(bounds.max);
4624 let mut cells = Vec::new();
4625 for cx in min_c.0..=max_c.0 {
4626 for cy in min_c.1..=max_c.1 {
4627 for cz in min_c.2..=max_c.2 {
4628 cells.push((cx, cy, cz));
4629 }
4630 }
4631 }
4632 cells
4633 }
4634
4635 pub fn cell_count(&self) -> usize {
4636 self.cells.len()
4637 }
4638
4639 pub fn object_count(&self) -> usize {
4640 self.object_cells.len()
4641 }
4642}
4643
4644#[derive(Debug, Clone)]
4649pub struct ScreenSpaceErrorMetric {
4650 pub screen_height_pixels: f32,
4651 pub fov_vertical_rad: f32,
4652 pub error_threshold: f32,
4653}
4654
4655impl ScreenSpaceErrorMetric {
4656 pub fn new(screen_height: f32, fov_rad: f32, threshold: f32) -> Self {
4657 Self {
4658 screen_height_pixels: screen_height,
4659 fov_vertical_rad: fov_rad,
4660 error_threshold: threshold,
4661 }
4662 }
4663
4664 pub fn project_error(&self, world_error: f32, distance: f32) -> f32 {
4665 if distance < 1e-6 { return f32::MAX; }
4666 let half_fov_tan = (self.fov_vertical_rad * 0.5).tan();
4667 (world_error / distance) / half_fov_tan * self.screen_height_pixels
4668 }
4669
4670 pub fn needs_refinement(&self, world_error: f32, distance: f32) -> bool {
4671 self.project_error(world_error, distance) > self.error_threshold
4672 }
4673
4674 pub fn select_lod_for_bounds(&self, bounds: &ChunkBounds, camera_pos: Vec3) -> LodLevel {
4675 let dist = bounds.distance_to_point(camera_pos);
4676 let radius = bounds.half_size().length();
4677 let screen_size = self.project_error(radius, dist.max(0.001));
4678 if screen_size > 512.0 { LodLevel::Ultra }
4679 else if screen_size > 256.0 { LodLevel::High }
4680 else if screen_size > 64.0 { LodLevel::Medium }
4681 else if screen_size > 16.0 { LodLevel::Low }
4682 else if screen_size > 2.0 { LodLevel::Impostor}
4683 else { LodLevel::Unloaded}
4684 }
4685
4686 pub fn blend_factor(&self, world_error: f32, distance: f32) -> f32 {
4687 let sse = self.project_error(world_error, distance);
4688 let near = self.error_threshold * 0.5;
4689 let far = self.error_threshold * 2.0;
4690 smooth_step(near, far, sse)
4691 }
4692
4693 pub fn max_error_for_distance(&self, distance: f32) -> f32 {
4694 let half_fov_tan = (self.fov_vertical_rad * 0.5).tan();
4695 self.error_threshold * distance * half_fov_tan / self.screen_height_pixels
4696 }
4697}
4698
4699#[derive(Debug, Clone)]
4704pub struct ChunkPool {
4705 pub free_chunks: VecDeque<StreamingChunk>,
4706 pub allocated_count: usize,
4707 pub chunk_size: f32,
4708 pub pool_capacity: usize,
4709}
4710
4711impl ChunkPool {
4712 pub fn new(capacity: usize, chunk_size: f32) -> Self {
4713 Self {
4714 free_chunks: VecDeque::with_capacity(capacity),
4715 allocated_count: 0,
4716 chunk_size,
4717 pool_capacity: capacity,
4718 }
4719 }
4720
4721 pub fn pre_allocate(&mut self, count: usize) {
4722 for i in 0..count.min(self.pool_capacity) {
4723 let coord = ChunkCoord::new(i as i32, 0, 0);
4724 self.free_chunks.push_back(StreamingChunk::new(coord, self.chunk_size));
4725 }
4726 }
4727
4728 pub fn acquire(&mut self, coord: ChunkCoord) -> StreamingChunk {
4729 if let Some(mut chunk) = self.free_chunks.pop_front() {
4730 chunk.coord = coord.clone();
4731 chunk.bounds = ChunkBounds::from_chunk_coord(&coord, self.chunk_size);
4732 chunk.lod_level = LodLevel::Unloaded;
4733 chunk.load_state = ChunkLoadState::Unloaded;
4734 chunk.resident_objects.clear();
4735 chunk.memory_bytes = 0;
4736 chunk.is_visible = false;
4737 chunk.flags = ChunkFlags::default();
4738 self.allocated_count += 1;
4739 chunk
4740 } else {
4741 self.allocated_count += 1;
4742 StreamingChunk::new(coord, self.chunk_size)
4743 }
4744 }
4745
4746 pub fn release(&mut self, chunk: StreamingChunk) {
4747 if self.free_chunks.len() < self.pool_capacity {
4748 self.free_chunks.push_back(chunk);
4749 }
4750 if self.allocated_count > 0 { self.allocated_count -= 1; }
4751 }
4752
4753 pub fn free_count(&self) -> usize {
4754 self.free_chunks.len()
4755 }
4756
4757 pub fn allocated_count(&self) -> usize {
4758 self.allocated_count
4759 }
4760}
4761
4762#[derive(Debug, Clone)]
4767pub struct MeshVertex {
4768 pub position: Vec3,
4769 pub normal: Vec3,
4770 pub uv: Vec2,
4771}
4772
4773#[derive(Debug, Clone)]
4774pub struct SimplifiedMesh {
4775 pub vertices: Vec<MeshVertex>,
4776 pub indices: Vec<u32>,
4777 pub lod_level: LodLevel,
4778 pub error_metric: f32,
4779}
4780
4781impl SimplifiedMesh {
4782 pub fn new(lod: LodLevel) -> Self {
4783 Self {
4784 vertices: Vec::new(),
4785 indices: Vec::new(),
4786 lod_level: lod,
4787 error_metric: 0.0,
4788 }
4789 }
4790
4791 pub fn vertex_count(&self) -> usize {
4792 self.vertices.len()
4793 }
4794
4795 pub fn triangle_count(&self) -> usize {
4796 self.indices.len() / 3
4797 }
4798
4799 pub fn memory_bytes(&self) -> usize {
4800 self.vertices.len() * 32 + self.indices.len() * 4
4802 }
4803
4804 pub fn compute_bounds(&self) -> ChunkBounds {
4805 if self.vertices.is_empty() {
4806 return ChunkBounds::new(Vec3::ZERO, Vec3::ZERO);
4807 }
4808 let mut min = Vec3::splat(f32::MAX);
4809 let mut max = Vec3::splat(f32::MIN);
4810 for v in &self.vertices {
4811 min = min.min(v.position);
4812 max = max.max(v.position);
4813 }
4814 ChunkBounds { min, max }
4815 }
4816
4817 pub fn recompute_normals(&mut self) {
4818 let n = self.vertices.len();
4819 let mut normals = vec![Vec3::ZERO; n];
4820 for tri in self.indices.chunks_exact(3) {
4821 let (i0, i1, i2) = (tri[0] as usize, tri[1] as usize, tri[2] as usize);
4822 if i0 >= n || i1 >= n || i2 >= n { continue; }
4823 let p0 = self.vertices[i0].position;
4824 let p1 = self.vertices[i1].position;
4825 let p2 = self.vertices[i2].position;
4826 let normal = triangle_normal(p0, p1, p2);
4827 normals[i0] += normal;
4828 normals[i1] += normal;
4829 normals[i2] += normal;
4830 }
4831 for (i, v) in self.vertices.iter_mut().enumerate() {
4832 let len = normals[i].length();
4833 if len > NORMAL_SMOOTH_EPSILON {
4834 v.normal = normals[i] / len;
4835 }
4836 }
4837 }
4838}
4839
4840#[derive(Debug, Clone)]
4845pub enum StreamingEvent {
4846 ChunkLoaded { coord: ChunkCoord, lod: LodLevel },
4847 ChunkUnloaded { coord: ChunkCoord },
4848 LodChanged { coord: ChunkCoord, from: LodLevel, to: LodLevel },
4849 MemoryWarning { used_mb: f32, budget_mb: f32 },
4850 LoadQueueFull { size: usize },
4851 ActorLoaded { actor_id: u32, lod: LodLevel },
4852 ActorUnloaded { actor_id: u32 },
4853 HlodActivated { cluster_id: u32 },
4854 HlodDeactivated { cluster_id: u32 },
4855 VirtualTextureEviction { tile_count: u32 },
4856}
4857
4858#[derive(Debug, Clone)]
4859pub struct StreamingEventQueue {
4860 pub events: VecDeque<StreamingEvent>,
4861 pub max_events: usize,
4862 pub total_events_enqueued: u64,
4863 pub total_events_dequeued: u64,
4864}
4865
4866impl StreamingEventQueue {
4867 pub fn new(max_events: usize) -> Self {
4868 Self {
4869 events: VecDeque::with_capacity(max_events),
4870 max_events,
4871 total_events_enqueued: 0,
4872 total_events_dequeued: 0,
4873 }
4874 }
4875
4876 pub fn push(&mut self, event: StreamingEvent) {
4877 if self.events.len() >= self.max_events {
4878 self.events.pop_front();
4879 }
4880 self.events.push_back(event);
4881 self.total_events_enqueued += 1;
4882 }
4883
4884 pub fn drain(&mut self) -> Vec<StreamingEvent> {
4885 let count = self.events.len();
4886 self.total_events_dequeued += count as u64;
4887 self.events.drain(..).collect()
4888 }
4889
4890 pub fn peek(&self) -> Option<&StreamingEvent> {
4891 self.events.front()
4892 }
4893
4894 pub fn len(&self) -> usize {
4895 self.events.len()
4896 }
4897
4898 pub fn is_empty(&self) -> bool {
4899 self.events.is_empty()
4900 }
4901
4902 pub fn count_by_type(&self, discriminant: &str) -> usize {
4903 self.events.iter().filter(|e| {
4904 match (e, discriminant) {
4905 (StreamingEvent::ChunkLoaded { .. }, "ChunkLoaded") => true,
4906 (StreamingEvent::ChunkUnloaded { .. }, "ChunkUnloaded") => true,
4907 (StreamingEvent::LodChanged { .. }, "LodChanged") => true,
4908 _ => false,
4909 }
4910 }).count()
4911 }
4912}
4913
4914#[derive(Debug, Clone)]
4919pub struct SceneNode {
4920 pub id: u32,
4921 pub local_transform: Mat4,
4922 pub world_transform: Mat4,
4923 pub bounds: ChunkBounds,
4924 pub children: Vec<u32>,
4925 pub parent: Option<u32>,
4926 pub lod_level: LodLevel,
4927 pub is_visible: bool,
4928 pub is_static: bool,
4929 pub chunk_coord: Option<ChunkCoord>,
4930}
4931
4932impl SceneNode {
4933 pub fn new(id: u32) -> Self {
4934 Self {
4935 id,
4936 local_transform: Mat4::IDENTITY,
4937 world_transform: Mat4::IDENTITY,
4938 bounds: ChunkBounds::new(Vec3::ZERO, Vec3::ONE),
4939 children: Vec::new(),
4940 parent: None,
4941 lod_level: LodLevel::Unloaded,
4942 is_visible: true,
4943 is_static: true,
4944 chunk_coord: None,
4945 }
4946 }
4947
4948 pub fn set_translation(&mut self, pos: Vec3) {
4949 let (scale, rot, _) = self.local_transform.to_scale_rotation_translation();
4950 self.local_transform = Mat4::from_scale_rotation_translation(scale, rot, pos);
4951 }
4952
4953 pub fn set_rotation(&mut self, rot: Quat) {
4954 let (scale, _, pos) = self.local_transform.to_scale_rotation_translation();
4955 self.local_transform = Mat4::from_scale_rotation_translation(scale, rot, pos);
4956 }
4957
4958 pub fn set_scale(&mut self, scale: Vec3) {
4959 let (_, rot, pos) = self.local_transform.to_scale_rotation_translation();
4960 self.local_transform = Mat4::from_scale_rotation_translation(scale, rot, pos);
4961 }
4962
4963 pub fn update_world_transform(&mut self, parent_world: Mat4) {
4964 self.world_transform = parent_world * self.local_transform;
4965 let world_bounds = self.bounds.transformed_by(self.world_transform);
4966 self.bounds = world_bounds;
4967 }
4968
4969 pub fn world_position(&self) -> Vec3 {
4970 let (_, _, pos) = self.world_transform.to_scale_rotation_translation();
4971 pos
4972 }
4973
4974 pub fn world_forward(&self) -> Vec3 {
4975 let rot = Quat::from_mat4(&self.world_transform);
4976 rot * Vec3::NEG_Z
4977 }
4978}
4979
4980#[derive(Debug, Clone)]
4981pub struct SceneGraph {
4982 pub nodes: HashMap<u32, SceneNode>,
4983 pub root_nodes: Vec<u32>,
4984 pub next_id: u32,
4985 pub dirty_nodes: HashSet<u32>,
4986}
4987
4988impl SceneGraph {
4989 pub fn new() -> Self {
4990 Self {
4991 nodes: HashMap::new(),
4992 root_nodes: Vec::new(),
4993 next_id: 1,
4994 dirty_nodes: HashSet::new(),
4995 }
4996 }
4997
4998 pub fn create_node(&mut self) -> u32 {
4999 let id = self.next_id;
5000 self.next_id += 1;
5001 self.nodes.insert(id, SceneNode::new(id));
5002 self.root_nodes.push(id);
5003 id
5004 }
5005
5006 pub fn attach_child(&mut self, parent: u32, child: u32) {
5007 if let Some(p) = self.nodes.get_mut(&parent) {
5008 if !p.children.contains(&child) {
5009 p.children.push(child);
5010 }
5011 }
5012 if let Some(c) = self.nodes.get_mut(&child) {
5013 c.parent = Some(parent);
5014 }
5015 self.root_nodes.retain(|&id| id != child);
5016 self.dirty_nodes.insert(child);
5017 }
5018
5019 pub fn detach(&mut self, node_id: u32) {
5020 if let Some(parent_id) = self.nodes.get(&node_id).and_then(|n| n.parent) {
5021 if let Some(p) = self.nodes.get_mut(&parent_id) {
5022 p.children.retain(|&id| id != node_id);
5023 }
5024 }
5025 if let Some(n) = self.nodes.get_mut(&node_id) {
5026 n.parent = None;
5027 }
5028 self.root_nodes.push(node_id);
5029 self.dirty_nodes.insert(node_id);
5030 }
5031
5032 pub fn update_transforms(&mut self) {
5033 let roots: Vec<u32> = self.root_nodes.clone();
5034 for root in roots {
5035 self.update_subtree(root, Mat4::IDENTITY);
5036 }
5037 self.dirty_nodes.clear();
5038 }
5039
5040 fn update_subtree(&mut self, node_id: u32, parent_world: Mat4) {
5041 let world = {
5042 if let Some(node) = self.nodes.get_mut(&node_id) {
5043 node.world_transform = parent_world * node.local_transform;
5044 node.world_transform
5045 } else {
5046 return;
5047 }
5048 };
5049 let children: Vec<u32> = self.nodes.get(&node_id).map(|n| n.children.clone()).unwrap_or_default();
5050 for child in children {
5051 self.update_subtree(child, world);
5052 }
5053 }
5054
5055 pub fn mark_dirty(&mut self, node_id: u32) {
5056 self.dirty_nodes.insert(node_id);
5057 }
5058
5059 pub fn node_count(&self) -> usize {
5060 self.nodes.len()
5061 }
5062}
5063
5064#[derive(Debug, Clone)]
5069pub struct MeshLodManager {
5070 pub mesh_lods: HashMap<u32, Vec<SimplifiedMesh>>,
5071 pub screen_space_metric: ScreenSpaceErrorMetric,
5072 pub transition_manager: LodTransitionManager,
5073 pub current_lods: HashMap<u32, usize>,
5074}
5075
5076impl MeshLodManager {
5077 pub fn new(screen_height: f32, fov_rad: f32) -> Self {
5078 Self {
5079 mesh_lods: HashMap::new(),
5080 screen_space_metric: ScreenSpaceErrorMetric::new(screen_height, fov_rad, SCREEN_SPACE_ERROR_THRESHOLD),
5081 transition_manager: LodTransitionManager::new(4, true),
5082 current_lods: HashMap::new(),
5083 }
5084 }
5085
5086 pub fn register_mesh(&mut self, mesh_id: u32, lods: Vec<SimplifiedMesh>) {
5087 self.current_lods.insert(mesh_id, lods.len().saturating_sub(1));
5088 self.mesh_lods.insert(mesh_id, lods);
5089 }
5090
5091 pub fn update_lod(&mut self, mesh_id: u32, camera_pos: Vec3) {
5092 let lods = match self.mesh_lods.get(&mesh_id) {
5093 Some(l) => l.clone(),
5094 None => return,
5095 };
5096 if lods.is_empty() { return; }
5097 let bounds = lods[0].compute_bounds();
5098 let desired_lod = self.screen_space_metric.select_lod_for_bounds(&bounds, camera_pos);
5099 let desired_idx = desired_lod.index().min(lods.len() - 1);
5100 let current_idx = *self.current_lods.get(&mesh_id).unwrap_or(&0);
5101 if desired_idx != current_idx {
5102 self.transition_manager.begin_transition(
5103 ChunkCoord::new(mesh_id as i32, 0, 0),
5104 LodLevel::from_index(current_idx),
5105 LodLevel::from_index(desired_idx),
5106 );
5107 self.current_lods.insert(mesh_id, desired_idx);
5108 }
5109 }
5110
5111 pub fn get_current_lod_mesh(&self, mesh_id: u32) -> Option<&SimplifiedMesh> {
5112 let lods = self.mesh_lods.get(&mesh_id)?;
5113 let idx = *self.current_lods.get(&mesh_id)?;
5114 lods.get(idx)
5115 }
5116
5117 pub fn tick(&mut self) {
5118 self.transition_manager.tick();
5119 }
5120
5121 pub fn total_triangle_count(&self) -> usize {
5122 self.mesh_lods.values()
5123 .filter_map(|lods| {
5124 let idx = 0; lods.get(idx).map(|m| m.triangle_count())
5126 })
5127 .sum()
5128 }
5129
5130 pub fn memory_usage_bytes(&self) -> usize {
5131 self.mesh_lods.values()
5132 .flat_map(|lods| lods.iter())
5133 .map(|m| m.memory_bytes())
5134 .sum()
5135 }
5136}
5137
5138#[derive(Debug, Clone)]
5143pub struct ClusterGrid {
5144 pub cell_size: f32,
5145 pub clusters: HashMap<(i32, i32), Vec<u32>>,
5146 pub actor_cluster_map: HashMap<u32, u32>,
5147 pub cluster_bounds: HashMap<u32, ChunkBounds>,
5148 pub next_cluster_id: u32,
5149}
5150
5151impl ClusterGrid {
5152 pub fn new(cell_size: f32) -> Self {
5153 Self {
5154 cell_size,
5155 clusters: HashMap::new(),
5156 actor_cluster_map: HashMap::new(),
5157 cluster_bounds: HashMap::new(),
5158 next_cluster_id: 1,
5159 }
5160 }
5161
5162 pub fn cell_for_pos(&self, pos: Vec3) -> (i32, i32) {
5163 (
5164 (pos.x / self.cell_size).floor() as i32,
5165 (pos.z / self.cell_size).floor() as i32,
5166 )
5167 }
5168
5169 pub fn insert_actor(&mut self, actor_id: u32, pos: Vec3, bounds: ChunkBounds) {
5170 let cell = self.cell_for_pos(pos);
5171 let cluster_id = if let Some(&existing) = self.clusters.get(&cell).and_then(|v| v.first()) {
5172 existing
5173 } else {
5174 let id = self.next_cluster_id;
5175 self.next_cluster_id += 1;
5176 self.clusters.entry(cell).or_insert_with(Vec::new).push(id);
5177 id
5178 };
5179 self.actor_cluster_map.insert(actor_id, cluster_id);
5180 let cb = self.cluster_bounds.entry(cluster_id).or_insert_with(|| bounds.clone());
5181 *cb = cb.merge(&bounds);
5182 }
5183
5184 pub fn get_cluster_for_actor(&self, actor_id: u32) -> Option<u32> {
5185 self.actor_cluster_map.get(&actor_id).cloned()
5186 }
5187
5188 pub fn actors_in_cell(&self, cell: (i32, i32)) -> Vec<u32> {
5189 self.clusters.get(&cell)
5190 .map(|cluster_ids| {
5191 cluster_ids.iter()
5192 .flat_map(|cid| {
5193 self.actor_cluster_map.iter()
5194 .filter(|(_, &c)| c == *cid)
5195 .map(|(&a, _)| a)
5196 })
5197 .collect()
5198 })
5199 .unwrap_or_default()
5200 }
5201
5202 pub fn cells_in_radius(&self, center: Vec3, radius: f32) -> Vec<(i32, i32)> {
5203 let cr = (radius / self.cell_size).ceil() as i32;
5204 let cc = self.cell_for_pos(center);
5205 let mut cells = Vec::new();
5206 for dx in -cr..=cr {
5207 for dz in -cr..=cr {
5208 let cx = cc.0 + dx;
5209 let cz = cc.1 + dz;
5210 let world_x = cx as f32 * self.cell_size + self.cell_size * 0.5;
5211 let world_z = cz as f32 * self.cell_size + self.cell_size * 0.5;
5212 let cell_center = Vec3::new(world_x, center.y, world_z);
5213 if (cell_center - center).length() <= radius + self.cell_size * std::f32::consts::SQRT_2 * 0.5 {
5214 cells.push((cx, cz));
5215 }
5216 }
5217 }
5218 cells
5219 }
5220
5221 pub fn cluster_count(&self) -> usize {
5222 self.cluster_bounds.len()
5223 }
5224}
5225
5226#[derive(Debug, Clone)]
5231pub struct SpatialQueryResult {
5232 pub object_id: u32,
5233 pub distance: f32,
5234 pub intersection_point: Vec3,
5235 pub normal: Vec3,
5236}
5237
5238#[derive(Debug, Clone)]
5239pub struct SpatialQuerySystem {
5240 pub bvh: Option<BvhNode>,
5241 pub octree: Option<OctreeNode>,
5242 pub visibility_grid: VisibilityGrid,
5243 pub object_bounds: HashMap<u32, ChunkBounds>,
5244}
5245
5246impl SpatialQuerySystem {
5247 pub fn new(grid_cell_size: f32) -> Self {
5248 Self {
5249 bvh: None,
5250 octree: None,
5251 visibility_grid: VisibilityGrid::new(grid_cell_size),
5252 object_bounds: HashMap::new(),
5253 }
5254 }
5255
5256 pub fn register_object(&mut self, id: u32, pos: Vec3, bounds: ChunkBounds) {
5257 self.visibility_grid.insert(id, pos);
5258 self.object_bounds.insert(id, bounds);
5259 }
5260
5261 pub fn unregister_object(&mut self, id: u32) {
5262 self.visibility_grid.remove(id);
5263 self.object_bounds.remove(&id);
5264 }
5265
5266 pub fn raycast(&self, origin: Vec3, direction: Vec3, max_dist: f32) -> Vec<SpatialQueryResult> {
5267 let mut candidates = Vec::new();
5268 if let Some(ref bvh) = self.bvh {
5269 bvh.query_ray(origin, direction, 0.0, max_dist, &mut candidates);
5270 }
5271 let mut results = Vec::new();
5272 for id in candidates {
5273 if let Some(bounds) = self.object_bounds.get(&id) {
5274 if ray_aabb_intersect(origin, direction, bounds, 0.0, max_dist) {
5275 let closest = bounds.closest_point(origin);
5276 let dist = (closest - origin).length();
5277 let normal = (origin - closest).normalize_or_zero();
5278 results.push(SpatialQueryResult {
5279 object_id: id,
5280 distance: dist,
5281 intersection_point: closest,
5282 normal,
5283 });
5284 }
5285 }
5286 }
5287 results.sort_by(|a, b| a.distance.partial_cmp(&b.distance).unwrap_or(std::cmp::Ordering::Equal));
5288 results
5289 }
5290
5291 pub fn overlap_sphere(&self, center: Vec3, radius: f32) -> Vec<u32> {
5292 let candidates = self.visibility_grid.query_radius(center, radius);
5293 candidates.into_iter()
5294 .filter(|id| {
5295 if let Some(bounds) = self.object_bounds.get(id) {
5296 bounds.distance_to_point(center) <= radius
5297 } else {
5298 false
5299 }
5300 })
5301 .collect()
5302 }
5303
5304 pub fn overlap_aabb(&self, bounds: &ChunkBounds) -> Vec<u32> {
5305 let candidates = self.visibility_grid.query_aabb(bounds);
5306 candidates.into_iter()
5307 .filter(|id| {
5308 if let Some(ob) = self.object_bounds.get(id) {
5309 ob.intersects(bounds)
5310 } else {
5311 false
5312 }
5313 })
5314 .collect()
5315 }
5316
5317 pub fn frustum_query(&self, frustum: &FrustumCulling) -> Vec<u32> {
5318 let mut result = Vec::new();
5319 if let Some(ref bvh) = self.bvh {
5320 bvh.query_frustum(frustum, &mut result);
5321 } else {
5322 for (&id, bounds) in &self.object_bounds {
5323 if frustum.test_aabb_fast(bounds) {
5324 result.push(id);
5325 }
5326 }
5327 }
5328 result
5329 }
5330
5331 pub fn rebuild_bvh(&mut self) {
5332 let objects: Vec<(u32, ChunkBounds)> = self.object_bounds.iter()
5333 .map(|(&id, b)| (id, b.clone()))
5334 .collect();
5335 let builder = BvhBuilder::new(BVH_MAX_LEAF_OBJECTS, 16);
5336 self.bvh = builder.build(&objects);
5337 }
5338
5339 pub fn rebuild_octree(&mut self) {
5340 let points: Vec<(u32, Vec3)> = self.object_bounds.iter()
5341 .map(|(&id, b)| (id, b.center()))
5342 .collect();
5343 let builder = OctreeBuilder::new(MAX_OCTREE_DEPTH, MAX_OBJECTS_PER_OCTREE_NODE);
5344 self.octree = Some(builder.build(&points));
5345 }
5346}
5347
5348pub fn sphere_vs_sphere(c0: Vec3, r0: f32, c1: Vec3, r1: f32) -> bool {
5353 (c0 - c1).length_squared() <= (r0 + r1) * (r0 + r1)
5354}
5355
5356pub fn capsule_vs_sphere(cap_a: Vec3, cap_b: Vec3, cap_r: f32, sphere_c: Vec3, sphere_r: f32) -> bool {
5357 let closest = closest_point_on_segment(sphere_c, cap_a, cap_b);
5358 (sphere_c - closest).length_squared() <= (cap_r + sphere_r) * (cap_r + sphere_r)
5359}
5360
5361pub fn aabb_vs_sphere(bounds: &ChunkBounds, center: Vec3, radius: f32) -> bool {
5362 bounds.distance_sq_to_point(center) <= radius * radius
5363}
5364
5365pub fn obb_vs_point(center: Vec3, half_extents: Vec3, orientation: Quat, point: Vec3) -> bool {
5366 let local = Quat::conjugate(orientation).mul_vec3(point - center);
5367 local.x.abs() <= half_extents.x
5368 && local.y.abs() <= half_extents.y
5369 && local.z.abs() <= half_extents.z
5370}
5371
5372pub fn compute_aabb_from_obb(center: Vec3, half_extents: Vec3, orientation: Quat) -> ChunkBounds {
5373 let mat = Mat4::from_quat(orientation);
5374 let wx = mat.col(0).truncate() * half_extents.x;
5375 let wy = mat.col(1).truncate() * half_extents.y;
5376 let wz = mat.col(2).truncate() * half_extents.z;
5377 let abs_wx = Vec3::new(wx.x.abs(), wx.y.abs(), wx.z.abs());
5378 let abs_wy = Vec3::new(wy.x.abs(), wy.y.abs(), wy.z.abs());
5379 let abs_wz = Vec3::new(wz.x.abs(), wz.y.abs(), wz.z.abs());
5380 let new_half = abs_wx + abs_wy + abs_wz;
5381 ChunkBounds {
5382 min: center - new_half,
5383 max: center + new_half,
5384 }
5385}
5386
5387pub fn slerp_quat(a: Quat, b: Quat, t: f32) -> Quat {
5388 a.slerp(b, t)
5389}
5390
5391pub fn compute_look_at_quat(forward: Vec3, up: Vec3) -> Quat {
5392 let f = forward.normalize();
5393 let r = up.cross(f).normalize();
5394 let u = f.cross(r).normalize();
5395 Quat::from_mat3(&glam::Mat3::from_cols(r, u, f))
5396}
5397
5398#[derive(Debug, Clone)]
5403pub struct TerrainMaterialLayer {
5404 pub material_id: u32,
5405 pub blend_weight: f32,
5406 pub uv_scale: Vec2,
5407 pub normal_intensity: f32,
5408 pub roughness: f32,
5409 pub metalness: f32,
5410}
5411
5412impl TerrainMaterialLayer {
5413 pub fn new(material_id: u32) -> Self {
5414 Self {
5415 material_id,
5416 blend_weight: 1.0,
5417 uv_scale: Vec2::ONE,
5418 normal_intensity: 1.0,
5419 roughness: 0.8,
5420 metalness: 0.0,
5421 }
5422 }
5423}
5424
5425#[derive(Debug, Clone)]
5426pub struct TerrainMaterialBlender {
5427 pub layers: Vec<TerrainMaterialLayer>,
5428 pub splat_map: Vec<Vec4>, pub width: usize,
5430 pub height: usize,
5431}
5432
5433impl TerrainMaterialBlender {
5434 pub fn new(width: usize, height: usize) -> Self {
5435 Self {
5436 layers: Vec::new(),
5437 splat_map: vec![Vec4::new(1.0, 0.0, 0.0, 0.0); width * height],
5438 width,
5439 height,
5440 }
5441 }
5442
5443 pub fn add_layer(&mut self, layer: TerrainMaterialLayer) {
5444 self.layers.push(layer);
5445 }
5446
5447 pub fn set_splat(&mut self, x: usize, z: usize, weights: Vec4) {
5448 if x < self.width && z < self.height {
5449 let normalized = {
5450 let sum = weights.x + weights.y + weights.z + weights.w;
5451 if sum > 1e-8 { weights / sum } else { Vec4::new(1.0, 0.0, 0.0, 0.0) }
5452 };
5453 self.splat_map[z * self.width + x] = normalized;
5454 }
5455 }
5456
5457 pub fn get_splat(&self, x: usize, z: usize) -> Vec4 {
5458 if x < self.width && z < self.height {
5459 self.splat_map[z * self.width + x]
5460 } else {
5461 Vec4::new(1.0, 0.0, 0.0, 0.0)
5462 }
5463 }
5464
5465 pub fn sample_splat_bilinear(&self, fx: f32, fz: f32) -> Vec4 {
5466 let ix = fx.floor() as isize;
5467 let iz = fz.floor() as isize;
5468 let tx = fx - ix as f32;
5469 let tz = fz - iz as f32;
5470 let get = |x: isize, z: isize| -> Vec4 {
5471 let cx = x.clamp(0, self.width as isize - 1) as usize;
5472 let cz = z.clamp(0, self.height as isize - 1) as usize;
5473 self.splat_map[cz * self.width + cx]
5474 };
5475 let v00 = get(ix, iz );
5476 let v10 = get(ix + 1, iz );
5477 let v01 = get(ix, iz + 1);
5478 let v11 = get(ix + 1, iz + 1);
5479 let h0 = v00 * (1.0 - tx) + v10 * tx;
5480 let h1 = v01 * (1.0 - tx) + v11 * tx;
5481 h0 * (1.0 - tz) + h1 * tz
5482 }
5483
5484 pub fn blend_roughness(&self, splat: Vec4) -> f32 {
5485 let mut result = 0.0f32;
5486 for (i, layer) in self.layers.iter().enumerate().take(4) {
5487 let w = match i { 0 => splat.x, 1 => splat.y, 2 => splat.z, _ => splat.w };
5488 result += layer.roughness * w;
5489 }
5490 result
5491 }
5492
5493 pub fn blend_normal_intensity(&self, splat: Vec4) -> f32 {
5494 let mut result = 0.0f32;
5495 for (i, layer) in self.layers.iter().enumerate().take(4) {
5496 let w = match i { 0 => splat.x, 1 => splat.y, 2 => splat.z, _ => splat.w };
5497 result += layer.normal_intensity * w;
5498 }
5499 result
5500 }
5501
5502 pub fn dominant_material_at(&self, x: usize, z: usize) -> u32 {
5503 let splat = self.get_splat(x, z);
5504 let weights = [splat.x, splat.y, splat.z, splat.w];
5505 let max_idx = weights.iter().enumerate().max_by(|a, b| a.1.partial_cmp(b.1).unwrap()).map(|(i, _)| i).unwrap_or(0);
5506 self.layers.get(max_idx).map(|l| l.material_id).unwrap_or(0)
5507 }
5508}
5509
5510#[derive(Debug, Clone)]
5515pub struct ChunkNeighborManager {
5516 pub neighbor_cache: HashMap<ChunkCoord, [Option<ChunkCoord>; 6]>,
5517 pub loaded_set: HashSet<ChunkCoord>,
5518}
5519
5520impl ChunkNeighborManager {
5521 pub fn new() -> Self {
5522 Self {
5523 neighbor_cache: HashMap::new(),
5524 loaded_set: HashSet::new(),
5525 }
5526 }
5527
5528 pub fn register_loaded(&mut self, coord: ChunkCoord) {
5529 self.loaded_set.insert(coord);
5530 }
5531
5532 pub fn unregister(&mut self, coord: &ChunkCoord) {
5533 self.loaded_set.remove(coord);
5534 self.neighbor_cache.remove(coord);
5535 }
5536
5537 pub fn get_neighbors(&mut self, coord: &ChunkCoord) -> [Option<ChunkCoord>; 6] {
5538 if let Some(cached) = self.neighbor_cache.get(coord) {
5539 return *cached;
5540 }
5541 let n6 = coord.neighbors_6();
5542 let result: [Option<ChunkCoord>; 6] = std::array::from_fn(|i| {
5543 if self.loaded_set.contains(&n6[i]) { Some(n6[i].clone()) } else { None }
5544 });
5545 self.neighbor_cache.insert(coord.clone(), result);
5546 result
5547 }
5548
5549 pub fn all_neighbors_loaded(&self, coord: &ChunkCoord) -> bool {
5550 coord.neighbors_6().iter().all(|n| self.loaded_set.contains(n))
5551 }
5552
5553 pub fn loaded_neighbor_count(&self, coord: &ChunkCoord) -> usize {
5554 coord.neighbors_6().iter().filter(|n| self.loaded_set.contains(n)).count()
5555 }
5556
5557 pub fn needs_seam_update(&self, coord: &ChunkCoord, other: &ChunkCoord) -> bool {
5558 coord.is_adjacent(other) && self.loaded_set.contains(coord) && self.loaded_set.contains(other)
5559 }
5560
5561 pub fn invalidate_cache_for(&mut self, coord: &ChunkCoord) {
5562 self.neighbor_cache.remove(coord);
5563 for neighbor in coord.neighbors_26() {
5564 self.neighbor_cache.remove(&neighbor);
5565 }
5566 }
5567}
5568
5569#[derive(Debug, Clone)]
5574pub struct PrefetchSystem {
5575 pub velocity_buffer: VecDeque<Vec3>,
5576 pub velocity_history: usize,
5577 pub prefetch_distance: f32,
5578 pub prefetch_angle_deg: f32,
5579 pub predicted_position: Vec3,
5580 pub predicted_forward: Vec3,
5581 pub confidence: f32,
5582}
5583
5584impl PrefetchSystem {
5585 pub fn new(history: usize, prefetch_dist: f32) -> Self {
5586 Self {
5587 velocity_buffer: VecDeque::with_capacity(history),
5588 velocity_history: history,
5589 prefetch_distance: prefetch_dist,
5590 prefetch_angle_deg: 90.0,
5591 predicted_position: Vec3::ZERO,
5592 predicted_forward: Vec3::NEG_Z,
5593 confidence: 0.0,
5594 }
5595 }
5596
5597 pub fn update(&mut self, current_pos: Vec3, current_forward: Vec3, dt: f32) {
5598 if dt < 1e-6 { return; }
5599 if let Some(&prev_pos) = self.velocity_buffer.back() {
5600 let vel = (current_pos - prev_pos) / dt;
5601 self.velocity_buffer.push_back(vel);
5602 } else {
5603 self.velocity_buffer.push_back(Vec3::ZERO);
5604 }
5605 self.velocity_buffer.push_back(current_pos);
5607 while self.velocity_buffer.len() > self.velocity_history * 2 {
5608 self.velocity_buffer.pop_front();
5609 }
5610 self.compute_prediction(current_pos, current_forward, dt);
5611 }
5612
5613 fn compute_prediction(&mut self, pos: Vec3, forward: Vec3, dt: f32) {
5614 let n = self.velocity_buffer.len();
5616 if n < 2 {
5617 self.predicted_position = pos + forward * self.prefetch_distance;
5618 self.predicted_forward = forward;
5619 self.confidence = 0.1;
5620 return;
5621 }
5622 let mut avg_vel = Vec3::ZERO;
5623 let mut weight_sum = 0.0f32;
5624 let samples: Vec<_> = self.velocity_buffer.iter().cloned().collect();
5625 for i in 0..samples.len().saturating_sub(1) {
5626 let w = (i + 1) as f32;
5627 avg_vel += samples[i] * w;
5628 weight_sum += w;
5629 }
5630 if weight_sum > 0.0 { avg_vel /= weight_sum; }
5631 let speed = avg_vel.length();
5632 self.confidence = (speed * 0.1).clamp(0.0, 1.0);
5633 let lookahead_time = self.prefetch_distance / speed.max(1.0);
5634 self.predicted_position = pos + avg_vel * lookahead_time;
5635 self.predicted_forward = if speed > 0.01 { avg_vel.normalize() } else { forward };
5636 }
5637
5638 pub fn chunks_to_prefetch(&self, chunk_size: f32, extra_radius_chunks: i32) -> Vec<ChunkCoord> {
5639 let center = ChunkCoord::from_world_pos(self.predicted_position, chunk_size);
5640 ChunkCoord::chunks_in_radius(¢er, extra_radius_chunks)
5641 }
5642
5643 pub fn should_prefetch(&self, coord: &ChunkCoord, chunk_size: f32) -> bool {
5644 if self.confidence < 0.3 { return false; }
5645 let center = coord.to_world_center(chunk_size);
5646 let to_chunk = (center - self.predicted_position).normalize_or_zero();
5647 let angle_cos = self.predicted_forward.dot(to_chunk);
5648 angle_cos >= (self.prefetch_angle_deg.to_radians() * 0.5).cos()
5649 }
5650}
5651
5652#[derive(Debug, Clone)]
5657pub struct LevelStreamingVolume {
5658 pub id: u32,
5659 pub bounds: ChunkBounds,
5660 pub trigger_on_enter: bool,
5661 pub trigger_on_exit: bool,
5662 pub associated_chunks: Vec<ChunkCoord>,
5663 pub load_distance_override: Option<f32>,
5664 pub priority: f32,
5665 pub is_active: bool,
5666 pub last_state: bool,
5667}
5668
5669impl LevelStreamingVolume {
5670 pub fn new(id: u32, bounds: ChunkBounds) -> Self {
5671 Self {
5672 id,
5673 bounds,
5674 trigger_on_enter: true,
5675 trigger_on_exit: false,
5676 associated_chunks: Vec::new(),
5677 load_distance_override: None,
5678 priority: 1.0,
5679 is_active: false,
5680 last_state: false,
5681 }
5682 }
5683
5684 pub fn check_viewer(&mut self, viewer_pos: Vec3) -> Option<bool> {
5685 let inside = self.bounds.contains(viewer_pos);
5686 if inside != self.last_state {
5687 self.last_state = inside;
5688 Some(inside)
5689 } else {
5690 None
5691 }
5692 }
5693
5694 pub fn effective_load_distance(&self, base_distance: f32) -> f32 {
5695 self.load_distance_override.unwrap_or(base_distance) * self.priority
5696 }
5697
5698 pub fn contains_viewer(&self, viewer_pos: Vec3) -> bool {
5699 self.bounds.contains(viewer_pos)
5700 }
5701
5702 pub fn distance_to_viewer(&self, viewer_pos: Vec3) -> f32 {
5703 self.bounds.distance_to_point(viewer_pos)
5704 }
5705}
5706
5707#[derive(Debug, Clone)]
5708pub struct LevelStreamingVolumeManager {
5709 pub volumes: HashMap<u32, LevelStreamingVolume>,
5710 pub next_id: u32,
5711 pub active_volume_ids: HashSet<u32>,
5712}
5713
5714impl LevelStreamingVolumeManager {
5715 pub fn new() -> Self {
5716 Self {
5717 volumes: HashMap::new(),
5718 next_id: 1,
5719 active_volume_ids: HashSet::new(),
5720 }
5721 }
5722
5723 pub fn add_volume(&mut self, bounds: ChunkBounds) -> u32 {
5724 let id = self.next_id;
5725 self.next_id += 1;
5726 self.volumes.insert(id, LevelStreamingVolume::new(id, bounds));
5727 id
5728 }
5729
5730 pub fn remove_volume(&mut self, id: u32) {
5731 self.volumes.remove(&id);
5732 self.active_volume_ids.remove(&id);
5733 }
5734
5735 pub fn update_viewer(&mut self, viewer_pos: Vec3) -> Vec<(u32, bool)> {
5736 let mut events = Vec::new();
5737 for (id, volume) in self.volumes.iter_mut() {
5738 if let Some(entered) = volume.check_viewer(viewer_pos) {
5739 if entered {
5740 self.active_volume_ids.insert(*id);
5741 } else {
5742 self.active_volume_ids.remove(id);
5743 }
5744 events.push((*id, entered));
5745 }
5746 }
5747 events
5748 }
5749
5750 pub fn chunks_to_force_load(&self) -> Vec<(ChunkCoord, f32)> {
5751 let mut result = Vec::new();
5752 for id in &self.active_volume_ids {
5753 if let Some(vol) = self.volumes.get(id) {
5754 for coord in &vol.associated_chunks {
5755 result.push((coord.clone(), vol.priority));
5756 }
5757 }
5758 }
5759 result
5760 }
5761
5762 pub fn active_volume_count(&self) -> usize {
5763 self.active_volume_ids.len()
5764 }
5765}
5766
5767#[derive(Debug, Clone)]
5772pub struct RuntimeHistogram {
5773 pub buckets: Vec<u64>,
5774 pub min_val: f32,
5775 pub max_val: f32,
5776 pub total_samples: u64,
5777 pub sum: f64,
5778}
5779
5780impl RuntimeHistogram {
5781 pub fn new(num_buckets: usize, min_val: f32, max_val: f32) -> Self {
5782 Self {
5783 buckets: vec![0u64; num_buckets],
5784 min_val,
5785 max_val,
5786 total_samples: 0,
5787 sum: 0.0,
5788 }
5789 }
5790
5791 pub fn record(&mut self, value: f32) {
5792 let n = self.buckets.len();
5793 let range = self.max_val - self.min_val;
5794 if range <= 0.0 { return; }
5795 let idx = (((value - self.min_val) / range) * n as f32) as usize;
5796 let clamped = idx.min(n - 1);
5797 self.buckets[clamped] += 1;
5798 self.total_samples += 1;
5799 self.sum += value as f64;
5800 }
5801
5802 pub fn mean(&self) -> f64 {
5803 if self.total_samples == 0 { return 0.0; }
5804 self.sum / self.total_samples as f64
5805 }
5806
5807 pub fn percentile(&self, pct: f32) -> f32 {
5808 if self.total_samples == 0 { return self.min_val; }
5809 let target = (pct / 100.0 * self.total_samples as f32) as u64;
5810 let mut cumulative = 0u64;
5811 let n = self.buckets.len();
5812 let range = self.max_val - self.min_val;
5813 for (i, &count) in self.buckets.iter().enumerate() {
5814 cumulative += count;
5815 if cumulative >= target {
5816 return self.min_val + (i as f32 / n as f32) * range;
5817 }
5818 }
5819 self.max_val
5820 }
5821
5822 pub fn reset(&mut self) {
5823 for b in &mut self.buckets { *b = 0; }
5824 self.total_samples = 0;
5825 self.sum = 0.0;
5826 }
5827
5828 pub fn mode_bucket(&self) -> usize {
5829 self.buckets.iter().enumerate().max_by_key(|(_, &c)| c).map(|(i, _)| i).unwrap_or(0)
5830 }
5831
5832 pub fn mode_value(&self) -> f32 {
5833 let n = self.buckets.len();
5834 let range = self.max_val - self.min_val;
5835 self.min_val + (self.mode_bucket() as f32 / n as f32) * range
5836 }
5837}
5838
5839#[derive(Debug, Clone)]
5844pub struct ChunkHeader {
5845 pub magic: u32,
5846 pub version: u32,
5847 pub coord: ChunkCoord,
5848 pub data_size_bytes: u64,
5849 pub lod_count: u32,
5850 pub has_terrain: bool,
5851 pub has_collision: bool,
5852 pub has_nav: bool,
5853 pub object_count: u32,
5854 pub checksum: u32,
5855}
5856
5857impl ChunkHeader {
5858 pub const MAGIC: u32 = 0x43484E4B; pub const VERSION: u32 = 1;
5860
5861 pub fn new(coord: ChunkCoord) -> Self {
5862 Self {
5863 magic: Self::MAGIC,
5864 version: Self::VERSION,
5865 coord,
5866 data_size_bytes: 0,
5867 lod_count: 0,
5868 has_terrain: false,
5869 has_collision: false,
5870 has_nav: false,
5871 object_count: 0,
5872 checksum: 0,
5873 }
5874 }
5875
5876 pub fn is_valid(&self) -> bool {
5877 self.magic == Self::MAGIC && self.version <= Self::VERSION
5878 }
5879
5880 pub fn compute_checksum(&self) -> u32 {
5881 let packed = self.coord.pack_u64();
5882 let mut h = packed as u32;
5883 h ^= self.data_size_bytes as u32;
5884 h = h.wrapping_mul(0x9e3779b9);
5885 h ^= self.object_count;
5886 h = h.rotate_left(13);
5887 h
5888 }
5889
5890 pub fn validate_checksum(&self) -> bool {
5891 self.checksum == self.compute_checksum()
5892 }
5893
5894 pub fn finalize(&mut self) {
5895 self.checksum = self.compute_checksum();
5896 }
5897}
5898
5899#[derive(Debug, Clone)]
5904pub struct RenderVisibilityState {
5905 pub visible_chunks: Vec<ChunkCoord>,
5906 pub impostor_chunks: Vec<ChunkCoord>,
5907 pub hlod_clusters: Vec<u32>,
5908 pub total_triangles: u64,
5909 pub total_draw_calls: u32,
5910 pub culled_by_frustum: u32,
5911 pub culled_by_distance: u32,
5912 pub culled_by_occlusion: u32,
5913}
5914
5915impl RenderVisibilityState {
5916 pub fn new() -> Self {
5917 Self {
5918 visible_chunks: Vec::new(),
5919 impostor_chunks: Vec::new(),
5920 hlod_clusters: Vec::new(),
5921 total_triangles: 0,
5922 total_draw_calls: 0,
5923 culled_by_frustum: 0,
5924 culled_by_distance: 0,
5925 culled_by_occlusion: 0,
5926 }
5927 }
5928
5929 pub fn reset(&mut self) {
5930 self.visible_chunks.clear();
5931 self.impostor_chunks.clear();
5932 self.hlod_clusters.clear();
5933 self.total_triangles = 0;
5934 self.total_draw_calls = 0;
5935 self.culled_by_frustum = 0;
5936 self.culled_by_distance = 0;
5937 self.culled_by_occlusion = 0;
5938 }
5939
5940 pub fn total_culled(&self) -> u32 {
5941 self.culled_by_frustum + self.culled_by_distance + self.culled_by_occlusion
5942 }
5943
5944 pub fn visibility_ratio(&self, total_chunks: u32) -> f32 {
5945 if total_chunks == 0 { return 0.0; }
5946 self.visible_chunks.len() as f32 / total_chunks as f32
5947 }
5948}
5949
5950pub fn create_default_editor() -> WorldStreamingEditor {
5955 WorldStreamingEditor::new(StreamingConfig::default())
5956}
5957
5958pub fn create_editor_with_preset(preset: QualityPreset) -> WorldStreamingEditor {
5959 WorldStreamingEditor::new(StreamingConfig::with_quality_preset(preset))
5960}
5961
5962pub fn build_test_world(editor: &mut WorldStreamingEditor, actor_count: u32, world_size: f32) {
5963 for i in 0..actor_count {
5964 let angle = (i as f32 / actor_count as f32) * std::f32::consts::TAU;
5965 let radius = (i as f32 / actor_count as f32) * world_size * 0.5;
5966 let x = angle.cos() * radius;
5967 let z = angle.sin() * radius;
5968 let y = value_noise_2d(x * 0.01, z * 0.01) * 50.0;
5969 let pos = Vec3::new(x, y, z);
5970 let half = Vec3::splat(5.0 + (i % 10) as f32);
5971 let bounds = ChunkBounds::from_center_size(pos, half);
5972 let dist = 200.0 + (i % 5) as f32 * 100.0;
5973 editor.register_actor(i, pos, bounds, dist);
5974 }
5975 editor.build_octree();
5976 editor.build_bvh();
5977}
5978
5979pub fn run_streaming_simulation(editor: &mut WorldStreamingEditor, frames: u32, path: &[(Vec3, Vec3)]) {
5980 let step = if path.is_empty() { 0 } else { (frames as usize / path.len()).max(1) };
5981 for frame in 0..frames {
5982 let path_idx = (frame as usize / step).min(path.len().saturating_sub(1));
5983 let (pos, fwd) = if path.is_empty() { (Vec3::ZERO, Vec3::NEG_Z) } else { path[path_idx] };
5984 editor.set_viewer(pos, fwd);
5985 editor.tick(1.0 / 60.0, frame as u64 * 16667);
5986 }
5987}
5988
5989pub fn compute_streaming_coverage(editor: &WorldStreamingEditor) -> f32 {
5990 let loaded = editor.loaded_chunk_count();
5991 let total = editor.chunk_count();
5992 if total == 0 { return 0.0; }
5993 loaded as f32 / total as f32
5994}
5995
5996pub fn debug_print_lod_distribution(editor: &WorldStreamingEditor) -> HashMap<LodLevel, usize> {
5997 let mut dist: HashMap<LodLevel, usize> = HashMap::new();
5998 for chunk in editor.chunks.values() {
5999 *dist.entry(chunk.lod_level.clone()).or_insert(0) += 1;
6000 }
6001 dist
6002}
6003
6004pub fn compute_world_bounds_from_chunks(editor: &WorldStreamingEditor) -> Option<ChunkBounds> {
6005 let mut all_bounds: Option<ChunkBounds> = None;
6006 for chunk in editor.chunks.values() {
6007 all_bounds = Some(match all_bounds {
6008 None => chunk.bounds.clone(),
6009 Some(b) => b.merge(&chunk.bounds),
6010 });
6011 }
6012 all_bounds
6013}
6014
6015pub fn find_nearest_loaded_chunk(editor: &WorldStreamingEditor, pos: Vec3) -> Option<ChunkCoord> {
6016 editor.chunks.iter()
6017 .filter(|(_, c)| c.load_state == ChunkLoadState::Loaded)
6018 .min_by(|(_, a), (_, b)| {
6019 let da = a.bounds.distance_sq_to_point(pos);
6020 let db = b.bounds.distance_sq_to_point(pos);
6021 da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
6022 })
6023 .map(|(coord, _)| coord.clone())
6024}
6025
6026pub fn recompute_all_chunk_priorities(editor: &mut WorldStreamingEditor) {
6027 let viewer = editor.viewer_position;
6028 let forward = editor.viewer_forward;
6029 let coords: Vec<ChunkCoord> = editor.chunks.keys().cloned().collect();
6030 for coord in coords {
6031 if let Some(chunk) = editor.chunks.get_mut(&coord) {
6032 chunk.compute_load_priority(viewer, forward);
6033 }
6034 }
6035}
6036
6037pub fn estimate_total_world_memory(editor: &WorldStreamingEditor) -> u64 {
6038 editor.chunks.values()
6039 .filter(|c| c.load_state == ChunkLoadState::Loaded)
6040 .map(|c| c.memory_bytes)
6041 .sum()
6042}
6043
6044pub fn validate_chunk_bounds(bounds: &ChunkBounds) -> bool {
6049 !bounds.is_degenerate()
6050 && bounds.volume() > 0.0
6051 && bounds.surface_area() > 0.0
6052 && bounds.center() == (bounds.min + bounds.max) * 0.5
6053}
6054
6055pub fn validate_frustum_planes(planes: &[Vec4; 6]) -> bool {
6056 for plane in planes {
6057 let n = Vec3::new(plane.x, plane.y, plane.z);
6058 let len = n.length();
6059 if (len - 1.0).abs() > 0.01 { return false; }
6060 }
6061 true
6062}
6063
6064pub fn validate_bvh(node: &BvhNode) -> bool {
6065 if node.is_leaf() { return true; }
6066 let ok_left = node.left.as_ref().map(|l| l.bounds.intersects(&node.bounds) && validate_bvh(l)).unwrap_or(true);
6067 let ok_right = node.right.as_ref().map(|r| r.bounds.intersects(&node.bounds) && validate_bvh(r)).unwrap_or(true);
6068 ok_left && ok_right
6069}
6070
6071pub fn validate_lod_transitions(from: &LodLevel, to: &LodLevel) -> bool {
6072 let diff = (from.index() as isize - to.index() as isize).abs();
6074 diff <= 2
6075}
6076
6077pub fn stress_test_octree(count: u32, world_size: f32) -> (OctreeNode, usize) {
6078 let mut points = Vec::with_capacity(count as usize);
6079 for i in 0..count {
6080 let angle = (i as f32 / count as f32) * std::f32::consts::TAU;
6081 let r = (i as f32 / count as f32) * world_size * 0.5;
6082 let h = value_noise_2d(angle, r * 0.01) * 50.0;
6083 let pos = Vec3::new(angle.cos() * r, h, angle.sin() * r);
6084 points.push((i, pos));
6085 }
6086 let builder = OctreeBuilder::new(MAX_OCTREE_DEPTH, MAX_OBJECTS_PER_OCTREE_NODE);
6087 let tree = builder.build(&points);
6088 let node_count = tree.node_count();
6089 (tree, node_count)
6090}
6091
6092pub fn stress_test_bvh(count: u32, world_size: f32) -> (Option<BvhNode>, usize) {
6093 let mut objects = Vec::with_capacity(count as usize);
6094 for i in 0..count {
6095 let angle = (i as f32 / count as f32) * std::f32::consts::TAU;
6096 let r = (i as f32 / count as f32) * world_size * 0.5;
6097 let center = Vec3::new(angle.cos() * r, 0.0, angle.sin() * r);
6098 let half = Vec3::splat(5.0);
6099 let bounds = ChunkBounds::from_center_size(center, half);
6100 objects.push((i, bounds));
6101 }
6102 let builder = BvhBuilder::new(BVH_MAX_LEAF_OBJECTS, 16);
6103 let bvh = builder.build(&objects);
6104 let node_count = bvh.as_ref().map(|b| b.node_count()).unwrap_or(0);
6105 (bvh, node_count)
6106}
6107
6108pub fn benchmark_frustum_cull(
6109 frustum: &FrustumCulling,
6110 bounds_list: &[ChunkBounds],
6111) -> (usize, usize) {
6112 let mut visible = 0;
6113 let mut culled = 0;
6114 for bounds in bounds_list {
6115 if frustum.test_aabb_fast(bounds) {
6116 visible += 1;
6117 } else {
6118 culled += 1;
6119 }
6120 }
6121 (visible, culled)
6122}
6123
6124pub fn generate_grid_bounds(cols: usize, rows: usize, cell_size: f32) -> Vec<(ChunkCoord, ChunkBounds)> {
6125 let mut result = Vec::with_capacity(cols * rows);
6126 for z in 0..rows as i32 {
6127 for x in 0..cols as i32 {
6128 let coord = ChunkCoord::new(x, 0, z);
6129 let bounds = ChunkBounds::from_chunk_coord(&coord, cell_size);
6130 result.push((coord, bounds));
6131 }
6132 }
6133 result
6134}
6135
6136pub fn compute_lod_histogram(editor: &WorldStreamingEditor) -> [u32; 6] {
6137 let mut hist = [0u32; 6];
6138 for chunk in editor.chunks.values() {
6139 hist[chunk.lod_level.index()] += 1;
6140 }
6141 hist
6142}
6143
6144pub fn build_minimal_test_scene() -> WorldStreamingEditor {
6145 let mut editor = create_default_editor();
6146 let camera = StreamingCamera::new(
6147 Vec3::new(0.0, 100.0, 0.0),
6148 Vec3::ZERO,
6149 Vec3::Y,
6150 60.0,
6151 16.0 / 9.0,
6152 0.1,
6153 10000.0,
6154 );
6155 editor.set_camera(camera);
6156 editor.set_viewer(Vec3::new(0.0, 100.0, 0.0), Vec3::NEG_Z);
6157
6158 for z in -2i32..=2 {
6160 for x in -2i32..=2 {
6161 let coord = ChunkCoord::new(x, 0, z);
6162 editor.add_terrain_patch(coord, TERRAIN_PATCH_SIZE, 4.0);
6163 }
6164 }
6165
6166 build_test_world(&mut editor, 200, 2048.0);
6168 editor
6169}
6170
6171#[derive(Debug, Clone)]
6182pub struct ActorImportance {
6183 pub actor_id: u32,
6184 pub base_importance: f32,
6185 pub distance_falloff_exponent: f32,
6186 pub is_gameplay_relevant: bool,
6187 pub last_interaction_frame: u64,
6188 pub interaction_boost: f32,
6189 pub tag_boosts: HashMap<String, f32>,
6190}
6191
6192impl ActorImportance {
6193 pub fn new(actor_id: u32, base_importance: f32) -> Self {
6194 Self {
6195 actor_id,
6196 base_importance,
6197 distance_falloff_exponent: 2.0,
6198 is_gameplay_relevant: false,
6199 last_interaction_frame: 0,
6200 interaction_boost: 0.0,
6201 tag_boosts: HashMap::new(),
6202 }
6203 }
6204
6205 pub fn compute_importance(&self, distance: f32, current_frame: u64) -> f32 {
6206 let dist_factor = 1.0 / (1.0 + distance.powf(self.distance_falloff_exponent) * 0.0001);
6207 let gameplay_factor = if self.is_gameplay_relevant { 3.0 } else { 1.0 };
6208 let interaction_decay = {
6209 let age = current_frame.saturating_sub(self.last_interaction_frame) as f32;
6210 self.interaction_boost * (-age * 0.01).exp()
6211 };
6212 let tag_sum: f32 = self.tag_boosts.values().sum();
6213 (self.base_importance + interaction_boost_clamped(interaction_decay) + tag_sum)
6214 * dist_factor
6215 * gameplay_factor
6216 }
6217
6218 pub fn boost_interaction(&mut self, frame: u64, boost: f32) {
6219 self.last_interaction_frame = frame;
6220 self.interaction_boost = (self.interaction_boost + boost).min(10.0);
6221 }
6222
6223 pub fn add_tag_boost(&mut self, tag: String, value: f32) {
6224 *self.tag_boosts.entry(tag).or_insert(0.0) += value;
6225 }
6226
6227 pub fn remove_tag_boost(&mut self, tag: &str) {
6228 self.tag_boosts.remove(tag);
6229 }
6230
6231 pub fn effective_streaming_distance(&self, base_dist: f32, distance: f32, frame: u64) -> f32 {
6232 let importance = self.compute_importance(distance, frame);
6233 base_dist * importance.sqrt().clamp(0.5, 4.0)
6234 }
6235}
6236
6237fn interaction_boost_clamped(v: f32) -> f32 { v.clamp(0.0, 10.0) }
6238
6239#[derive(Debug, Clone)]
6244pub struct LoadingBudgetController {
6245 pub max_loads_per_frame: usize,
6246 pub max_unloads_per_frame: usize,
6247 pub target_frame_time_ms: f32,
6248 pub last_frame_time_ms: f32,
6249 pub smoothed_frame_time_ms: f32,
6250 pub smoothing_alpha: f32,
6251 pub overbudget_scale: f32,
6252 pub underbudget_scale: f32,
6253 pub min_loads: usize,
6254 pub max_loads_cap: usize,
6255}
6256
6257impl LoadingBudgetController {
6258 pub fn new(target_ms: f32, max_loads: usize) -> Self {
6259 Self {
6260 max_loads_per_frame: max_loads,
6261 max_unloads_per_frame: max_loads / 2,
6262 target_frame_time_ms: target_ms,
6263 last_frame_time_ms: target_ms,
6264 smoothed_frame_time_ms: target_ms,
6265 smoothing_alpha: 0.1,
6266 overbudget_scale: 0.7,
6267 underbudget_scale: 1.3,
6268 min_loads: 1,
6269 max_loads_cap: max_loads * 4,
6270 }
6271 }
6272
6273 pub fn update(&mut self, measured_frame_time_ms: f32) {
6274 self.last_frame_time_ms = measured_frame_time_ms;
6275 self.smoothed_frame_time_ms = self.smoothed_frame_time_ms * (1.0 - self.smoothing_alpha)
6276 + measured_frame_time_ms * self.smoothing_alpha;
6277 self.adjust_budget();
6278 }
6279
6280 fn adjust_budget(&mut self) {
6281 let ratio = self.smoothed_frame_time_ms / self.target_frame_time_ms;
6282 if ratio > 1.1 {
6283 let new_max = (self.max_loads_per_frame as f32 * self.overbudget_scale) as usize;
6285 self.max_loads_per_frame = new_max.max(self.min_loads);
6286 } else if ratio < 0.9 {
6287 let new_max = (self.max_loads_per_frame as f32 * self.underbudget_scale) as usize;
6289 self.max_loads_per_frame = new_max.min(self.max_loads_cap);
6290 }
6291 self.max_unloads_per_frame = self.max_loads_per_frame / 2;
6292 }
6293
6294 pub fn loads_allowed(&self) -> usize {
6295 self.max_loads_per_frame
6296 }
6297
6298 pub fn unloads_allowed(&self) -> usize {
6299 self.max_unloads_per_frame
6300 }
6301
6302 pub fn is_overbudget(&self) -> bool {
6303 self.smoothed_frame_time_ms > self.target_frame_time_ms * 1.1
6304 }
6305
6306 pub fn headroom_ms(&self) -> f32 {
6307 (self.target_frame_time_ms - self.smoothed_frame_time_ms).max(0.0)
6308 }
6309}
6310
6311#[derive(Debug, Clone)]
6316pub struct DistanceField2D {
6317 pub width: usize,
6318 pub height: usize,
6319 pub data: Vec<f32>,
6320 pub cell_size: f32,
6321 pub origin: Vec2,
6322}
6323
6324impl DistanceField2D {
6325 pub fn new(width: usize, height: usize, cell_size: f32, origin: Vec2) -> Self {
6326 Self {
6327 width,
6328 height,
6329 data: vec![f32::MAX; width * height],
6330 cell_size,
6331 origin,
6332 }
6333 }
6334
6335 pub fn set(&mut self, x: usize, y: usize, value: f32) {
6336 if x < self.width && y < self.height {
6337 self.data[y * self.width + x] = value;
6338 }
6339 }
6340
6341 pub fn get(&self, x: usize, y: usize) -> f32 {
6342 if x < self.width && y < self.height {
6343 self.data[y * self.width + x]
6344 } else {
6345 f32::MAX
6346 }
6347 }
6348
6349 pub fn sample(&self, world_x: f32, world_y: f32) -> f32 {
6350 let lx = (world_x - self.origin.x) / self.cell_size;
6351 let ly = (world_y - self.origin.y) / self.cell_size;
6352 let ix = lx.floor() as isize;
6353 let iy = ly.floor() as isize;
6354 let fx = lx - ix as f32;
6355 let fy = ly - iy as f32;
6356 let get_c = |x: isize, y: isize| -> f32 {
6357 let cx = x.clamp(0, self.width as isize - 1) as usize;
6358 let cy = y.clamp(0, self.height as isize - 1) as usize;
6359 self.data[cy * self.width + cx]
6360 };
6361 let v00 = get_c(ix, iy );
6362 let v10 = get_c(ix + 1, iy );
6363 let v01 = get_c(ix, iy + 1);
6364 let v11 = get_c(ix + 1, iy + 1);
6365 let h0 = v00 * (1.0 - fx) + v10 * fx;
6366 let h1 = v01 * (1.0 - fx) + v11 * fx;
6367 h0 * (1.0 - fy) + h1 * fy
6368 }
6369
6370 pub fn compute_from_obstacles(
6371 &mut self,
6372 obstacles: &[(f32, f32)], ) {
6374 for z in 0..self.height {
6375 for x in 0..self.width {
6376 let wx = self.origin.x + x as f32 * self.cell_size;
6377 let wy = self.origin.y + z as f32 * self.cell_size;
6378 let min_dist = obstacles.iter()
6379 .map(|(ox, oy)| ((wx - ox) * (wx - ox) + (wy - oy) * (wy - oy)).sqrt())
6380 .fold(f32::MAX, f32::min);
6381 self.data[z * self.width + x] = min_dist;
6382 }
6383 }
6384 }
6385
6386 pub fn gradient_at(&self, world_x: f32, world_y: f32) -> Vec2 {
6387 let eps = self.cell_size;
6388 let dx = (self.sample(world_x + eps, world_y) - self.sample(world_x - eps, world_y)) / (2.0 * eps);
6389 let dy = (self.sample(world_x, world_y + eps) - self.sample(world_x, world_y - eps)) / (2.0 * eps);
6390 Vec2::new(dx, dy)
6391 }
6392
6393 pub fn is_inside_obstacle(&self, world_x: f32, world_y: f32, threshold: f32) -> bool {
6394 self.sample(world_x, world_y) < threshold
6395 }
6396
6397 pub fn sweep_pass_horizontal(&mut self) {
6398 let w = self.width;
6399 let h = self.height;
6400 for y in 0..h {
6401 for x in 1..w {
6403 let prev = self.data[y * w + (x - 1)];
6404 if prev + self.cell_size < self.data[y * w + x] {
6405 self.data[y * w + x] = prev + self.cell_size;
6406 }
6407 }
6408 for x in (0..w - 1).rev() {
6410 let next = self.data[y * w + (x + 1)];
6411 if next + self.cell_size < self.data[y * w + x] {
6412 self.data[y * w + x] = next + self.cell_size;
6413 }
6414 }
6415 }
6416 }
6417
6418 pub fn sweep_pass_vertical(&mut self) {
6419 let w = self.width;
6420 let h = self.height;
6421 for x in 0..w {
6422 for y in 1..h {
6423 let prev = self.data[(y - 1) * w + x];
6424 if prev + self.cell_size < self.data[y * w + x] {
6425 self.data[y * w + x] = prev + self.cell_size;
6426 }
6427 }
6428 for y in (0..h - 1).rev() {
6429 let next = self.data[(y + 1) * w + x];
6430 if next + self.cell_size < self.data[y * w + x] {
6431 self.data[y * w + x] = next + self.cell_size;
6432 }
6433 }
6434 }
6435 }
6436
6437 pub fn fast_sweep(&mut self) {
6438 self.sweep_pass_horizontal();
6439 self.sweep_pass_vertical();
6440 self.sweep_pass_horizontal();
6441 }
6442}
6443
6444#[derive(Debug, Clone)]
6449pub struct RenderBatch {
6450 pub mesh_id: u32,
6451 pub lod_level: LodLevel,
6452 pub instance_data: Vec<Mat4>,
6453 pub bounds_union: ChunkBounds,
6454 pub material_id: u32,
6455 pub is_impostor: bool,
6456}
6457
6458impl RenderBatch {
6459 pub fn new(mesh_id: u32, lod: LodLevel, material_id: u32) -> Self {
6460 Self {
6461 mesh_id,
6462 lod_level: lod,
6463 instance_data: Vec::new(),
6464 bounds_union: ChunkBounds::new(Vec3::splat(f32::MAX), Vec3::splat(f32::MIN)),
6465 material_id,
6466 is_impostor: false,
6467 }
6468 }
6469
6470 pub fn add_instance(&mut self, transform: Mat4, bounds: &ChunkBounds) {
6471 self.instance_data.push(transform);
6472 self.bounds_union = self.bounds_union.merge(bounds);
6473 }
6474
6475 pub fn instance_count(&self) -> usize {
6476 self.instance_data.len()
6477 }
6478
6479 pub fn is_valid(&self) -> bool {
6480 !self.instance_data.is_empty()
6481 }
6482
6483 pub fn sort_back_to_front(&mut self, camera_pos: Vec3) {
6484 self.instance_data.sort_by(|a, b| {
6485 let pa = a.col(3).truncate();
6486 let pb = b.col(3).truncate();
6487 let da = (pa - camera_pos).length_squared();
6488 let db = (pb - camera_pos).length_squared();
6489 db.partial_cmp(&da).unwrap_or(std::cmp::Ordering::Equal)
6490 });
6491 }
6492
6493 pub fn sort_front_to_back(&mut self, camera_pos: Vec3) {
6494 self.instance_data.sort_by(|a, b| {
6495 let pa = a.col(3).truncate();
6496 let pb = b.col(3).truncate();
6497 let da = (pa - camera_pos).length_squared();
6498 let db = (pb - camera_pos).length_squared();
6499 da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
6500 });
6501 }
6502}
6503
6504#[derive(Debug, Clone)]
6505pub struct RenderBatchBuilder {
6506 pub batches: HashMap<u64, RenderBatch>,
6507 pub max_instances_per_batch: usize,
6508}
6509
6510impl RenderBatchBuilder {
6511 pub fn new(max_instances: usize) -> Self {
6512 Self {
6513 batches: HashMap::new(),
6514 max_instances_per_batch: max_instances,
6515 }
6516 }
6517
6518 pub fn add(&mut self, mesh_id: u32, lod: LodLevel, material_id: u32, transform: Mat4, bounds: &ChunkBounds) {
6519 let key = (mesh_id as u64) | ((material_id as u64) << 32) | ((lod.index() as u64) << 48);
6520 let batch = self.batches.entry(key).or_insert_with(|| RenderBatch::new(mesh_id, lod.clone(), material_id));
6521 if batch.instance_count() < self.max_instances_per_batch {
6522 batch.add_instance(transform, bounds);
6523 } else {
6524 let overflow_key = key ^ ((batch.instance_count() as u64) << 56);
6526 let ob = self.batches.entry(overflow_key).or_insert_with(|| RenderBatch::new(mesh_id, lod.clone(), material_id));
6527 ob.add_instance(transform, bounds);
6528 }
6529 }
6530
6531 pub fn build(&self) -> Vec<&RenderBatch> {
6532 let mut batches: Vec<_> = self.batches.values().filter(|b| b.is_valid()).collect();
6533 batches.sort_by_key(|b| b.material_id);
6535 batches
6536 }
6537
6538 pub fn clear(&mut self) {
6539 self.batches.clear();
6540 }
6541
6542 pub fn total_instances(&self) -> usize {
6543 self.batches.values().map(|b| b.instance_count()).sum()
6544 }
6545
6546 pub fn batch_count(&self) -> usize {
6547 self.batches.len()
6548 }
6549}
6550
6551#[derive(Debug, Clone)]
6556pub struct PatchStitcher {
6557 pub blend_region_cells: usize,
6558 pub use_geomorphing: bool,
6559 pub geomorph_distance_range: (f32, f32),
6560}
6561
6562impl PatchStitcher {
6563 pub fn new(blend_cells: usize, geomorph: bool, near: f32, far: f32) -> Self {
6564 Self {
6565 blend_region_cells: blend_cells,
6566 use_geomorphing: geomorph,
6567 geomorph_distance_range: (near, far),
6568 }
6569 }
6570
6571 pub fn compute_geomorph_alpha(&self, distance: f32) -> f32 {
6572 let (near, far) = self.geomorph_distance_range;
6573 smooth_step(near, far, distance)
6574 }
6575
6576 pub fn blend_heights(
6577 &self,
6578 h_fine: f32,
6579 h_coarse: f32,
6580 blend_alpha: f32,
6581 ) -> f32 {
6582 h_fine * (1.0 - blend_alpha) + h_coarse * blend_alpha
6583 }
6584
6585 pub fn compute_skirt_heights(
6586 &self,
6587 edge_heights: &[f32],
6588 skirt_depth: f32,
6589 ) -> Vec<f32> {
6590 edge_heights.iter().map(|&h| h - skirt_depth).collect()
6591 }
6592
6593 pub fn stitch_border(
6594 &self,
6595 patch: &mut TerrainHeightmap,
6596 side: usize,
6597 neighbor: &TerrainHeightmap,
6598 blend_alpha: f32,
6599 ) {
6600 let size = patch.width;
6601 match side {
6602 0 => { for z in 0..patch.height {
6604 let my_h = patch.get_height(size - 1, z);
6605 let nb_h = neighbor.get_height(0, z);
6606 let blended = my_h * (1.0 - blend_alpha) + nb_h * blend_alpha;
6607 patch.set_height(size - 1, z, blended);
6608 }
6609 }
6610 1 => { for z in 0..patch.height {
6612 let my_h = patch.get_height(0, z);
6613 let nb_h = neighbor.get_height(size - 1, z);
6614 let blended = my_h * (1.0 - blend_alpha) + nb_h * blend_alpha;
6615 patch.set_height(0, z, blended);
6616 }
6617 }
6618 2 => { for x in 0..patch.width {
6620 let my_h = patch.get_height(x, size - 1);
6621 let nb_h = neighbor.get_height(x, 0);
6622 let blended = my_h * (1.0 - blend_alpha) + nb_h * blend_alpha;
6623 patch.set_height(x, size - 1, blended);
6624 }
6625 }
6626 3 => { for x in 0..patch.width {
6628 let my_h = patch.get_height(x, 0);
6629 let nb_h = neighbor.get_height(x, size - 1);
6630 let blended = my_h * (1.0 - blend_alpha) + nb_h * blend_alpha;
6631 patch.set_height(x, 0, blended);
6632 }
6633 }
6634 _ => {}
6635 }
6636 }
6637
6638 pub fn compute_blend_weights_for_row(
6639 &self,
6640 row_len: usize,
6641 is_start: bool,
6642 ) -> Vec<f32> {
6643 let blend_count = self.blend_region_cells.min(row_len);
6644 let mut weights = vec![1.0f32; row_len];
6645 for i in 0..blend_count {
6646 let t = i as f32 / blend_count as f32;
6647 let w = if is_start { t } else { 1.0 - t };
6648 let idx = if is_start { i } else { row_len - 1 - i };
6649 weights[idx] = smooth_step(0.0, 1.0, w);
6650 }
6651 weights
6652 }
6653}
6654
6655#[derive(Debug, Clone)]
6660pub struct InstanceCullingPipeline {
6661 pub frustum: FrustumCulling,
6662 pub lod_calculator: StreamingDistanceCalculator,
6663 pub screen_error_metric: ScreenSpaceErrorMetric,
6664 pub max_instances: usize,
6665 pub culled_count: u32,
6666 pub passed_count: u32,
6667}
6668
6669impl InstanceCullingPipeline {
6670 pub fn new(
6671 frustum: FrustumCulling,
6672 screen_h: f32,
6673 fov_rad: f32,
6674 max_instances: usize,
6675 ) -> Self {
6676 Self {
6677 frustum,
6678 lod_calculator: StreamingDistanceCalculator::new(screen_h, fov_rad),
6679 screen_error_metric: ScreenSpaceErrorMetric::new(screen_h, fov_rad, SCREEN_SPACE_ERROR_THRESHOLD),
6680 max_instances,
6681 culled_count: 0,
6682 passed_count: 0,
6683 }
6684 }
6685
6686 pub fn cull_instances(
6687 &mut self,
6688 instances: &[(u32, Mat4, ChunkBounds)],
6689 camera_pos: Vec3,
6690 ) -> Vec<(u32, Mat4, LodLevel)> {
6691 self.culled_count = 0;
6692 self.passed_count = 0;
6693 let mut result = Vec::with_capacity(instances.len());
6694
6695 for (id, transform, bounds) in instances {
6696 if !self.frustum.test_aabb_fast(bounds) {
6697 self.culled_count += 1;
6698 continue;
6699 }
6700 let dist = bounds.distance_to_point(camera_pos);
6701 let lod = self.screen_error_metric.select_lod_for_bounds(bounds, camera_pos);
6702 if lod == LodLevel::Unloaded {
6703 self.culled_count += 1;
6704 continue;
6705 }
6706 result.push((*id, *transform, lod));
6707 self.passed_count += 1;
6708 if result.len() >= self.max_instances { break; }
6709 }
6710 result
6711 }
6712
6713 pub fn cull_ratio(&self) -> f32 {
6714 let total = self.culled_count + self.passed_count;
6715 if total == 0 { return 0.0; }
6716 self.culled_count as f32 / total as f32
6717 }
6718
6719 pub fn update_frustum(&mut self, view_proj: Mat4) {
6720 self.frustum = FrustumCulling::from_view_proj(view_proj);
6721 }
6722
6723 pub fn sort_by_distance_asc(
6724 instances: &mut Vec<(u32, Mat4, LodLevel)>,
6725 camera_pos: Vec3,
6726 ) {
6727 instances.sort_by(|(_, ta, _), (_, tb, _)| {
6728 let da = (ta.col(3).truncate() - camera_pos).length_squared();
6729 let db = (tb.col(3).truncate() - camera_pos).length_squared();
6730 da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
6731 });
6732 }
6733
6734 pub fn group_by_lod(
6735 instances: Vec<(u32, Mat4, LodLevel)>,
6736 ) -> HashMap<usize, Vec<(u32, Mat4)>> {
6737 let mut map: HashMap<usize, Vec<(u32, Mat4)>> = HashMap::new();
6738 for (id, t, lod) in instances {
6739 map.entry(lod.index()).or_insert_with(Vec::new).push((id, t));
6740 }
6741 map
6742 }
6743}
6744
6745#[derive(Debug, Clone)]
6750pub struct HeightfieldCollision {
6751 pub heightmap: TerrainHeightmap,
6752 pub friction: f32,
6753 pub restitution: f32,
6754 pub layer_mask: u32,
6755}
6756
6757impl HeightfieldCollision {
6758 pub fn new(heightmap: TerrainHeightmap) -> Self {
6759 Self {
6760 heightmap,
6761 friction: 0.7,
6762 restitution: 0.1,
6763 layer_mask: 0xFFFF_FFFF,
6764 }
6765 }
6766
6767 pub fn height_at_world(&self, x: f32, z: f32) -> f32 {
6768 self.heightmap.sample_bilinear(x, z)
6769 }
6770
6771 pub fn normal_at_world(&self, x: f32, z: f32) -> Vec3 {
6772 self.heightmap.compute_normal_bilinear(x, z)
6773 }
6774
6775 pub fn penetration_depth(&self, point: Vec3) -> f32 {
6776 let surface_h = self.height_at_world(point.x, point.z);
6777 (surface_h - point.y).max(0.0)
6778 }
6779
6780 pub fn resolve_sphere(
6781 &self,
6782 center: Vec3,
6783 radius: f32,
6784 ) -> Option<(Vec3, Vec3)> {
6785 let surf_h = self.height_at_world(center.x, center.z);
6786 let pen = surf_h + radius - center.y;
6787 if pen <= 0.0 { return None; }
6788 let normal = self.normal_at_world(center.x, center.z);
6789 let resolved = center + normal * pen;
6790 Some((resolved, normal))
6791 }
6792
6793 pub fn raycast(&self, origin: Vec3, dir: Vec3, max_dist: f32, steps: usize) -> Option<(Vec3, Vec3, f32)> {
6794 let dir_n = dir.normalize();
6795 let step_size = max_dist / steps as f32;
6796 for i in 0..=steps {
6797 let t = i as f32 * step_size;
6798 let p = origin + dir_n * t;
6799 let h = self.height_at_world(p.x, p.z);
6800 if p.y <= h {
6801 let normal = self.normal_at_world(p.x, p.z);
6802 return Some((p, normal, t));
6803 }
6804 }
6805 None
6806 }
6807
6808 pub fn slope_degrees_at(&self, x: f32, z: f32) -> f32 {
6809 self.heightmap.slope_at(x, z)
6810 }
6811
6812 pub fn is_walkable(&self, x: f32, z: f32, max_slope_degrees: f32) -> bool {
6813 self.slope_degrees_at(x, z) <= max_slope_degrees
6814 }
6815
6816 pub fn compute_contact_manifold(
6817 &self,
6818 sphere_center: Vec3,
6819 sphere_radius: f32,
6820 sample_radius: f32,
6821 samples: u32,
6822 ) -> Vec<(Vec3, Vec3, f32)> {
6823 let mut contacts = Vec::new();
6824 for i in 0..samples {
6825 let angle = (i as f32 / samples as f32) * std::f32::consts::TAU;
6826 let sx = sphere_center.x + angle.cos() * sample_radius;
6827 let sz = sphere_center.z + angle.sin() * sample_radius;
6828 let h = self.height_at_world(sx, sz);
6829 let contact_pt = Vec3::new(sx, h, sz);
6830 let pen = (sphere_center.y - sphere_radius) - h;
6831 if pen < 0.0 {
6832 let normal = self.normal_at_world(sx, sz);
6833 contacts.push((contact_pt, normal, pen.abs()));
6834 }
6835 }
6836 contacts
6837 }
6838}
6839
6840#[derive(Debug, Clone)]
6845pub struct AdaptiveLodController {
6846 pub target_fps: f32,
6847 pub current_fps: f32,
6848 pub global_lod_bias: f32,
6849 pub min_bias: f32,
6850 pub max_bias: f32,
6851 pub adjustment_speed: f32,
6852 pub history: VecDeque<f32>,
6853 pub history_size: usize,
6854 pub hysteresis: f32,
6855}
6856
6857impl AdaptiveLodController {
6858 pub fn new(target_fps: f32) -> Self {
6859 Self {
6860 target_fps,
6861 current_fps: target_fps,
6862 global_lod_bias: 0.0,
6863 min_bias: -1.0,
6864 max_bias: 2.0,
6865 adjustment_speed: 0.05,
6866 history: VecDeque::new(),
6867 history_size: 30,
6868 hysteresis: 5.0,
6869 }
6870 }
6871
6872 pub fn update(&mut self, measured_fps: f32) {
6873 self.current_fps = measured_fps;
6874 self.history.push_back(measured_fps);
6875 if self.history.len() > self.history_size {
6876 self.history.pop_front();
6877 }
6878 let avg_fps = self.history.iter().sum::<f32>() / self.history.len() as f32;
6879 let deficit = self.target_fps - avg_fps;
6880 if deficit > self.hysteresis {
6881 self.global_lod_bias = (self.global_lod_bias + self.adjustment_speed).min(self.max_bias);
6883 } else if deficit < -self.hysteresis {
6884 self.global_lod_bias = (self.global_lod_bias - self.adjustment_speed).max(self.min_bias);
6886 }
6887 }
6888
6889 pub fn adjusted_lod_distance(&self, base_dist: f32) -> f32 {
6890 base_dist * (1.0 - self.global_lod_bias * 0.2)
6891 }
6892
6893 pub fn adjusted_streaming_radius(&self, base_radius: f32) -> f32 {
6894 base_radius * (1.0 - self.global_lod_bias * 0.15).clamp(0.5, 1.5)
6895 }
6896
6897 pub fn is_struggling(&self) -> bool {
6898 self.current_fps < self.target_fps * 0.8
6899 }
6900
6901 pub fn quality_factor(&self) -> f32 {
6902 (1.0 - self.global_lod_bias / self.max_bias).clamp(0.0, 1.0)
6903 }
6904}
6905
6906#[derive(Debug, Clone, Default)]
6911pub struct RegionBitmask {
6912 pub bits: Vec<u64>,
6913 pub width: usize,
6914 pub height: usize,
6915}
6916
6917impl RegionBitmask {
6918 pub fn new(width: usize, height: usize) -> Self {
6919 let words = (width * height + 63) / 64;
6920 Self { bits: vec![0u64; words], width, height }
6921 }
6922
6923 pub fn set(&mut self, x: usize, y: usize) {
6924 if x < self.width && y < self.height {
6925 let idx = y * self.width + x;
6926 self.bits[idx / 64] |= 1u64 << (idx % 64);
6927 }
6928 }
6929
6930 pub fn clear(&mut self, x: usize, y: usize) {
6931 if x < self.width && y < self.height {
6932 let idx = y * self.width + x;
6933 self.bits[idx / 64] &= !(1u64 << (idx % 64));
6934 }
6935 }
6936
6937 pub fn get(&self, x: usize, y: usize) -> bool {
6938 if x < self.width && y < self.height {
6939 let idx = y * self.width + x;
6940 (self.bits[idx / 64] >> (idx % 64)) & 1 == 1
6941 } else {
6942 false
6943 }
6944 }
6945
6946 pub fn count_set(&self) -> usize {
6947 self.bits.iter().map(|w| w.count_ones() as usize).sum()
6948 }
6949
6950 pub fn or_with(&mut self, other: &RegionBitmask) {
6951 let len = self.bits.len().min(other.bits.len());
6952 for i in 0..len {
6953 self.bits[i] |= other.bits[i];
6954 }
6955 }
6956
6957 pub fn and_with(&mut self, other: &RegionBitmask) {
6958 let len = self.bits.len().min(other.bits.len());
6959 for i in 0..len {
6960 self.bits[i] &= other.bits[i];
6961 }
6962 }
6963
6964 pub fn invert(&mut self) {
6965 for word in &mut self.bits { *word = !*word; }
6966 let total = self.width * self.height;
6968 let last_bits = total % 64;
6969 if last_bits != 0 {
6970 if let Some(last) = self.bits.last_mut() {
6971 let mask = (1u64 << last_bits) - 1;
6972 *last &= mask;
6973 }
6974 }
6975 }
6976
6977 pub fn flood_fill(&mut self, start_x: usize, start_y: usize) {
6978 let mut stack = vec![(start_x, start_y)];
6979 while let Some((x, y)) = stack.pop() {
6980 if self.get(x, y) { continue; }
6981 self.set(x, y);
6982 if x > 0 { stack.push((x - 1, y)); }
6983 if x + 1 < self.width { stack.push((x + 1, y)); }
6984 if y > 0 { stack.push((x, y - 1)); }
6985 if y + 1 < self.height { stack.push((x, y + 1)); }
6986 }
6987 }
6988}
6989
6990#[derive(Debug, Clone)]
6995pub struct ChunkUpdateScheduler {
6996 pub update_queue: VecDeque<(ChunkCoord, u64)>, pub in_progress: HashSet<ChunkCoord>,
6998 pub completed_this_frame: Vec<ChunkCoord>,
6999 pub max_updates_per_frame: usize,
7000 pub update_interval_frames: u64,
7001 pub next_scheduled_frame: HashMap<ChunkCoord, u64>,
7002}
7003
7004impl ChunkUpdateScheduler {
7005 pub fn new(max_per_frame: usize, interval: u64) -> Self {
7006 Self {
7007 update_queue: VecDeque::new(),
7008 in_progress: HashSet::new(),
7009 completed_this_frame: Vec::new(),
7010 max_updates_per_frame: max_per_frame,
7011 update_interval_frames: interval,
7012 next_scheduled_frame: HashMap::new(),
7013 }
7014 }
7015
7016 pub fn schedule(&mut self, coord: ChunkCoord, current_frame: u64) {
7017 let next = *self.next_scheduled_frame.get(&coord).unwrap_or(&0);
7018 if current_frame >= next && !self.in_progress.contains(&coord) {
7019 self.update_queue.push_back((coord.clone(), current_frame));
7020 self.next_scheduled_frame.insert(coord, current_frame + self.update_interval_frames);
7021 }
7022 }
7023
7024 pub fn dispatch(&mut self, current_frame: u64) -> Vec<ChunkCoord> {
7025 let mut dispatched = Vec::new();
7026 let max = self.max_updates_per_frame;
7027 while dispatched.len() < max {
7028 if let Some((coord, frame)) = self.update_queue.pop_front() {
7029 if current_frame < frame + self.update_interval_frames * 2 {
7030 self.in_progress.insert(coord.clone());
7031 dispatched.push(coord);
7032 }
7033 } else {
7034 break;
7035 }
7036 }
7037 dispatched
7038 }
7039
7040 pub fn complete(&mut self, coord: ChunkCoord) {
7041 self.in_progress.remove(&coord);
7042 self.completed_this_frame.push(coord);
7043 }
7044
7045 pub fn end_frame(&mut self) {
7046 self.completed_this_frame.clear();
7047 }
7048
7049 pub fn pending_count(&self) -> usize {
7050 self.update_queue.len()
7051 }
7052
7053 pub fn in_progress_count(&self) -> usize {
7054 self.in_progress.len()
7055 }
7056
7057 pub fn reschedule_all_loaded(&mut self, loaded: &HashSet<ChunkCoord>, frame: u64) {
7058 for coord in loaded {
7059 self.schedule(coord.clone(), frame);
7060 }
7061 }
7062}
7063
7064#[derive(Debug, Clone)]
7069pub struct ImpostorCaptureJob {
7070 pub actor_id: u32,
7071 pub world_bounds: ChunkBounds,
7072 pub num_views: u32,
7073 pub atlas_slot: u32,
7074 pub is_complete: bool,
7075 pub capture_frame: u64,
7076 pub view_directions: Vec<Vec3>,
7077}
7078
7079impl ImpostorCaptureJob {
7080 pub fn new(actor_id: u32, bounds: ChunkBounds, num_views: u32, slot: u32) -> Self {
7081 let view_directions = Self::compute_view_directions(num_views);
7082 Self {
7083 actor_id,
7084 world_bounds: bounds,
7085 num_views,
7086 atlas_slot: slot,
7087 is_complete: false,
7088 capture_frame: 0,
7089 view_directions,
7090 }
7091 }
7092
7093 fn compute_view_directions(num_views: u32) -> Vec<Vec3> {
7094 (0..num_views)
7095 .map(|i| {
7096 let angle = (i as f32 / num_views as f32) * std::f32::consts::TAU;
7097 Vec3::new(angle.cos(), 0.0, angle.sin())
7098 })
7099 .collect()
7100 }
7101
7102 pub fn view_matrix_for_view(&self, view_idx: u32) -> Mat4 {
7103 if view_idx as usize >= self.view_directions.len() {
7104 return Mat4::IDENTITY;
7105 }
7106 let dir = self.view_directions[view_idx as usize];
7107 let center = self.world_bounds.center();
7108 let radius = self.world_bounds.half_size().length() * 2.0;
7109 let eye = center - dir * radius;
7110 Mat4::look_at_rh(eye, center, Vec3::Y)
7111 }
7112
7113 pub fn atlas_uv_for_view(&self, view_idx: u32) -> (Vec2, Vec2) {
7114 let cols = IMPOSTOR_ATLAS_COLS;
7115 let rows = IMPOSTOR_ATLAS_ROWS;
7116 let slot_col = (self.atlas_slot % cols) as f32;
7117 let slot_row = (self.atlas_slot / cols) as f32;
7118 let view_col = (view_idx % cols) as f32;
7119 let view_row = (view_idx / cols) as f32;
7120 let cell_w = 1.0 / cols as f32;
7121 let cell_h = 1.0 / rows as f32;
7122 let _ = (slot_col, slot_row); let uv_min = Vec2::new(view_col * cell_w, view_row * cell_h);
7124 let uv_max = uv_min + Vec2::new(cell_w, cell_h);
7125 (uv_min, uv_max)
7126 }
7127
7128 pub fn mark_complete(&mut self, frame: u64) {
7129 self.is_complete = true;
7130 self.capture_frame = frame;
7131 }
7132}
7133
7134#[derive(Debug, Clone)]
7135pub struct ImpostorCaptureQueue {
7136 pub pending: VecDeque<ImpostorCaptureJob>,
7137 pub in_progress: Option<ImpostorCaptureJob>,
7138 pub next_atlas_slot: u32,
7139 pub max_atlas_slots: u32,
7140 pub completed_jobs: Vec<ImpostorCaptureJob>,
7141}
7142
7143impl ImpostorCaptureQueue {
7144 pub fn new(max_slots: u32) -> Self {
7145 Self {
7146 pending: VecDeque::new(),
7147 in_progress: None,
7148 next_atlas_slot: 0,
7149 max_atlas_slots: max_slots,
7150 completed_jobs: Vec::new(),
7151 }
7152 }
7153
7154 pub fn request_capture(&mut self, actor_id: u32, bounds: ChunkBounds, num_views: u32) -> Option<u32> {
7155 if self.next_atlas_slot >= self.max_atlas_slots { return None; }
7156 let slot = self.next_atlas_slot;
7157 self.next_atlas_slot += 1;
7158 let job = ImpostorCaptureJob::new(actor_id, bounds, num_views, slot);
7159 self.pending.push_back(job);
7160 Some(slot)
7161 }
7162
7163 pub fn tick(&mut self, frame: u64) -> Option<&ImpostorCaptureJob> {
7164 if self.in_progress.is_none() {
7165 self.in_progress = self.pending.pop_front();
7166 }
7167 if let Some(ref mut job) = self.in_progress {
7168 if !job.is_complete {
7169 job.mark_complete(frame);
7170 let completed = self.in_progress.take().unwrap();
7171 self.completed_jobs.push(completed);
7172 }
7173 }
7174 self.completed_jobs.last()
7175 }
7176
7177 pub fn drain_completed(&mut self) -> Vec<ImpostorCaptureJob> {
7178 std::mem::take(&mut self.completed_jobs)
7179 }
7180
7181 pub fn pending_count(&self) -> usize {
7182 self.pending.len()
7183 }
7184}
7185
7186#[derive(Debug, Clone)]
7191pub struct SectorStreamingMap {
7192 pub sectors: HashMap<ChunkCoord, SectorInfo>,
7193 pub sector_size_chunks: u32,
7194 pub chunk_size: f32,
7195}
7196
7197#[derive(Debug, Clone)]
7198pub struct SectorInfo {
7199 pub coord: ChunkCoord,
7200 pub chunks: Vec<ChunkCoord>,
7201 pub is_loaded: bool,
7202 pub priority: f32,
7203 pub load_order: u32,
7204 pub memory_estimate_mb: f32,
7205 pub last_resident_frame: u64,
7206}
7207
7208impl SectorInfo {
7209 pub fn new(coord: ChunkCoord, sector_size: u32) -> Self {
7210 let mut chunks = Vec::new();
7211 let size = sector_size as i32;
7212 for dz in 0..size {
7213 for dx in 0..size {
7214 chunks.push(ChunkCoord::new(
7215 coord.x * size + dx,
7216 coord.y,
7217 coord.z * size + dz,
7218 ));
7219 }
7220 }
7221 Self {
7222 coord,
7223 chunks,
7224 is_loaded: false,
7225 priority: 0.0,
7226 load_order: 0,
7227 memory_estimate_mb: sector_size as f32 * sector_size as f32 * 8.0,
7228 last_resident_frame: 0,
7229 }
7230 }
7231
7232 pub fn chunk_count(&self) -> usize {
7233 self.chunks.len()
7234 }
7235}
7236
7237impl SectorStreamingMap {
7238 pub fn new(sector_size_chunks: u32, chunk_size: f32) -> Self {
7239 Self {
7240 sectors: HashMap::new(),
7241 sector_size_chunks,
7242 chunk_size,
7243 }
7244 }
7245
7246 pub fn world_pos_to_sector(&self, pos: Vec3) -> ChunkCoord {
7247 let chunk = ChunkCoord::from_world_pos(pos, self.chunk_size);
7248 let size = self.sector_size_chunks as i32;
7249 ChunkCoord::new(
7250 chunk.x.div_euclid(size),
7251 chunk.y.div_euclid(size),
7252 chunk.z.div_euclid(size),
7253 )
7254 }
7255
7256 pub fn get_or_create_sector(&mut self, coord: ChunkCoord) -> &mut SectorInfo {
7257 let size = self.sector_size_chunks;
7258 self.sectors.entry(coord.clone()).or_insert_with(|| SectorInfo::new(coord, size))
7259 }
7260
7261 pub fn sectors_in_radius(&self, center_pos: Vec3, radius_m: f32) -> Vec<ChunkCoord> {
7262 let sector_size_m = self.sector_size_chunks as f32 * self.chunk_size;
7263 let center_sector = self.world_pos_to_sector(center_pos);
7264 let radius_sectors = (radius_m / sector_size_m).ceil() as i32 + 1;
7265 let mut result = Vec::new();
7266 for dz in -radius_sectors..=radius_sectors {
7267 for dx in -radius_sectors..=radius_sectors {
7268 let coord = center_sector.offset(dx, 0, dz);
7269 let world_center = Vec3::new(
7270 (coord.x as f32 + 0.5) * sector_size_m,
7271 center_pos.y,
7272 (coord.z as f32 + 0.5) * sector_size_m,
7273 );
7274 if (world_center - center_pos).length() <= radius_m + sector_size_m {
7275 result.push(coord);
7276 }
7277 }
7278 }
7279 result
7280 }
7281
7282 pub fn compute_sector_priorities(&mut self, viewer_pos: Vec3) {
7283 let chunk_size = self.chunk_size;
7284 let sector_size = self.sector_size_chunks;
7285 for (coord, info) in self.sectors.iter_mut() {
7286 let sector_world = Vec3::new(
7287 (coord.x as f32 + 0.5) * sector_size as f32 * chunk_size,
7288 viewer_pos.y,
7289 (coord.z as f32 + 0.5) * sector_size as f32 * chunk_size,
7290 );
7291 let dist = (sector_world - viewer_pos).length();
7292 info.priority = 1.0 / (1.0 + dist * 0.001);
7293 }
7294 }
7295
7296 pub fn loaded_sector_count(&self) -> usize {
7297 self.sectors.values().filter(|s| s.is_loaded).count()
7298 }
7299
7300 pub fn total_sector_count(&self) -> usize {
7301 self.sectors.len()
7302 }
7303
7304 pub fn total_memory_estimate_mb(&self) -> f32 {
7305 self.sectors.values().filter(|s| s.is_loaded).map(|s| s.memory_estimate_mb).sum()
7306 }
7307}
7308
7309#[derive(Debug, Clone)]
7314pub struct WorldBoundsTracker {
7315 pub world_bounds: ChunkBounds,
7316 pub occupied_cells: HashSet<ChunkCoord>,
7317 pub cell_size: f32,
7318 pub total_actors: u32,
7319 pub dirty: bool,
7320}
7321
7322impl WorldBoundsTracker {
7323 pub fn new(cell_size: f32) -> Self {
7324 Self {
7325 world_bounds: ChunkBounds::new(Vec3::ZERO, Vec3::ZERO),
7326 occupied_cells: HashSet::new(),
7327 cell_size,
7328 total_actors: 0,
7329 dirty: false,
7330 }
7331 }
7332
7333 pub fn register(&mut self, pos: Vec3) {
7334 let coord = ChunkCoord::from_world_pos(pos, self.cell_size);
7335 self.occupied_cells.insert(coord);
7336 self.total_actors += 1;
7337 self.dirty = true;
7338 }
7339
7340 pub fn recompute_bounds(&mut self) {
7341 if !self.dirty { return; }
7342 let mut min = Vec3::splat(f32::MAX);
7343 let mut max = Vec3::splat(f32::MIN);
7344 for coord in &self.occupied_cells {
7345 let cell_min = coord.to_world_min(self.cell_size);
7346 let cell_max = cell_min + Vec3::splat(self.cell_size);
7347 min = min.min(cell_min);
7348 max = max.max(cell_max);
7349 }
7350 if min.x <= max.x {
7351 self.world_bounds = ChunkBounds { min, max };
7352 }
7353 self.dirty = false;
7354 }
7355
7356 pub fn center(&mut self) -> Vec3 {
7357 self.recompute_bounds();
7358 self.world_bounds.center()
7359 }
7360
7361 pub fn extents(&mut self) -> Vec3 {
7362 self.recompute_bounds();
7363 self.world_bounds.size()
7364 }
7365
7366 pub fn is_point_in_world(&self, pos: Vec3) -> bool {
7367 self.world_bounds.contains(pos)
7368 }
7369
7370 pub fn cell_count(&self) -> usize {
7371 self.occupied_cells.len()
7372 }
7373}
7374
7375#[derive(Debug, Clone)]
7380pub struct StreamingLevel {
7381 pub id: u32,
7382 pub name: String,
7383 pub bounds: ChunkBounds,
7384 pub chunks: Vec<ChunkCoord>,
7385 pub load_state: ChunkLoadState,
7386 pub is_persistent: bool,
7387 pub min_streaming_distance: f32,
7388 pub max_streaming_distance: f32,
7389 pub priority: f32,
7390}
7391
7392impl StreamingLevel {
7393 pub fn new(id: u32, name: String, bounds: ChunkBounds) -> Self {
7394 Self {
7395 id,
7396 name,
7397 bounds,
7398 chunks: Vec::new(),
7399 load_state: ChunkLoadState::Unloaded,
7400 is_persistent: false,
7401 min_streaming_distance: 0.0,
7402 max_streaming_distance: 2048.0,
7403 priority: 1.0,
7404 }
7405 }
7406
7407 pub fn should_load(&self, viewer_pos: Vec3) -> bool {
7408 let dist = self.bounds.distance_to_point(viewer_pos);
7409 dist >= self.min_streaming_distance && dist <= self.max_streaming_distance
7410 }
7411
7412 pub fn should_unload(&self, viewer_pos: Vec3) -> bool {
7413 if self.is_persistent { return false; }
7414 let dist = self.bounds.distance_to_point(viewer_pos);
7415 dist > self.max_streaming_distance * 1.2
7416 }
7417
7418 pub fn chunk_count(&self) -> usize {
7419 self.chunks.len()
7420 }
7421}
7422
7423#[derive(Debug, Clone)]
7424pub struct StreamingLevelManager {
7425 pub levels: HashMap<u32, StreamingLevel>,
7426 pub next_id: u32,
7427 pub loaded_levels: HashSet<u32>,
7428 pub pending_load: HashSet<u32>,
7429 pub pending_unload: HashSet<u32>,
7430}
7431
7432impl StreamingLevelManager {
7433 pub fn new() -> Self {
7434 Self {
7435 levels: HashMap::new(),
7436 next_id: 1,
7437 loaded_levels: HashSet::new(),
7438 pending_load: HashSet::new(),
7439 pending_unload: HashSet::new(),
7440 }
7441 }
7442
7443 pub fn register_level(&mut self, name: String, bounds: ChunkBounds) -> u32 {
7444 let id = self.next_id;
7445 self.next_id += 1;
7446 self.levels.insert(id, StreamingLevel::new(id, name, bounds));
7447 id
7448 }
7449
7450 pub fn update_streaming(&mut self, viewer_pos: Vec3) {
7451 self.pending_load.clear();
7452 self.pending_unload.clear();
7453
7454 for (id, level) in &self.levels {
7455 if self.loaded_levels.contains(id) {
7456 if level.should_unload(viewer_pos) {
7457 self.pending_unload.insert(*id);
7458 }
7459 } else {
7460 if level.should_load(viewer_pos) {
7461 self.pending_load.insert(*id);
7462 }
7463 }
7464 }
7465 }
7466
7467 pub fn commit_loads(&mut self) {
7468 for id in self.pending_load.drain().collect::<Vec<_>>() {
7469 self.loaded_levels.insert(id);
7470 if let Some(level) = self.levels.get_mut(&id) {
7471 level.load_state = ChunkLoadState::Loaded;
7472 }
7473 }
7474 }
7475
7476 pub fn commit_unloads(&mut self) {
7477 for id in self.pending_unload.drain().collect::<Vec<_>>() {
7478 self.loaded_levels.remove(&id);
7479 if let Some(level) = self.levels.get_mut(&id) {
7480 level.load_state = ChunkLoadState::Unloaded;
7481 }
7482 }
7483 }
7484
7485 pub fn chunks_for_loaded_levels(&self) -> Vec<ChunkCoord> {
7486 self.loaded_levels.iter()
7487 .filter_map(|id| self.levels.get(id))
7488 .flat_map(|l| l.chunks.iter().cloned())
7489 .collect()
7490 }
7491
7492 pub fn loaded_level_count(&self) -> usize {
7493 self.loaded_levels.len()
7494 }
7495
7496 pub fn total_level_count(&self) -> usize {
7497 self.levels.len()
7498 }
7499}
7500
7501#[derive(Debug, Clone)]
7506pub struct Portal {
7507 pub id: u32,
7508 pub from_chunk: ChunkCoord,
7509 pub to_chunk: ChunkCoord,
7510 pub center: Vec3,
7511 pub normal: Vec3,
7512 pub half_extents: Vec2,
7513 pub is_open: bool,
7514}
7515
7516impl Portal {
7517 pub fn new(id: u32, from: ChunkCoord, to: ChunkCoord, center: Vec3, normal: Vec3, half_extents: Vec2) -> Self {
7518 Self {
7519 id,
7520 from_chunk: from,
7521 to_chunk: to,
7522 center,
7523 normal,
7524 half_extents,
7525 is_open: true,
7526 }
7527 }
7528
7529 pub fn is_visible_from(&self, viewer_pos: Vec3, viewer_forward: Vec3) -> bool {
7530 if !self.is_open { return false; }
7531 let to_portal = (self.center - viewer_pos).normalize_or_zero();
7532 let facing = viewer_forward.dot(to_portal);
7533 let normal_facing = self.normal.dot(to_portal);
7534 facing > -0.5 && normal_facing < 0.1
7535 }
7536
7537 pub fn bounds_2d(&self) -> [Vec3; 4] {
7538 let right = Vec3::new(-self.normal.z, 0.0, self.normal.x).normalize_or_zero();
7539 let up = Vec3::Y;
7540 let c = self.center;
7541 let hw = right * self.half_extents.x;
7542 let hh = up * self.half_extents.y;
7543 [c - hw - hh, c + hw - hh, c + hw + hh, c - hw + hh]
7544 }
7545
7546 pub fn project_to_screen_rect(
7547 &self,
7548 view_proj: Mat4,
7549 ) -> Option<(Vec2, Vec2)> {
7550 let corners = self.bounds_2d();
7551 let mut min_ndc = Vec2::splat(f32::MAX);
7552 let mut max_ndc = Vec2::splat(f32::MIN);
7553 for corner in &corners {
7554 let proj = view_proj.project_point3(*corner);
7555 let ndc = Vec2::new(proj.x, proj.y);
7556 min_ndc = min_ndc.min(ndc);
7557 max_ndc = max_ndc.max(ndc);
7558 }
7559 if min_ndc.x > 1.0 || max_ndc.x < -1.0 || min_ndc.y > 1.0 || max_ndc.y < -1.0 {
7560 None
7561 } else {
7562 Some((min_ndc, max_ndc))
7563 }
7564 }
7565}
7566
7567#[derive(Debug, Clone)]
7568pub struct PortalVisibilitySystem {
7569 pub portals: HashMap<u32, Portal>,
7570 pub chunk_portals: HashMap<ChunkCoord, Vec<u32>>,
7571 pub next_id: u32,
7572 pub visible_chunks: HashSet<ChunkCoord>,
7573}
7574
7575impl PortalVisibilitySystem {
7576 pub fn new() -> Self {
7577 Self {
7578 portals: HashMap::new(),
7579 chunk_portals: HashMap::new(),
7580 next_id: 1,
7581 visible_chunks: HashSet::new(),
7582 }
7583 }
7584
7585 pub fn add_portal(&mut self, from: ChunkCoord, to: ChunkCoord, center: Vec3, normal: Vec3, half: Vec2) -> u32 {
7586 let id = self.next_id;
7587 self.next_id += 1;
7588 let portal = Portal::new(id, from.clone(), to.clone(), center, normal, half);
7589 self.portals.insert(id, portal);
7590 self.chunk_portals.entry(from).or_insert_with(Vec::new).push(id);
7591 id
7592 }
7593
7594 pub fn compute_visibility(
7595 &mut self,
7596 viewer_chunk: &ChunkCoord,
7597 viewer_pos: Vec3,
7598 viewer_forward: Vec3,
7599 max_depth: usize,
7600 ) {
7601 self.visible_chunks.clear();
7602 self.visible_chunks.insert(viewer_chunk.clone());
7603 let mut to_visit = vec![(viewer_chunk.clone(), 0usize)];
7604 while let Some((current, depth)) = to_visit.pop() {
7605 if depth >= max_depth { continue; }
7606 if let Some(portal_ids) = self.chunk_portals.get(¤t).cloned() {
7607 for pid in portal_ids {
7608 if let Some(portal) = self.portals.get(&pid) {
7609 if portal.is_visible_from(viewer_pos, viewer_forward) {
7610 let dest = portal.to_chunk.clone();
7611 if self.visible_chunks.insert(dest.clone()) {
7612 to_visit.push((dest, depth + 1));
7613 }
7614 }
7615 }
7616 }
7617 }
7618 }
7619 }
7620
7621 pub fn is_chunk_visible(&self, coord: &ChunkCoord) -> bool {
7622 self.visible_chunks.contains(coord)
7623 }
7624
7625 pub fn visible_chunk_count(&self) -> usize {
7626 self.visible_chunks.len()
7627 }
7628}
7629
7630pub fn compute_chunk_lod_blend_weights(
7635 chunk: &StreamingChunk,
7636 camera: &StreamingCamera,
7637 config: &StreamingConfig,
7638 screen_height: f32,
7639) -> (LodLevel, LodLevel, f32) {
7640 let dist = chunk.distance_to_viewer;
7641 let desired = config.lod_for_distance(dist);
7642 let blend_near = if desired.index() > 0 {
7643 config.lod_distances[desired.index().saturating_sub(1)]
7644 } else {
7645 0.0
7646 };
7647 let blend_far = if desired.index() < 5 {
7648 config.lod_distances[desired.index().min(4)]
7649 } else {
7650 config.streaming_radius
7651 };
7652 let alpha = smooth_step(blend_near, blend_far, dist);
7653 (desired.next_lower(), desired.clone(), alpha)
7654}
7655
7656pub fn compute_dynamic_streaming_radius(
7657 base_radius: f32,
7658 memory_pressure: f32,
7659 fps_scale: f32,
7660) -> f32 {
7661 let mem_scale = (1.0 - memory_pressure * 0.5).clamp(0.4, 1.0);
7662 let fps_factor = fps_scale.clamp(0.5, 1.5);
7663 base_radius * mem_scale * fps_factor
7664}
7665
7666pub fn clamp_viewer_to_world_bounds(viewer: Vec3, world: &ChunkBounds) -> Vec3 {
7667 viewer.clamp(world.min, world.max)
7668}
7669
7670pub fn world_to_chunk_grid(pos: Vec3, chunk_size: f32) -> (i32, i32, i32) {
7671 (
7672 (pos.x / chunk_size).floor() as i32,
7673 (pos.y / chunk_size).floor() as i32,
7674 (pos.z / chunk_size).floor() as i32,
7675 )
7676}
7677
7678pub fn chunk_grid_to_world_center(cx: i32, cy: i32, cz: i32, chunk_size: f32) -> Vec3 {
7679 Vec3::new(
7680 (cx as f32 + 0.5) * chunk_size,
7681 (cy as f32 + 0.5) * chunk_size,
7682 (cz as f32 + 0.5) * chunk_size,
7683 )
7684}
7685
7686pub fn compute_level_streaming_priority(
7687 level_bounds: &ChunkBounds,
7688 viewer_pos: Vec3,
7689 viewer_velocity: Vec3,
7690 lookahead_t: f32,
7691) -> f32 {
7692 let predicted = viewer_pos + viewer_velocity * lookahead_t;
7693 let dist_current = level_bounds.distance_to_point(viewer_pos);
7694 let dist_predicted = level_bounds.distance_to_point(predicted);
7695 let approach_rate = (dist_current - dist_predicted) / lookahead_t.max(0.001);
7696 let base = 1.0 / (1.0 + dist_current * 0.001);
7697 let approach_bonus = approach_rate.max(0.0) * 0.01;
7698 (base + approach_bonus).clamp(0.0, 1.0)
7699}
7700
7701pub fn estimate_lod_memory_total(
7702 chunks: &HashMap<ChunkCoord, StreamingChunk>,
7703 base_chunk_memory_mb: f32,
7704) -> f32 {
7705 chunks.values()
7706 .filter(|c| c.lod_level.is_loaded())
7707 .map(|c| base_chunk_memory_mb * c.lod_level.memory_multiplier())
7708 .sum()
7709}
7710
7711pub fn compute_lod_switch_hysteresis(
7712 current_lod: &LodLevel,
7713 desired_lod: &LodLevel,
7714 base_dist: f32,
7715 hysteresis_fraction: f32,
7716) -> f32 {
7717 if current_lod == desired_lod { return base_dist; }
7718 if current_lod < desired_lod {
7719 base_dist * (1.0 + hysteresis_fraction)
7720 } else {
7721 base_dist * (1.0 - hysteresis_fraction)
7722 }
7723}
7724
7725pub fn build_lod_distance_array(base_dist: f32, scale_factor: f32) -> [f32; 5] {
7726 [
7727 base_dist,
7728 base_dist * scale_factor,
7729 base_dist * scale_factor * scale_factor,
7730 base_dist * scale_factor * scale_factor * scale_factor,
7731 base_dist * scale_factor * scale_factor * scale_factor * scale_factor,
7732 ]
7733}
7734
7735pub fn compute_per_frame_memory_delta(
7736 prev_loaded: &HashSet<ChunkCoord>,
7737 curr_loaded: &HashSet<ChunkCoord>,
7738 memory_per_chunk_mb: f32,
7739) -> f32 {
7740 let newly_loaded = curr_loaded.difference(prev_loaded).count() as f32;
7741 let newly_unloaded = prev_loaded.difference(curr_loaded).count() as f32;
7742 (newly_loaded - newly_unloaded) * memory_per_chunk_mb
7743}
7744
7745pub fn aabb_corner_distances(bounds: &ChunkBounds, point: Vec3) -> [f32; 8] {
7746 let corners = bounds.corners();
7747 std::array::from_fn(|i| (corners[i] - point).length())
7748}
7749
7750pub fn bvh_sah_cost(left_sa: f32, right_sa: f32, parent_sa: f32, left_count: usize, right_count: usize) -> f32 {
7751 SAH_TRAVERSAL_COST + SAH_INTERSECTION_COST * (
7752 left_sa / parent_sa * left_count as f32
7753 + right_sa / parent_sa * right_count as f32
7754 )
7755}
7756
7757pub fn compute_cluster_merge_cost(a: &HlodCluster, b: &HlodCluster) -> f32 {
7758 let merged = a.merged_bounds.merge(&b.merged_bounds);
7759 let sa_merged = merged.surface_area();
7760 let sa_a = a.merged_bounds.surface_area();
7761 let sa_b = b.merged_bounds.surface_area();
7762 sa_merged - sa_a - sa_b
7763}
7764
7765pub fn evaluate_lod_quality_metrics(
7766 editor: &WorldStreamingEditor,
7767 reference_positions: &[Vec3],
7768) -> Vec<(Vec3, LodLevel, f32)> {
7769 reference_positions.iter().map(|&pos| {
7770 let dist = pos.length();
7771 let lod = editor.config.lod_for_distance(dist);
7772 let quality = 1.0 - lod.memory_multiplier();
7773 (pos, lod, quality)
7774 }).collect()
7775}
7776
7777pub fn build_streaming_config_from_hardware_caps(
7778 available_memory_mb: u64,
7779 available_cores: u32,
7780 screen_width: u32,
7781 screen_height: u32,
7782 target_fps: f32,
7783) -> StreamingConfig {
7784 let mut cfg = StreamingConfig::default();
7785 cfg.max_memory_mb = available_memory_mb;
7786 cfg.max_concurrent_loads = (available_cores / 2).max(1) as usize;
7787 cfg.screen_height_pixels = screen_height;
7788 let aspect = screen_width as f32 / screen_height as f32;
7789 let budget_tier = available_memory_mb / 1024;
7790 if budget_tier >= 8 {
7791 cfg.max_loaded_chunks = 512;
7792 cfg.enable_occlusion_culling = true;
7793 cfg.enable_hlod = true;
7794 cfg.enable_virtual_textures = true;
7795 } else if budget_tier >= 4 {
7796 cfg.max_loaded_chunks = 256;
7797 cfg.enable_hlod = true;
7798 } else {
7799 cfg.max_loaded_chunks = 128;
7800 cfg.enable_impostor_billboards = false;
7801 cfg.enable_virtual_textures = false;
7802 }
7803 if target_fps >= 120.0 {
7804 cfg.lod_distances = build_lod_distance_array(32.0, 2.0);
7805 } else if target_fps >= 60.0 {
7806 cfg.lod_distances = LOD_DISTANCES;
7807 } else {
7808 cfg.lod_distances = build_lod_distance_array(24.0, 2.2);
7809 }
7810 cfg
7811}
7812
7813
7814pub fn catmull_rom(p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, t: f32) -> Vec3 {
7819 let t2 = t * t;
7820 let t3 = t2 * t;
7821 let m0 = (p2 - p0) * 0.5;
7822 let m1 = (p3 - p1) * 0.5;
7823 let b0 = 2.0 * t3 - 3.0 * t2 + 1.0;
7824 let b1 = t3 - 2.0 * t2 + t;
7825 let b2 = -2.0 * t3 + 3.0 * t2;
7826 let b3 = t3 - t2;
7827 p1 * b0 + m0 * b1 + p2 * b2 + m1 * b3
7828}
7829
7830pub fn bezier_cubic(p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, t: f32) -> Vec3 {
7831 let u = 1.0 - t;
7832 p0 * (u * u * u) + p1 * (3.0 * u * u * t) + p2 * (3.0 * u * t * t) + p3 * (t * t * t)
7833}
7834
7835pub fn bezier_cubic_tangent(p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, t: f32) -> Vec3 {
7836 let u = 1.0 - t;
7837 (p1 - p0) * (3.0 * u * u) + (p2 - p1) * (6.0 * u * t) + (p3 - p2) * (3.0 * t * t)
7838}
7839
7840pub fn bezier_arc_length_table(p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, samples: usize) -> Vec<f32> {
7841 let mut table = vec![0.0f32; samples + 1];
7842 let mut prev = p0;
7843 for i in 1..=samples {
7844 let t = i as f32 / samples as f32;
7845 let cur = bezier_cubic(p0, p1, p2, p3, t);
7846 table[i] = table[i - 1] + (cur - prev).length();
7847 prev = cur;
7848 }
7849 table
7850}
7851
7852pub fn bezier_arc_length_t(table: &[f32], target_len: f32) -> f32 {
7853 let total = *table.last().unwrap_or(&0.0);
7854 if total < 1e-8 { return 0.0; }
7855 let target = (target_len / total).clamp(0.0, 1.0) * total;
7856 let n = table.len() - 1;
7857 for i in 1..table.len() {
7858 if table[i] >= target {
7859 let t0 = (i - 1) as f32 / n as f32;
7860 let t1 = i as f32 / n as f32;
7861 let frac = if table[i] - table[i-1] > 1e-8 {
7862 (target - table[i-1]) / (table[i] - table[i-1])
7863 } else { 0.0 };
7864 return t0 + (t1 - t0) * frac;
7865 }
7866 }
7867 1.0
7868}
7869
7870pub fn thermal_erosion_pass(hm: &mut TerrainHeightmap, talus_angle: f32, carry_fraction: f32) {
7871 let w = hm.width;
7872 let h = hm.height;
7873 let talus = talus_angle.tan() * hm.cell_size;
7874 let mut delta = vec![0.0f32; w * h];
7875 for z in 1..h-1 {
7876 for x in 1..w-1 {
7877 let c = hm.get_height(x, z);
7878 let neighbors = [(x-1, z), (x+1, z), (x, z-1), (x, z+1)];
7879 let mut max_diff = 0.0f32;
7880 let mut max_n = (x, z);
7881 for &(nx, nz) in &neighbors {
7882 let diff = c - hm.get_height(nx, nz);
7883 if diff > max_diff { max_diff = diff; max_n = (nx, nz); }
7884 }
7885 if max_diff > talus {
7886 let transport = (max_diff - talus) * carry_fraction;
7887 delta[z * w + x] -= transport;
7888 delta[max_n.1 * w + max_n.0] += transport;
7889 }
7890 }
7891 }
7892 for z in 0..h {
7893 for x in 0..w {
7894 hm.set_height(x, z, hm.get_height(x, z) + delta[z * w + x]);
7895 }
7896 }
7897}
7898
7899pub fn hydraulic_erosion_step(
7900 hm: &mut TerrainHeightmap, drop_x: f32, drop_z: f32,
7901 volume: f32, erosion_rate: f32, deposition_rate: f32, max_steps: usize,
7902) {
7903 let (mut x, mut z, mut vol, mut sediment) = (drop_x, drop_z, volume, 0.0f32);
7904 for _ in 0..max_steps {
7905 let grad = hm.compute_normal_bilinear(x, z);
7906 let speed = Vec2::new(-grad.x, -grad.z).length();
7907 if speed < 1e-6 { break; }
7908 let capacity = speed * vol;
7909 let h_here = hm.sample_bilinear(x, z);
7910 let ix = ((x - hm.origin.x) / hm.cell_size) as usize;
7911 let iz = ((z - hm.origin.y) / hm.cell_size) as usize;
7912 if sediment > capacity {
7913 let d = (sediment - capacity) * deposition_rate;
7914 sediment -= d;
7915 hm.set_height(ix, iz, h_here + d);
7916 } else {
7917 let e = (capacity - sediment).min(h_here * erosion_rate);
7918 sediment += e;
7919 hm.set_height(ix, iz, (h_here - e).max(0.0));
7920 }
7921 x -= grad.x * hm.cell_size;
7922 z -= grad.z * hm.cell_size;
7923 vol *= 0.99;
7924 if vol < 0.01 { break; }
7925 }
7926}
7927
7928pub fn compute_heightmap_ao(hm: &TerrainHeightmap, num_rays: usize, max_dist: f32) -> Vec<f32> {
7929 let (w, h) = (hm.width, hm.height);
7930 let mut ao = vec![1.0f32; w * h];
7931 for z in 0..h {
7932 for x in 0..w {
7933 let height = hm.get_height(x, z);
7934 let wx = hm.origin.x + x as f32 * hm.cell_size;
7935 let wz = hm.origin.y + z as f32 * hm.cell_size;
7936 let mut occ = 0.0f32;
7937 let steps = (max_dist / hm.cell_size) as usize;
7938 for ray in 0..num_rays {
7939 let angle = (ray as f32 / num_rays as f32) * std::f32::consts::TAU;
7940 let (dx, dz) = (angle.cos(), angle.sin());
7941 let mut max_h = 0.0f32;
7942 for step in 1..=steps {
7943 let t = step as f32 * hm.cell_size;
7944 let sh = hm.sample_bilinear(wx + dx * t, wz + dz * t);
7945 let horizon = (sh - height) / t;
7946 if horizon > max_h { max_h = horizon; }
7947 }
7948 occ += (max_h.atan() / std::f32::consts::FRAC_PI_2).clamp(0.0, 1.0);
7949 }
7950 ao[z * w + x] = 1.0 - (occ / num_rays as f32).clamp(0.0, 1.0);
7951 }
7952 }
7953 ao
7954}
7955
7956pub fn build_lod_transition_curve(min_dist: f32, max_dist: f32, steps: usize) -> Vec<(f32, f32)> {
7957 (0..=steps).map(|i| {
7958 let t = i as f32 / steps as f32;
7959 let d = min_dist + t * (max_dist - min_dist);
7960 (d, (1.0 - (d - min_dist) / (max_dist - min_dist + 1e-8)).clamp(0.0, 1.0))
7961 }).collect()
7962}
7963
7964pub fn sh_l1_eval(coeffs: &[Vec3; 4], normal: Vec3) -> Vec3 {
7965 let n = normal.normalize_or_zero();
7966 coeffs[0] * 0.282095 + coeffs[1] * 0.488603 * n.y
7967 + coeffs[2] * 0.488603 * n.z + coeffs[3] * 0.488603 * n.x
7968}
7969
7970pub fn sh_l1_project(dir: Vec3, color: Vec3) -> [Vec3; 4] {
7971 let n = dir.normalize_or_zero();
7972 let w = std::f32::consts::FRAC_1_PI * 0.25;
7973 [color*w*0.282095, color*w*0.488603*n.y, color*w*0.488603*n.z, color*w*0.488603*n.x]
7974}
7975
7976pub fn pack_rgba8(r: f32, g: f32, b: f32, a: f32) -> u32 {
7977 (r.clamp(0.0,1.0)*255.0) as u32
7978 | (((g.clamp(0.0,1.0)*255.0) as u32) << 8)
7979 | (((b.clamp(0.0,1.0)*255.0) as u32) << 16)
7980 | (((a.clamp(0.0,1.0)*255.0) as u32) << 24)
7981}
7982
7983pub fn unpack_rgba8(packed: u32) -> (f32, f32, f32, f32) {
7984 ((packed & 0xFF) as f32 / 255.0, ((packed>>8)&0xFF) as f32/255.0,
7985 ((packed>>16)&0xFF) as f32/255.0, ((packed>>24)&0xFF) as f32/255.0)
7986}
7987
7988pub fn halton(index: u32, base: u32) -> f32 {
7989 let (mut result, mut f, mut i) = (0.0f32, 1.0f32, index);
7990 while i > 0 { f /= base as f32; result += f * (i % base) as f32; i /= base; }
7991 result
7992}
7993
7994pub fn halton_2d(index: u32) -> Vec2 { Vec2::new(halton(index,2), halton(index,3)) }
7995pub fn halton_3d(index: u32) -> Vec3 { Vec3::new(halton(index,2), halton(index,3), halton(index,5)) }
7996
7997pub fn exp_smooth(current: f32, target: f32, lambda: f32, dt: f32) -> f32 {
7998 current + (target - current) * (1.0 - (-lambda * dt).exp())
7999}
8000
8001pub fn exp_smooth_vec3(current: Vec3, target: Vec3, lambda: f32, dt: f32) -> Vec3 {
8002 current + (target - current) * (1.0 - (-lambda * dt).exp())
8003}
8004
8005pub fn aabb_solid_angle_approx(bounds: &ChunkBounds, viewpoint: Vec3) -> f32 {
8006 let dist = (bounds.center() - viewpoint).length();
8007 if dist < 1e-6 { return std::f32::consts::TAU * 2.0; }
8008 let angle = (bounds.half_size().length() / dist).min(1.0).asin();
8009 std::f32::consts::PI * angle * angle
8010}
8011
8012pub fn chunk_visual_importance(bounds: &ChunkBounds, viewpoint: Vec3, object_count: usize) -> f32 {
8013 aabb_solid_angle_approx(bounds, viewpoint)
8014 * (object_count as f32 / bounds.volume().max(1.0)).sqrt()
8015}
8016
8017pub fn isqrt(n: u64) -> u64 {
8018 if n == 0 { return 0; }
8019 let (mut x, mut y) = (n, (n + 1) / 2);
8020 while y < x { x = y; y = (x + n / x) / 2; }
8021 x
8022}
8023
8024pub fn required_lod_levels(world_size: f32, min_feature: f32) -> u32 {
8025 if min_feature <= 0.0 { return 1; }
8026 ((world_size / min_feature).log2().ceil() as u32).max(1)
8027}
8028
8029pub fn estimate_bvh_memory_bytes(n: usize) -> usize { 2 * n * 128 }
8030
8031pub fn estimate_octree_memory_bytes(n: usize, depth: u32) -> usize {
8032 ((8usize.pow(depth + 1) - 1) / 7).min(n * 4) * 64
8033}
8034
8035pub fn bvh_sah_cost_fn(lsa: f32, rsa: f32, psa: f32, lc: usize, rc: usize) -> f32 {
8036 SAH_TRAVERSAL_COST + SAH_INTERSECTION_COST * (lsa/psa*lc as f32 + rsa/psa*rc as f32)
8037}
8038
8039pub fn cluster_merge_cost(a: &HlodCluster, b: &HlodCluster) -> f32 {
8040 a.merged_bounds.merge(&b.merged_bounds).surface_area()
8041 - a.merged_bounds.surface_area() - b.merged_bounds.surface_area()
8042}
8043
8044pub fn evaluate_lod_quality(editor: &WorldStreamingEditor, positions: &[Vec3]) -> Vec<(Vec3, LodLevel, f32)> {
8045 positions.iter().map(|&p| {
8046 let lod = editor.config.lod_for_distance(p.length());
8047 (p, lod.clone(), 1.0 - lod.memory_multiplier())
8048 }).collect()
8049}
8050
8051pub fn system_capacity_summary() -> Vec<(&'static str, usize)> {
8052 vec![
8053 ("MAX_OCTREE_DEPTH", MAX_OCTREE_DEPTH as usize),
8054 ("BVH_MAX_LEAF_OBJECTS", BVH_MAX_LEAF_OBJECTS),
8055 ("DEFAULT_MAX_LOADED_CHUNKS", DEFAULT_MAX_LOADED_CHUNKS),
8056 ("DEFAULT_MAX_MEMORY_MB", DEFAULT_MAX_MEMORY_MB as usize),
8057 ("MAX_ASYNC_LOAD_QUEUE", MAX_ASYNC_LOAD_QUEUE),
8058 ("TERRAIN_PATCH_SIZE", TERRAIN_PATCH_SIZE),
8059 ("PROFILER_HISTORY_FRAMES", PROFILER_HISTORY_FRAMES),
8060 ]
8061}