1
2use glam::{Vec2, Vec3, Vec4, Mat4};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum LodTransitionMode {
13 Discrete,
14 CrossFade,
15 SpeedTree,
16 Dither,
17}
18
19#[derive(Debug, Clone)]
20pub struct LodLevel {
21 pub index: usize,
22 pub mesh_id: u64,
23 pub screen_relative_transition_height: f32,
24 pub fade_transition_width: f32,
25 pub renderers_enabled: Vec<bool>,
26 pub triangle_count: u32,
27 pub vertex_count: u32,
28 pub reduction_ratio: f32,
29 pub shadow_casting: bool,
30 pub shadow_receiving: bool,
31 pub motion_vectors: bool,
32 pub skinned_motion_vectors: bool,
33}
34
35impl LodLevel {
36 pub fn new(index: usize, mesh_id: u64, transition: f32, tris: u32, verts: u32) -> Self {
37 Self {
38 index,
39 mesh_id,
40 screen_relative_transition_height: transition,
41 fade_transition_width: 0.1,
42 renderers_enabled: vec![true],
43 triangle_count: tris,
44 vertex_count: verts,
45 reduction_ratio: 1.0,
46 shadow_casting: true,
47 shadow_receiving: true,
48 motion_vectors: false,
49 skinned_motion_vectors: false,
50 }
51 }
52}
53
54#[derive(Debug, Clone)]
59pub struct LodGroup {
60 pub id: u64,
61 pub name: String,
62 pub center: Vec3,
63 pub size: f32,
64 pub levels: Vec<LodLevel>,
65 pub transition_mode: LodTransitionMode,
66 pub animate_cross_fading: bool,
67 pub fade_mode: FadeMode,
68 pub current_lod: usize,
69 pub fade_t: f32,
70 pub enabled: bool,
71 pub position: Vec3,
72}
73
74#[derive(Debug, Clone, Copy, PartialEq)]
75pub enum FadeMode {
76 None,
77 CrossFade,
78 SpeedTree,
79}
80
81impl LodGroup {
82 pub fn new(id: u64, name: impl Into<String>, position: Vec3, size: f32) -> Self {
83 Self {
84 id,
85 name: name.into(),
86 center: Vec3::ZERO,
87 size,
88 levels: Vec::new(),
89 transition_mode: LodTransitionMode::CrossFade,
90 animate_cross_fading: true,
91 fade_mode: FadeMode::CrossFade,
92 current_lod: 0,
93 fade_t: 0.0,
94 enabled: true,
95 position,
96 }
97 }
98
99 pub fn add_level(&mut self, level: LodLevel) {
100 let i = self.levels.partition_point(|l| l.screen_relative_transition_height > level.screen_relative_transition_height);
101 self.levels.insert(i, level);
102 for (j, l) in self.levels.iter_mut().enumerate() {
104 l.index = j;
105 }
106 }
107
108 pub fn screen_coverage(&self, distance: f32, fov_tan: f32, screen_height: f32) -> f32 {
110 if distance < 0.001 { return 1.0; }
111 let world_size = self.size;
112 let projected_size = world_size / (distance * fov_tan);
113 (projected_size / screen_height).clamp(0.0, 1.0)
114 }
115
116 pub fn compute_lod(&self, screen_coverage: f32) -> usize {
118 for (i, level) in self.levels.iter().enumerate() {
119 if screen_coverage >= level.screen_relative_transition_height {
120 return i;
121 }
122 }
123 self.levels.len().saturating_sub(1) }
125
126 pub fn update(&mut self, camera_pos: Vec3, fov_tan: f32, screen_height: f32, dt: f32) {
127 let dist = self.position.distance(camera_pos);
128 let cov = self.screen_coverage(dist, fov_tan, screen_height);
129 let new_lod = self.compute_lod(cov);
130 if new_lod != self.current_lod {
131 if self.transition_mode == LodTransitionMode::CrossFade {
132 self.fade_t = 1.0;
133 }
134 self.current_lod = new_lod;
135 }
136 if self.fade_t > 0.0 {
137 self.fade_t = (self.fade_t - dt * 4.0).max(0.0);
138 }
139 }
140
141 pub fn triangle_reduction_from_lod0(&self, lod_idx: usize) -> f32 {
142 if self.levels.is_empty() { return 1.0; }
143 let base = self.levels[0].triangle_count as f32;
144 let current = self.levels.get(lod_idx).map(|l| l.triangle_count as f32).unwrap_or(0.0);
145 if base > 0.0 { current / base } else { 1.0 }
146 }
147}
148
149#[derive(Debug, Clone, Copy, PartialEq)]
154pub enum SimplificationAlgorithm {
155 QEM, MeshOptimizer,
157 Sloppy,
158 UniformGrid,
159}
160
161#[derive(Debug, Clone)]
162pub struct SimplificationSettings {
163 pub algorithm: SimplificationAlgorithm,
164 pub target_ratio: f32, pub max_error: f32,
166 pub preserve_borders: bool,
167 pub preserve_uvs: bool,
168 pub preserve_normals: bool,
169 pub preserve_attributes: bool,
170 pub lock_border: bool,
171 pub merge_threshold: f32,
172 pub attribute_weight: f32,
173}
174
175impl Default for SimplificationSettings {
176 fn default() -> Self {
177 Self {
178 algorithm: SimplificationAlgorithm::QEM,
179 target_ratio: 0.5,
180 max_error: 0.001,
181 preserve_borders: true,
182 preserve_uvs: true,
183 preserve_normals: true,
184 preserve_attributes: true,
185 lock_border: false,
186 merge_threshold: 1e-4,
187 attribute_weight: 0.1,
188 }
189 }
190}
191
192#[derive(Debug, Clone)]
193pub struct SimplificationResult {
194 pub original_tris: u32,
195 pub result_tris: u32,
196 pub original_verts: u32,
197 pub result_verts: u32,
198 pub max_deviation: f32,
199 pub rms_deviation: f32,
200 pub processing_ms: f32,
201 pub success: bool,
202 pub error_message: Option<String>,
203}
204
205impl SimplificationResult {
206 pub fn ratio(&self) -> f32 {
207 if self.original_tris == 0 { return 1.0; }
208 self.result_tris as f32 / self.original_tris as f32
209 }
210}
211
212pub fn generate_lod_levels(mesh_id: u64, original_tris: u32, original_verts: u32, ratios: &[f32]) -> Vec<(LodLevel, SimplificationResult)> {
214 ratios.iter().enumerate().map(|(i, &ratio)| {
215 let result_tris = (original_tris as f32 * ratio) as u32;
216 let result_verts = (original_verts as f32 * ratio) as u32;
217 let transition = match i {
218 0 => 1.0,
219 1 => 0.5,
220 2 => 0.25,
221 3 => 0.1,
222 _ => 0.05 / i as f32,
223 };
224 let level = LodLevel::new(i, mesh_id * 100 + i as u64, transition, result_tris, result_verts);
225 let result = SimplificationResult {
226 original_tris,
227 result_tris,
228 original_verts,
229 result_verts,
230 max_deviation: 0.001 * (1.0 - ratio),
231 rms_deviation: 0.0003 * (1.0 - ratio),
232 processing_ms: original_tris as f32 * 0.001 * (1.0 - ratio),
233 success: true,
234 error_message: None,
235 };
236 (level, result)
237 }).collect()
238}
239
240#[derive(Debug, Clone, Copy, PartialEq)]
245pub enum OcclusionMode {
246 None,
247 HiZ, SoftwareRasterization,
249 PvsPortal,
250 Umbra,
251}
252
253#[derive(Debug, Clone)]
254pub struct OcclusionSettings {
255 pub mode: OcclusionMode,
256 pub occluder_size_threshold: f32,
257 pub occludee_size_threshold: f32,
258 pub backface_culling: bool,
259 pub hi_z_mip_levels: u32,
260 pub conservative_depth: bool,
261 pub async_readback: bool,
262 pub readback_frame_delay: u32,
263 pub debug_draw_occluders: bool,
264 pub debug_draw_occludees: bool,
265}
266
267impl Default for OcclusionSettings {
268 fn default() -> Self {
269 Self {
270 mode: OcclusionMode::HiZ,
271 occluder_size_threshold: 0.01,
272 occludee_size_threshold: 0.001,
273 backface_culling: true,
274 hi_z_mip_levels: 8,
275 conservative_depth: false,
276 async_readback: true,
277 readback_frame_delay: 2,
278 debug_draw_occluders: false,
279 debug_draw_occludees: false,
280 }
281 }
282}
283
284#[derive(Debug, Clone)]
285pub struct OcclusionQuery {
286 pub object_id: u64,
287 pub bounding_sphere: (Vec3, f32),
288 pub visible_last_frame: bool,
289 pub frames_invisible: u32,
290 pub frames_visible: u32,
291}
292
293#[derive(Debug, Clone, Copy, PartialEq)]
298pub enum StreamingState {
299 Unloaded,
300 Queued,
301 Loading,
302 Loaded,
303 Unloading,
304 Error,
305}
306
307#[derive(Debug, Clone)]
308pub struct StreamableAsset {
309 pub id: u64,
310 pub name: String,
311 pub size_bytes: u64,
312 pub lod_group: Option<u64>,
313 pub streaming_state: StreamingState,
314 pub load_priority: f32,
315 pub last_visible_frame: u64,
316 pub retain_frames: u32,
317 pub memory_budget_category: MemoryCategory,
318}
319
320#[derive(Debug, Clone, Copy, PartialEq)]
321pub enum MemoryCategory {
322 Critical,
323 High,
324 Medium,
325 Low,
326 Background,
327}
328
329impl MemoryCategory {
330 pub fn eviction_priority(self) -> u8 {
331 match self {
332 MemoryCategory::Critical => 255,
333 MemoryCategory::High => 200,
334 MemoryCategory::Medium => 128,
335 MemoryCategory::Low => 64,
336 MemoryCategory::Background => 0,
337 }
338 }
339}
340
341#[derive(Debug, Clone)]
342pub struct StreamingManager {
343 pub assets: Vec<StreamableAsset>,
344 pub max_memory_bytes: u64,
345 pub current_memory_bytes: u64,
346 pub load_queue: Vec<u64>,
347 pub unload_queue: Vec<u64>,
348 pub current_frame: u64,
349 pub bandwidth_limit_bytes_per_frame: u64,
350 pub bytes_loaded_this_frame: u64,
351 pub priority_bias_distance: f32,
352 pub camera_pos: Vec3,
353}
354
355impl StreamingManager {
356 pub fn new(max_memory_mb: u32) -> Self {
357 Self {
358 assets: Vec::new(),
359 max_memory_bytes: max_memory_mb as u64 * 1_048_576,
360 current_memory_bytes: 0,
361 load_queue: Vec::new(),
362 unload_queue: Vec::new(),
363 current_frame: 0,
364 bandwidth_limit_bytes_per_frame: 64 * 1_048_576, bytes_loaded_this_frame: 0,
366 priority_bias_distance: 50.0,
367 camera_pos: Vec3::ZERO,
368 }
369 }
370
371 pub fn register_asset(&mut self, asset: StreamableAsset) {
372 self.assets.push(asset);
373 }
374
375 pub fn update(&mut self, camera_pos: Vec3) {
376 self.camera_pos = camera_pos;
377 self.current_frame += 1;
378 self.bytes_loaded_this_frame = 0;
379 let mut loaded = Vec::new();
381 for &id in &self.load_queue {
382 if let Some(asset) = self.assets.iter_mut().find(|a| a.id == id) {
383 if self.current_memory_bytes + asset.size_bytes <= self.max_memory_bytes
384 && self.bytes_loaded_this_frame + asset.size_bytes <= self.bandwidth_limit_bytes_per_frame {
385 asset.streaming_state = StreamingState::Loaded;
386 self.current_memory_bytes += asset.size_bytes;
387 self.bytes_loaded_this_frame += asset.size_bytes;
388 loaded.push(id);
389 }
390 }
391 }
392 self.load_queue.retain(|id| !loaded.contains(id));
393 let mut unloaded = Vec::new();
395 for &id in &self.unload_queue {
396 if let Some(asset) = self.assets.iter_mut().find(|a| a.id == id) {
397 self.current_memory_bytes = self.current_memory_bytes.saturating_sub(asset.size_bytes);
398 asset.streaming_state = StreamingState::Unloaded;
399 unloaded.push(id);
400 }
401 }
402 self.unload_queue.retain(|id| !unloaded.contains(id));
403 if self.current_memory_bytes > self.max_memory_bytes {
405 self.evict_lru();
406 }
407 }
408
409 pub fn request_load(&mut self, id: u64, priority: f32) {
410 if let Some(asset) = self.assets.iter_mut().find(|a| a.id == id) {
411 if asset.streaming_state == StreamingState::Unloaded {
412 asset.streaming_state = StreamingState::Queued;
413 asset.load_priority = priority;
414 self.load_queue.push(id);
415 let assets = &self.assets;
417 self.load_queue.sort_by(|a, b| {
418 let pa = assets.iter().find(|x| x.id == *a).map(|x| x.load_priority).unwrap_or(0.0);
419 let pb = assets.iter().find(|x| x.id == *b).map(|x| x.load_priority).unwrap_or(0.0);
420 pb.partial_cmp(&pa).unwrap_or(std::cmp::Ordering::Equal)
421 });
422 }
423 }
424 }
425
426 pub fn request_unload(&mut self, id: u64) {
427 if let Some(asset) = self.assets.iter_mut().find(|a| a.id == id) {
428 if asset.streaming_state == StreamingState::Loaded {
429 asset.streaming_state = StreamingState::Unloading;
430 self.unload_queue.push(id);
431 }
432 }
433 }
434
435 fn evict_lru(&mut self) {
436 let current_frame = self.current_frame;
438 let evict_id = self.assets.iter()
439 .filter(|a| a.streaming_state == StreamingState::Loaded && a.memory_budget_category != MemoryCategory::Critical)
440 .min_by_key(|a| a.last_visible_frame)
441 .map(|a| a.id);
442 if let Some(id) = evict_id {
443 self.request_unload(id);
444 }
445 }
446
447 pub fn memory_pressure(&self) -> f32 {
448 self.current_memory_bytes as f32 / self.max_memory_bytes as f32
449 }
450
451 pub fn loaded_count(&self) -> usize {
452 self.assets.iter().filter(|a| a.streaming_state == StreamingState::Loaded).count()
453 }
454}
455
456#[derive(Debug, Clone)]
461pub struct LodManagerStats {
462 pub total_lod_groups: usize,
463 pub active_lod_groups: usize,
464 pub lod0_count: usize,
465 pub lod1_count: usize,
466 pub lod2_count: usize,
467 pub culled_count: usize,
468 pub total_triangles_without_lod: u64,
469 pub total_triangles_with_lod: u64,
470 pub savings_ratio: f32,
471}
472
473#[derive(Debug, Clone)]
474pub struct LodManager {
475 pub groups: Vec<LodGroup>,
476 pub occlusion_settings: OcclusionSettings,
477 pub occlusion_queries: Vec<OcclusionQuery>,
478 pub streaming: StreamingManager,
479 pub camera_pos: Vec3,
480 pub camera_fov: f32,
481 pub screen_height: f32,
482 pub enable_lod: bool,
483 pub enable_occlusion: bool,
484 pub lod_bias: f32,
485 pub max_active_groups: usize,
486 pub stats: LodManagerStats,
487}
488
489impl LodManager {
490 pub fn new() -> Self {
491 let mut mgr = Self {
492 groups: Vec::new(),
493 occlusion_settings: OcclusionSettings::default(),
494 occlusion_queries: Vec::new(),
495 streaming: StreamingManager::new(1024),
496 camera_pos: Vec3::ZERO,
497 camera_fov: 60.0,
498 screen_height: 1080.0,
499 enable_lod: true,
500 enable_occlusion: true,
501 lod_bias: 1.0,
502 max_active_groups: 10000,
503 stats: LodManagerStats {
504 total_lod_groups: 0, active_lod_groups: 0, lod0_count: 0,
505 lod1_count: 0, lod2_count: 0, culled_count: 0,
506 total_triangles_without_lod: 0, total_triangles_with_lod: 0, savings_ratio: 0.0,
507 },
508 };
509 mgr.populate_demo();
510 mgr
511 }
512
513 fn populate_demo(&mut self) {
514 let positions = [
516 Vec3::new(0.0, 0.0, 0.0),
517 Vec3::new(10.0, 0.0, 0.0),
518 Vec3::new(-10.0, 0.0, 5.0),
519 Vec3::new(0.0, 0.0, 15.0),
520 Vec3::new(20.0, 0.0, -5.0),
521 ];
522 let sizes = [2.0_f32, 5.0, 1.5, 8.0, 3.0];
523 let base_tris = [5000u32, 12000, 3000, 20000, 8000];
524 for (i, ((pos, size), tris)) in positions.iter().zip(sizes.iter()).zip(base_tris.iter()).enumerate() {
525 let mut group = LodGroup::new(i as u64 + 1, format!("Object_{}", i), *pos, *size);
526 let levels = generate_lod_levels(i as u64 * 100, *tris, tris / 2, &[1.0, 0.5, 0.25, 0.1]);
527 for (level, _) in levels {
528 group.add_level(level);
529 }
530 self.groups.push(group);
531 }
532 }
533
534 pub fn add_group(&mut self, group: LodGroup) {
535 self.groups.push(group);
536 }
537
538 pub fn update(&mut self, camera_pos: Vec3, dt: f32) {
539 self.camera_pos = camera_pos;
540 self.streaming.update(camera_pos);
541 if !self.enable_lod { return; }
542 let fov_tan = (self.camera_fov * 0.5 * std::f32::consts::PI / 180.0).tan();
543 let screen_h = self.screen_height;
544 for group in &mut self.groups {
546 if !group.enabled { continue; }
547 group.update(camera_pos, fov_tan, screen_h, dt);
548 }
549 self.recompute_stats();
550 }
551
552 fn recompute_stats(&mut self) {
553 let mut stats = LodManagerStats {
554 total_lod_groups: self.groups.len(),
555 active_lod_groups: self.groups.iter().filter(|g| g.enabled).count(),
556 lod0_count: 0, lod1_count: 0, lod2_count: 0, culled_count: 0,
557 total_triangles_without_lod: 0,
558 total_triangles_with_lod: 0,
559 savings_ratio: 0.0,
560 };
561 for group in &self.groups {
562 if !group.enabled { continue; }
563 if group.levels.is_empty() { continue; }
564 let base_tris = group.levels[0].triangle_count as u64;
565 stats.total_triangles_without_lod += base_tris;
566 let cur_tris = group.levels.get(group.current_lod).map(|l| l.triangle_count as u64).unwrap_or(0);
567 stats.total_triangles_with_lod += cur_tris;
568 match group.current_lod {
569 0 => stats.lod0_count += 1,
570 1 => stats.lod1_count += 1,
571 2 => stats.lod2_count += 1,
572 _ => stats.culled_count += 1,
573 }
574 }
575 if stats.total_triangles_without_lod > 0 {
576 stats.savings_ratio = 1.0 - (stats.total_triangles_with_lod as f32 / stats.total_triangles_without_lod as f32);
577 }
578 self.stats = stats;
579 }
580
581 pub fn find_group(&self, id: u64) -> Option<&LodGroup> {
582 self.groups.iter().find(|g| g.id == id)
583 }
584
585 pub fn find_group_mut(&mut self, id: u64) -> Option<&mut LodGroup> {
586 self.groups.iter_mut().find(|g| g.id == id)
587 }
588
589 pub fn triangle_savings_str(&self) -> String {
590 format!("{:.1}% triangle reduction ({} → {})",
591 self.stats.savings_ratio * 100.0,
592 self.stats.total_triangles_without_lod,
593 self.stats.total_triangles_with_lod)
594 }
595
596 pub fn generate_lod_for_group(&mut self, group_id: u64, original_tris: u32, original_verts: u32, ratios: &[f32]) {
597 if let Some(group) = self.find_group_mut(group_id) {
598 group.levels.clear();
599 let levels = generate_lod_levels(group_id, original_tris, original_verts, ratios);
600 for (level, _) in levels {
601 group.add_level(level);
602 }
603 }
604 }
605}
606
607#[cfg(test)]
611mod tests {
612 use super::*;
613
614 #[test]
615 fn test_lod_group() {
616 let mut group = LodGroup::new(1, "test", Vec3::ZERO, 5.0);
617 let levels = generate_lod_levels(1, 10000, 5000, &[1.0, 0.5, 0.25]);
618 for (l, _) in levels { group.add_level(l); }
619 assert_eq!(group.levels.len(), 3);
620 let cov = group.screen_coverage(10.0, 0.5773, 1080.0);
621 assert!(cov > 0.0 && cov <= 1.0);
622 }
623
624 #[test]
625 fn test_lod_compute() {
626 let mut group = LodGroup::new(1, "test", Vec3::ZERO, 5.0);
627 let levels = generate_lod_levels(1, 10000, 5000, &[1.0, 0.5, 0.25, 0.1]);
628 for (l, _) in levels { group.add_level(l); }
629 assert_eq!(group.compute_lod(1.0), 0);
630 assert_eq!(group.compute_lod(0.4), 1);
631 assert_eq!(group.compute_lod(0.05), 3);
632 }
633
634 #[test]
635 fn test_streaming_manager() {
636 let mut mgr = StreamingManager::new(256);
637 mgr.register_asset(StreamableAsset {
638 id: 1, name: "TestMesh".into(), size_bytes: 1_048_576,
639 lod_group: None, streaming_state: StreamingState::Unloaded,
640 load_priority: 1.0, last_visible_frame: 0, retain_frames: 10,
641 memory_budget_category: MemoryCategory::Medium,
642 });
643 mgr.request_load(1, 1.0);
644 mgr.update(Vec3::ZERO);
645 assert_eq!(mgr.loaded_count(), 1);
646 }
647
648 #[test]
649 fn test_lod_manager() {
650 let mut mgr = LodManager::new();
651 assert!(!mgr.groups.is_empty());
652 mgr.update(Vec3::ZERO, 0.016);
653 assert!(mgr.stats.total_lod_groups > 0);
654 }
655}