1#![no_std]
2#[cfg(feature = "std")]
3extern crate std;
4use camera::Camera;
5use embedded_graphics_core::pixelcolor::Rgb565;
6use embedded_graphics_core::pixelcolor::RgbColor;
7use mesh::K3dMesh;
8use mesh::RenderMode;
9use nalgebra::Matrix4;
10use nalgebra::Point2;
11use nalgebra::Point3;
12use nalgebra::Vector3;
13use nalgebra::Vector4;
14
15#[allow(unused_imports)]
18use nalgebra::ComplexField;
19
20pub mod animation;
21pub mod billboard;
22pub mod bridge;
23pub mod bsp;
24pub mod camera;
25pub mod character;
26pub mod command_buffer;
27pub mod config;
28pub mod display_backend;
29pub mod draw;
30pub mod error;
31pub mod fixed_math;
32pub mod hardware_profile;
33pub mod hud;
34pub mod input;
35pub mod lights;
36pub mod lut;
37pub mod mesh;
38#[cfg(feature = "std")]
39pub mod painters;
40pub mod particles;
41#[cfg(feature = "perfcounter")]
42pub mod perfcounter;
43#[cfg(feature = "physics")]
44pub mod physics;
45pub mod renderer;
46pub mod retro;
47pub mod scene_format;
48pub mod scene_stream;
49pub mod sector_lights;
50pub mod skeleton;
51#[cfg(feature = "physics")]
52pub mod softbody;
53pub mod swapchain;
54pub mod telemetry;
55pub mod texture;
56pub mod tilebin;
57pub mod transform_anim;
58pub mod tween;
59
60pub use embedded_graphics_framebuf::{
62 FrameBuf,
63 backends::{DMACapableFrameBufferBackend, EndianCorrectedBuffer, EndianCorrection},
64};
65
66#[cfg(feature = "aa")]
67pub use draw::ReadPixel;
68
69pub use bridge::{
70 AsEgPoint, AsNalgebraPoint, draw_to, eg_to_nalgebra, nalgebra_to_eg, render_drawable_to_buffer,
71};
72pub use character::CharacterController;
73#[cfg(feature = "aa")]
74pub use draw::draw_zbuffered_2xssaa;
75pub use draw::{
76 DitherConfig, FogConfig, fast_blend_rgb565, fast_blend_rgba8888, fast_blend_rgba8888_to_rgb565,
77 reverse_color_rgb565, reverse_color_rgba8888,
78};
79pub use fixed_math::{
80 FP_ONE,
81 Q16,
83 Q16_MAX,
84 Q16_MIN,
85 ScanlineInterp,
87 abs_q16,
88 angle_to_q16,
89 div_f_q16,
90 div_fp,
91 div_n_q16,
92 div_q16,
93 from_fp,
94 from_i16_q16,
95 from_q16,
96 lerp_q16,
98 mul_f_q16,
99 mul_fp,
100 mul_n_q16,
101 mul_q16,
103 q16_to_q31,
104 q31_to_q16,
105 qadd_q16,
107 qsub_q16,
108 recip_q16,
109 to_fp,
111 to_i16_q16,
112 to_q16,
114};
115pub use input::InputState;
116pub use lights::{PointLight, PointLightSet};
117pub use particles::{ParticleSpawn, ParticleSystem};
118pub use renderer::{DirtyRegion, FrameCtx};
119pub use retro::{
120 LightLevels, PaletteMode, RetroStyle, ScreenTint, SkyConfig, StippleMode, TextureMapping,
121};
122pub use sector_lights::{LightEffectKind, SectorLight, light_level_at, light_level_u8_at};
123pub use tilebin::{TileBinStats, TileConfig};
124pub use transform_anim::{AnimationPlayer, SampledTransform, TransformKeyframe, TransformTrack};
125pub use tween::{Easing, Tween, Tween3, apply_easing, lerp, lerp3, scale_rgb565};
126
127#[derive(Debug, Clone)]
128pub enum DrawPrimitive {
129 ColoredPoint(Point2<i32>, Rgb565),
130 Line([Point2<i32>; 2], Rgb565),
131 ColoredTriangle([Point2<i32>; 3], Rgb565),
132 ColoredTriangleWithDepth {
133 points: [Point2<i32>; 3],
134 depths: [f32; 3],
135 color: Rgb565,
136 },
137 TranslucentTriangleWithDepth {
138 points: [Point2<i32>; 3],
139 depths: [f32; 3],
140 color: Rgb565,
141 alpha: u8,
142 },
143 GouraudTriangle {
144 points: [Point2<i32>; 3],
145 colors: [Rgb565; 3],
146 },
147 GouraudTriangleWithDepth {
148 points: [Point2<i32>; 3],
149 depths: [f32; 3],
150 colors: [Rgb565; 3],
151 },
152 TexturedTriangle {
153 points: [Point2<i32>; 3],
154 uvs: [[f32; 2]; 3],
155 texture_id: u32,
156 },
157 TexturedTriangleWithDepth {
158 points: [Point2<i32>; 3],
159 depths: [f32; 3],
160 ws: [f32; 3],
161 uvs: [[f32; 2]; 3],
162 texture_id: u32,
163 },
164 LightmappedTriangle {
172 points: [Point2<i32>; 3],
173 depths: [f32; 3],
174 ws: [f32; 3],
175 surface_uvs: [[f32; 2]; 3],
176 lm_uvs: [[f32; 2]; 3],
177 texture_id: u32,
178 lightmap_id: u32,
179 brightness: u8,
181 dynamic_tint: Rgb565,
183 },
184}
185
186pub struct K3dengine {
187 pub camera: Camera,
188 width: u16,
189 height: u16,
190 caps: Option<crate::config::ProfileCaps>,
191 quality_tier: crate::config::QualityTier,
192 material_profile: crate::config::MaterialProfile,
193 fog: Option<crate::draw::FogConfig>,
195 dither: Option<crate::draw::DitherConfig>,
197 vertex_snap_bits: u8,
199 texture_mapping: crate::retro::TextureMapping,
201 light_levels: crate::retro::LightLevels,
203 stipple_mode: crate::retro::StippleMode,
205 screen_tint: Option<crate::retro::ScreenTint>,
207 palette_mode: crate::retro::PaletteMode,
209 sky: Option<crate::retro::SkyConfig>,
211 point_lights: heapless::Vec<crate::lights::PointLight, 16>,
214}
215
216#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217pub struct BudgetFallbackOutcome {
218 pub used_fallback: bool,
219 pub primary_budget_error: Option<crate::error::BudgetKind>,
220}
221
222#[derive(Debug, Clone, Copy, PartialEq, Eq)]
223pub struct DegradationOutcome {
224 pub used_degradation: bool,
225 pub steps_applied: usize,
226 pub dropped_meshes: usize,
227 pub final_quality_tier: crate::config::QualityTier,
228 pub primary_budget_error: Option<crate::error::BudgetKind>,
229}
230
231impl K3dengine {
232 pub fn new(width: u16, height: u16) -> K3dengine {
233 K3dengine {
234 camera: Camera::new(width as f32 / height as f32),
235 width,
236 height,
237 caps: None,
238 quality_tier: crate::config::QualityTier::Balanced,
239 material_profile: crate::config::MaterialProfile::Lambert,
240 fog: None,
241 dither: None,
242 vertex_snap_bits: 0,
243 texture_mapping: crate::retro::TextureMapping::PerspectiveCorrect,
244 light_levels: crate::retro::LightLevels::Linear,
245 stipple_mode: crate::retro::StippleMode::Off,
246 screen_tint: None,
247 palette_mode: crate::retro::PaletteMode::Off,
248 sky: None,
249 point_lights: heapless::Vec::new(),
250 }
251 }
252
253 pub fn set_fog(&mut self, fog: crate::draw::FogConfig) {
255 self.fog = Some(fog);
256 }
257
258 pub fn clear_fog(&mut self) {
260 self.fog = None;
261 }
262
263 pub fn set_dither(&mut self, dither: crate::draw::DitherConfig) {
265 self.dither = Some(dither);
266 }
267
268 pub fn clear_dither(&mut self) {
270 self.dither = None;
271 }
272
273 pub fn set_vertex_snap_bits(&mut self, bits: u8) {
275 self.vertex_snap_bits = bits.min(16);
276 }
277
278 pub fn set_texture_mapping(&mut self, mapping: crate::retro::TextureMapping) {
280 self.texture_mapping = mapping;
281 }
282
283 pub fn set_light_levels(&mut self, levels: crate::retro::LightLevels) {
285 self.light_levels = levels;
286 }
287
288 pub fn set_stipple_mode(&mut self, mode: crate::retro::StippleMode) {
290 self.stipple_mode = mode;
291 }
292
293 pub fn set_screen_tint(&mut self, tint: crate::retro::ScreenTint) {
295 self.screen_tint = Some(tint);
296 }
297
298 pub fn clear_screen_tint(&mut self) {
300 self.screen_tint = None;
301 }
302
303 pub fn set_palette_mode(&mut self, mode: crate::retro::PaletteMode) {
305 self.palette_mode = mode;
306 }
307
308 pub fn set_sky(&mut self, sky: crate::retro::SkyConfig) {
310 self.sky = Some(sky);
311 }
312
313 pub fn clear_sky(&mut self) {
315 self.sky = None;
316 }
317
318 pub fn apply_retro_style(&mut self, style: crate::retro::RetroStyle) {
320 self.fog = style.fog;
321 self.dither = style.dither;
322 self.set_vertex_snap_bits(style.vertex_snap_bits);
323 self.texture_mapping = style.texture_mapping;
324 self.light_levels = style.light_levels;
325 self.stipple_mode = style.stipple_mode;
326 self.screen_tint = style.screen_tint;
327 self.palette_mode = style.palette_mode;
328 self.sky = style.sky;
329 }
330
331 pub fn add_point_light(&mut self, light: crate::lights::PointLight) -> bool {
334 self.point_lights.push(light).is_ok()
335 }
336
337 pub fn clear_point_lights(&mut self) {
339 self.point_lights.clear();
340 }
341
342 #[inline]
345 fn light_tint_at(&self, world_pos: Point3<f32>) -> Rgb565 {
346 let mut r = 0u32;
347 let mut g = 0u32;
348 let mut b = 0u32;
349 for light in &self.point_lights {
350 let c = light.contribution_at(world_pos);
351 r += c.r() as u32;
352 g += c.g() as u32;
353 b += c.b() as u32;
354 }
355 Rgb565::new(r.min(31) as u8, g.min(63) as u8, b.min(31) as u8)
356 }
357
358 #[inline]
360 fn add_tint(base: Rgb565, tint: Rgb565) -> Rgb565 {
361 Rgb565::new(
362 (base.r() as u16 + tint.r() as u16).min(31) as u8,
363 (base.g() as u16 + tint.g() as u16).min(63) as u8,
364 (base.b() as u16 + tint.b() as u16).min(31) as u8,
365 )
366 }
367
368 const DOOM_LIGHT_TABLE: [u8; 32] = [
370 8, 12, 16, 20, 24, 28, 34, 40, 48, 56, 64, 72, 82, 92, 102, 112, 124, 136, 148, 160, 172,
371 184, 196, 206, 216, 224, 232, 238, 244, 248, 252, 255,
372 ];
373
374 #[inline]
375 fn sector_shaded_color(
376 &self,
377 base: Rgb565,
378 brightness: u8,
379 face_center: Point3<f32>,
380 ) -> Rgb565 {
381 let level_u8 = match self.light_levels {
382 crate::retro::LightLevels::Linear => brightness,
383 crate::retro::LightLevels::Doom32 => {
384 let base_level = (brightness as usize * 31) / 255;
385 let distance = (face_center - self.camera.position).norm();
386 let distance_drop = (distance * 2.0) as usize;
388 let idx = base_level.saturating_sub(distance_drop).min(31);
389 Self::DOOM_LIGHT_TABLE[idx]
390 }
391 };
392
393 let factor = level_u8 as f32 / 255.0;
394 Rgb565::new(
395 (base.r() as f32 * factor) as u8,
396 (base.g() as f32 * factor) as u8,
397 (base.b() as f32 * factor) as u8,
398 )
399 }
400
401 #[inline]
403 fn face_world_center(
404 face: &[usize; 3],
405 vertices: &[[f32; 3]],
406 model_matrix: Matrix4<f32>,
407 ) -> Point3<f32> {
408 let v0 = vertices[face[0]];
409 let v1 = vertices[face[1]];
410 let v2 = vertices[face[2]];
411 let cx = (v0[0] + v1[0] + v2[0]) / 3.0;
412 let cy = (v0[1] + v1[1] + v2[1]) / 3.0;
413 let cz = (v0[2] + v1[2] + v2[2]) / 3.0;
414 model_matrix.transform_point(&Point3::new(cx, cy, cz))
415 }
416
417 pub fn set_caps(&mut self, caps: crate::config::ProfileCaps) {
418 self.caps = Some(caps);
419 self.apply_render_defaults(crate::config::render_defaults_for_profile(caps));
420 }
421
422 pub fn clear_caps(&mut self) {
423 self.caps = None;
424 }
425
426 pub fn set_quality_tier(&mut self, tier: crate::config::QualityTier) {
427 self.quality_tier = tier;
428 }
429
430 pub fn set_material_profile(&mut self, profile: crate::config::MaterialProfile) {
431 self.material_profile = profile;
432 }
433
434 pub fn apply_render_defaults(&mut self, defaults: crate::config::RenderDefaults) {
435 self.quality_tier = defaults.quality_tier;
436 self.material_profile = defaults.material_profile;
437 }
438
439 fn resolve_render_mode(&self, mode: &RenderMode) -> RenderMode {
440 use crate::config::{MaterialProfile, QualityTier};
441 match self.quality_tier {
442 QualityTier::Fastest => match mode {
443 RenderMode::BlinnPhong { .. }
444 | RenderMode::GouraudLightDir(_)
445 | RenderMode::SolidLightDir(_) => RenderMode::Solid,
446 _ => mode.clone(),
447 },
448 QualityTier::Balanced => match (self.material_profile, mode) {
449 (MaterialProfile::Unlit, RenderMode::BlinnPhong { .. })
450 | (MaterialProfile::Unlit, RenderMode::GouraudLightDir(_))
451 | (MaterialProfile::Unlit, RenderMode::SolidLightDir(_)) => RenderMode::Solid,
452 (MaterialProfile::Lambert, RenderMode::BlinnPhong { light_dir, .. }) => {
453 RenderMode::SolidLightDir(light_dir.clone())
454 }
455 _ => mode.clone(),
456 },
457 QualityTier::Quality => match (self.material_profile, mode) {
458 (MaterialProfile::Unlit, RenderMode::BlinnPhong { .. })
459 | (MaterialProfile::Unlit, RenderMode::GouraudLightDir(_))
460 | (MaterialProfile::Unlit, RenderMode::SolidLightDir(_)) => RenderMode::Solid,
461 (MaterialProfile::Lambert, RenderMode::BlinnPhong { light_dir, .. }) => {
462 RenderMode::SolidLightDir(light_dir.clone())
463 }
464 _ => mode.clone(),
465 },
466 }
467 }
468
469 #[inline]
472 fn should_cull_mesh(&self, mesh: &K3dMesh) -> bool {
473 let mesh_pos = mesh.get_position();
475
476 let to_mesh = mesh_pos - self.camera.position;
478 let distance = to_mesh.norm(); let radius_sq = mesh.compute_bounding_radius_sq();
483 let radius = radius_sq.sqrt(); if distance - radius > self.camera.far {
487 return true;
488 }
489
490 if distance + radius < self.camera.near {
492 return true;
493 }
494
495 false
497 }
498
499 #[inline(always)]
500 fn transform_point(&self, point: &[f32; 3], model_matrix: Matrix4<f32>) -> Option<Point3<i32>> {
501 #[cfg(feature = "fixed-transform")]
502 {
503 return self.transform_point_fixed(point, model_matrix);
504 }
505 #[cfg(not(feature = "fixed-transform"))]
506 {
507 let point = nalgebra::Vector4::new(point[0], point[1], point[2], 1.0);
508 let point = model_matrix * point;
509
510 if point.w < 0.0 {
511 return None;
512 }
513 if point.w < self.camera.near || point.w > self.camera.far {
522 return None;
523 }
524
525 let point = Point3::from_homogeneous(point)?;
526
527 let x = ((1.0 + point.x) * 0.5 * self.width as f32) as i32;
528 let y = ((1.0 - point.y) * 0.5 * self.height as f32) as i32;
529
530 if x < 0 || x >= self.width as i32 || y < 0 || y >= self.height as i32 {
531 return None;
532 }
533
534 Some(Point3::new(
535 x,
536 y,
537 (point.z * (self.camera.far - self.camera.near) + self.camera.near) as i32,
538 ))
539 }
540 }
541
542 #[cfg(feature = "fixed-transform")]
543 #[inline(always)]
544 fn transform_point_fixed(
545 &self,
546 point: &[f32; 3],
547 model_matrix: Matrix4<f32>,
548 ) -> Option<Point3<i32>> {
549 use crate::fixed_math::{div_fp, from_fp, to_fp};
550
551 let point = nalgebra::Vector4::new(point[0], point[1], point[2], 1.0);
552 let point = model_matrix * point;
553
554 if point.w <= 0.0 {
555 return None;
556 }
557 if point.w < self.camera.near || point.w > self.camera.far {
562 return None;
563 }
564
565 let x_fp = div_fp(to_fp(point.x), to_fp(point.w))?;
566 let y_fp = div_fp(to_fp(point.y), to_fp(point.w))?;
567 let z_ndc = from_fp(div_fp(to_fp(point.z), to_fp(point.w))?);
568
569 let x = ((1.0 + from_fp(x_fp)) * 0.5 * self.width as f32) as i32;
570 let y = ((1.0 - from_fp(y_fp)) * 0.5 * self.height as f32) as i32;
571
572 if x < 0 || x >= self.width as i32 || y < 0 || y >= self.height as i32 {
573 return None;
574 }
575
576 Some(Point3::new(
577 x,
578 y,
579 (z_ndc * (self.camera.far - self.camera.near) + self.camera.near) as i32,
580 ))
581 }
582
583 #[inline(always)]
584 pub fn transform_points<const N: usize>(
585 &self,
586 indices: &[usize; N],
587 vertices: &[[f32; 3]],
588 model_matrix: Matrix4<f32>,
589 ) -> Option<[Point3<i32>; N]> {
590 let mut ret = [Point3::new(0, 0, 0); N];
591
592 for i in 0..N {
593 ret[i] = self.transform_point(&vertices[indices[i]], model_matrix)?;
594 }
595
596 Some(ret)
597 }
598
599 fn transform_point_with_w(
602 &self,
603 point: &[f32; 3],
604 model_matrix: Matrix4<f32>,
605 ) -> Option<(Point3<i32>, f32)> {
606 let v = nalgebra::Vector4::new(point[0], point[1], point[2], 1.0);
607 let clip = model_matrix * v;
608 if clip.w < self.camera.near || clip.w > self.camera.far {
613 return None;
614 }
615 let ndc_x = clip.x / clip.w;
616 let ndc_y = clip.y / clip.w;
617 let ndc_z = clip.z / clip.w;
618 let x = ((1.0 + ndc_x) * 0.5 * self.width as f32) as i32;
619 let y = ((1.0 - ndc_y) * 0.5 * self.height as f32) as i32;
620 if x < 0 || x >= self.width as i32 || y < 0 || y >= self.height as i32 {
621 return None;
622 }
623 let z = (ndc_z * (self.camera.far - self.camera.near) + self.camera.near) as i32;
624 Some((Point3::new(x, y, z), clip.w))
625 }
626
627 #[inline(always)]
629 pub fn transform_points_with_w<const N: usize>(
630 &self,
631 indices: &[usize; N],
632 vertices: &[[f32; 3]],
633 model_matrix: Matrix4<f32>,
634 ) -> Option<([Point3<i32>; N], [f32; N])> {
635 let mut pts = [Point3::new(0, 0, 0); N];
636 let mut ws = [1.0f32; N];
637 for i in 0..N {
638 let (p, w) = self.transform_point_with_w(&vertices[indices[i]], model_matrix)?;
639 pts[i] = p;
640 ws[i] = w;
641 }
642 Some((pts, ws))
643 }
644
645 #[inline]
656 fn is_backface(
657 &self,
658 face: &[usize; 3],
659 vertices: &[[f32; 3]],
660 model_matrix: Matrix4<f32>,
661 world_normal: &Vector3<f32>,
662 ) -> bool {
663 let v0 = vertices[face[0]];
664 let v0_world = if model_matrix == Matrix4::identity() {
665 Point3::new(v0[0], v0[1], v0[2])
666 } else {
667 model_matrix.transform_point(&Point3::new(v0[0], v0[1], v0[2]))
668 };
669 (self.camera.position - v0_world).dot(world_normal) < 0.0
670 }
671
672 #[inline]
690 fn clip_to_screen(&self, c: Vector4<f32>) -> Option<Point3<i32>> {
691 if c.w <= 0.0 {
692 return None;
693 }
694 let mut ndc = Point3::from_homogeneous(c)?;
695 if self.vertex_snap_bits > 0 {
696 let scale = (1u32 << self.vertex_snap_bits) as f32;
697 ndc.x = (ndc.x * scale).round() / scale;
698 ndc.y = (ndc.y * scale).round() / scale;
699 }
700 let w = self.width as f32;
701 let h = self.height as f32;
702 let x = ((1.0 + ndc.x) * 0.5 * w).clamp(-w * 8.0, w * 9.0) as i32;
703 let y = ((1.0 - ndc.y) * 0.5 * h).clamp(-h * 8.0, h * 9.0) as i32;
704 let depth = (ndc.z * (self.camera.far - self.camera.near) + self.camera.near) as i32;
705 Some(Point3::new(x, y, depth))
706 }
707
708 #[inline]
711 fn project_and_emit<F>(
712 &self,
713 c0: Vector4<f32>,
714 c1: Vector4<f32>,
715 c2: Vector4<f32>,
716 color: Rgb565,
717 callback: &mut F,
718 ) where
719 F: FnMut(DrawPrimitive),
720 {
721 if let (Some(p0), Some(p1), Some(p2)) = (
722 self.clip_to_screen(c0),
723 self.clip_to_screen(c1),
724 self.clip_to_screen(c2),
725 ) {
726 callback(DrawPrimitive::ColoredTriangleWithDepth {
727 points: [p0.xy(), p1.xy(), p2.xy()],
728 depths: [p0.z as f32, p1.z as f32, p2.z as f32],
729 color,
730 });
731 }
732 }
733
734 fn clip_polygon_plane(
739 input: &[Vector4<f32>],
740 output: &mut [Vector4<f32>; 8],
741 dist: impl Fn(Vector4<f32>) -> f32,
742 ) -> usize {
743 let n = input.len();
744 let mut m = 0usize;
745 for i in 0..n {
746 let prev = input[(n + i - 1) % n];
747 let curr = input[i];
748 let d_prev = dist(prev);
749 let d_curr = dist(curr);
750 if d_curr >= 0.0 {
751 if d_prev < 0.0 {
752 let t = d_prev / (d_prev - d_curr);
754 if m < 8 {
755 output[m] = prev + (curr - prev) * t;
756 m += 1;
757 }
758 }
759 if m < 8 {
760 output[m] = curr;
761 m += 1;
762 }
763 } else if d_prev >= 0.0 {
764 let t = d_prev / (d_prev - d_curr);
766 if m < 8 {
767 output[m] = prev + (curr - prev) * t;
768 m += 1;
769 }
770 }
771 }
772 m
773 }
774
775 fn emit_clipped<F>(&self, clip: [Vector4<f32>; 3], color: Rgb565, callback: &mut F)
784 where
785 F: FnMut(DrawPrimitive),
786 {
787 let nw = self.camera.near;
788
789 let mut a = [Vector4::zeros(); 8];
790 let mut b = [Vector4::zeros(); 8];
791 a[0] = clip[0];
792 a[1] = clip[1];
793 a[2] = clip[2];
794
795 let n = Self::clip_polygon_plane(&a[..3], &mut b, |v| v.w - nw);
797 if n < 3 {
798 return;
799 }
800 let n = Self::clip_polygon_plane(&b[..n], &mut a, |v| v.x + v.w);
802 if n < 3 {
803 return;
804 }
805 let n = Self::clip_polygon_plane(&a[..n], &mut b, |v| v.w - v.x);
807 if n < 3 {
808 return;
809 }
810 let n = Self::clip_polygon_plane(&b[..n], &mut a, |v| v.y + v.w);
812 if n < 3 {
813 return;
814 }
815 let n = Self::clip_polygon_plane(&a[..n], &mut b, |v| v.w - v.y);
817 if n < 3 {
818 return;
819 }
820
821 for i in 1..n - 1 {
823 self.project_and_emit(b[0], b[i], b[i + 1], color, callback);
824 }
825 }
826
827 fn render<'a, MS, F>(&self, meshes: MS, mut callback: F)
828 where
829 MS: IntoIterator<Item = &'a K3dMesh<'a>>,
830 F: FnMut(DrawPrimitive),
831 {
832 for mesh in meshes {
833 if mesh.geometry.vertices.is_empty() {
834 continue;
835 }
836
837 if self.should_cull_mesh(mesh) {
841 continue;
842 }
843
844 let mesh_pos = mesh.get_position();
846 let distance = (mesh_pos - self.camera.position).norm();
847 let geometry = mesh.select_lod(distance);
848
849 let transform_matrix = self.camera.vp_matrix * mesh.model_matrix;
850
851 let mut v_cache_plain: [Option<Option<Point3<i32>>>; 256] = [None; 256];
852 let mut v_cache_w: [Option<Option<(Point3<i32>, f32)>>; 256] = [None; 256];
853
854 let mut get_pt = |idx: usize| -> Option<Point3<i32>> {
855 if idx < 256 {
856 if let Some(cached) = v_cache_plain[idx] {
857 return cached;
858 }
859 let p = self.transform_point(&geometry.vertices[idx], transform_matrix);
860 v_cache_plain[idx] = Some(p);
861 p
862 } else {
863 self.transform_point(&geometry.vertices[idx], transform_matrix)
864 }
865 };
866
867 let mut get_pt_w = |idx: usize| -> Option<(Point3<i32>, f32)> {
868 if idx < 256 {
869 if let Some(cached) = v_cache_w[idx] {
870 return cached;
871 }
872 let p = self.transform_point_with_w(&geometry.vertices[idx], transform_matrix);
873 v_cache_w[idx] = Some(p);
874 p
875 } else {
876 self.transform_point_with_w(&geometry.vertices[idx], transform_matrix)
877 }
878 };
879
880 let mut tf_face = |face: &[usize; 3]| -> Option<[Point3<i32>; 3]> {
881 Some([get_pt(face[0])?, get_pt(face[1])?, get_pt(face[2])?])
882 };
883
884 let mut tf_face_w = |face: &[usize; 3]| -> Option<([Point3<i32>; 3], [f32; 3])> {
885 let (p0, w0) = get_pt_w(face[0])?;
886 let (p1, w1) = get_pt_w(face[1])?;
887 let (p2, w2) = get_pt_w(face[2])?;
888 Some(([p0, p1, p2], [w0, w1, w2]))
889 };
890
891 let render_mode = self.resolve_render_mode(&mesh.render_mode);
892 match render_mode {
893 RenderMode::Points => {
894 let screen_space_points = (0..geometry.vertices.len()).filter_map(&mut get_pt);
895
896 if geometry.colors.len() == geometry.vertices.len() {
897 for (point, color) in screen_space_points.zip(geometry.colors) {
898 callback(DrawPrimitive::ColoredPoint(point.xy(), *color));
899 }
900 } else {
901 for point in screen_space_points {
902 callback(DrawPrimitive::ColoredPoint(point.xy(), mesh.color));
903 }
904 }
905 }
906
907 RenderMode::Lines if !geometry.lines.is_empty() => {
908 for line in geometry.lines {
909 if let (Some(p1), Some(p2)) = (get_pt(line[0]), get_pt(line[1])) {
910 callback(DrawPrimitive::Line([p1.xy(), p2.xy()], mesh.color));
911 }
912 }
913 }
914
915 RenderMode::Lines if !geometry.faces.is_empty() => {
916 for face in geometry.faces {
917 if let Some([p1, p2, p3]) = tf_face(face) {
918 callback(DrawPrimitive::Line([p1.xy(), p2.xy()], mesh.color));
919 callback(DrawPrimitive::Line([p2.xy(), p3.xy()], mesh.color));
920 callback(DrawPrimitive::Line([p3.xy(), p1.xy()], mesh.color));
921 }
922 }
923 }
924
925 RenderMode::Lines => {}
926
927 RenderMode::SolidLightDir(direction) => {
928 let color_as_float = Vector3::new(
929 mesh.color.r() as f32 / 32.0,
930 mesh.color.g() as f32 / 64.0,
931 mesh.color.b() as f32 / 32.0,
932 );
933 let ambient_color = color_as_float * 0.1;
934 let adjusted_dir = Vector3::new(direction.x, direction.y, -direction.z);
935
936 for (face, normal) in geometry.faces.iter().zip(geometry.normals.iter()) {
937 let normal = Vector3::new(normal[0], normal[1], normal[2]);
938 let transformed_normal = mesh.model_matrix.transform_vector(&normal);
939 if self.is_backface(
940 face,
941 geometry.vertices,
942 mesh.model_matrix,
943 &transformed_normal,
944 ) {
945 continue;
946 }
947
948 if let Some([p1, p2, p3]) = tf_face(face) {
949 let intensity = transformed_normal.dot(&adjusted_dir).max(0.0);
950 let final_color = color_as_float * intensity + ambient_color;
951 let final_color = Vector3::new(
952 final_color.x.clamp(0.0, 1.0),
953 final_color.y.clamp(0.0, 1.0),
954 final_color.z.clamp(0.0, 1.0),
955 );
956 let mut color = Rgb565::new(
957 (final_color.x * 31.0) as u8,
958 (final_color.y * 63.0) as u8,
959 (final_color.z * 31.0) as u8,
960 );
961 if !self.point_lights.is_empty() {
962 let wc = Self::face_world_center(
963 face,
964 geometry.vertices,
965 mesh.model_matrix,
966 );
967 color = Self::add_tint(color, self.light_tint_at(wc));
968 }
969 callback(DrawPrimitive::ColoredTriangleWithDepth {
970 points: [p1.xy(), p2.xy(), p3.xy()],
971 depths: [p1.z as f32, p2.z as f32, p3.z as f32],
972 color,
973 });
974 }
975 }
976 }
977
978 RenderMode::GouraudLightDir(direction) => {
979 let color_as_float = Vector3::new(
980 mesh.color.r() as f32 / 32.0,
981 mesh.color.g() as f32 / 64.0,
982 mesh.color.b() as f32 / 32.0,
983 );
984 let ambient_color = color_as_float * 0.1;
985 let adjusted_dir = Vector3::new(direction.x, direction.y, -direction.z);
986
987 for (face, face_normal) in geometry.faces.iter().zip(geometry.normals.iter()) {
988 let fn_vec = Vector3::new(face_normal[0], face_normal[1], face_normal[2]);
989 let transformed_fn = mesh.model_matrix.transform_vector(&fn_vec);
990
991 if self.is_backface(
992 face,
993 geometry.vertices,
994 mesh.model_matrix,
995 &transformed_fn,
996 ) {
997 continue;
998 }
999
1000 if let Some([p1, p2, p3]) = tf_face(face) {
1001 let vertex_colors: [Rgb565; 3] = core::array::from_fn(|k| {
1002 let vn = if !geometry.vertex_normals.is_empty() {
1003 let vn_arr = geometry.vertex_normals[face[k]];
1004 let vn_vec = Vector3::new(vn_arr[0], vn_arr[1], vn_arr[2]);
1005 mesh.model_matrix.transform_vector(&vn_vec)
1006 } else {
1007 transformed_fn
1008 };
1009
1010 let intensity = vn.dot(&adjusted_dir).max(0.0);
1011 let c = color_as_float * intensity + ambient_color;
1012 let mut vc = Rgb565::new(
1013 (c.x.clamp(0.0, 1.0) * 31.0) as u8,
1014 (c.y.clamp(0.0, 1.0) * 63.0) as u8,
1015 (c.z.clamp(0.0, 1.0) * 31.0) as u8,
1016 );
1017 if !self.point_lights.is_empty() {
1018 let vpos = geometry.vertices[face[k]];
1019 let wp = mesh
1020 .model_matrix
1021 .transform_point(&Point3::new(vpos[0], vpos[1], vpos[2]));
1022 vc = Self::add_tint(vc, self.light_tint_at(wp));
1023 }
1024 vc
1025 });
1026
1027 callback(DrawPrimitive::GouraudTriangleWithDepth {
1028 points: [p1.xy(), p2.xy(), p3.xy()],
1029 depths: [p1.z as f32, p2.z as f32, p3.z as f32],
1030 colors: vertex_colors,
1031 });
1032 }
1033 }
1034 }
1035
1036 RenderMode::BlinnPhong {
1037 light_dir,
1038 specular_intensity,
1039 shininess,
1040 } => {
1041 let color_as_float = Vector3::new(
1043 mesh.color.r() as f32 / 32.0,
1044 mesh.color.g() as f32 / 64.0,
1045 mesh.color.b() as f32 / 32.0,
1046 );
1047
1048 let ambient_color = color_as_float * 0.1;
1050
1051 let adjusted_light_dir = Vector3::new(light_dir.x, light_dir.y, -light_dir.z);
1054
1055 let light_dir_normalized = adjusted_light_dir.normalize();
1057
1058 for (face, normal) in geometry.faces.iter().zip(geometry.normals.iter()) {
1059 let normal = Vector3::new(normal[0], normal[1], normal[2]);
1061 let transformed_normal = mesh.model_matrix.transform_vector(&normal);
1062 let normalized_normal = transformed_normal.normalize();
1063
1064 if self.is_backface(
1066 face,
1067 geometry.vertices,
1068 mesh.model_matrix,
1069 &normalized_normal,
1070 ) {
1071 continue;
1072 }
1073
1074 if let Some([p1, p2, p3]) = tf_face(face) {
1075 let v0 = geometry.vertices[face[0]];
1077 let v1 = geometry.vertices[face[1]];
1078 let v2 = geometry.vertices[face[2]];
1079 let face_center = Point3::new(
1080 (v0[0] + v1[0] + v2[0]) / 3.0,
1081 (v0[1] + v1[1] + v2[1]) / 3.0,
1082 (v0[2] + v1[2] + v2[2]) / 3.0,
1083 );
1084 let face_center_world = mesh.model_matrix.transform_point(&face_center);
1085
1086 let view_dir = (self.camera.position - face_center_world).normalize();
1088
1089 let half_vector = (light_dir_normalized + view_dir).normalize();
1091
1092 let diffuse_intensity =
1094 normalized_normal.dot(&light_dir_normalized).max(0.0);
1095
1096 let specular_term =
1098 normalized_normal.dot(&half_vector).max(0.0).powf(shininess);
1099
1100 let diffuse_color = color_as_float * diffuse_intensity;
1102 let specular_color =
1103 Vector3::new(1.0, 1.0, 1.0) * specular_term * specular_intensity;
1104 let final_color = ambient_color + diffuse_color + specular_color;
1105
1106 let final_color = Vector3::new(
1107 final_color.x.clamp(0.0, 1.0),
1108 final_color.y.clamp(0.0, 1.0),
1109 final_color.z.clamp(0.0, 1.0),
1110 );
1111
1112 let mut color = Rgb565::new(
1113 (final_color.x * 31.0) as u8,
1114 (final_color.y * 63.0) as u8,
1115 (final_color.z * 31.0) as u8,
1116 );
1117 if !self.point_lights.is_empty() {
1118 color =
1119 Self::add_tint(color, self.light_tint_at(face_center_world));
1120 }
1121 callback(DrawPrimitive::ColoredTriangleWithDepth {
1122 points: [p1.xy(), p2.xy(), p3.xy()],
1123 depths: [p1.z as f32, p2.z as f32, p3.z as f32],
1124 color,
1125 });
1126 }
1127 }
1128 }
1129
1130 RenderMode::Solid => {
1131 if geometry.normals.is_empty() {
1132 for face in geometry.faces.iter() {
1133 let color = if !self.point_lights.is_empty() {
1134 let wc = Self::face_world_center(
1135 face,
1136 geometry.vertices,
1137 mesh.model_matrix,
1138 );
1139 Self::add_tint(mesh.color, self.light_tint_at(wc))
1140 } else {
1141 mesh.color
1142 };
1143 let v = &geometry.vertices;
1144 let clip = [
1145 transform_matrix
1146 * Vector4::new(
1147 v[face[0]][0],
1148 v[face[0]][1],
1149 v[face[0]][2],
1150 1.0,
1151 ),
1152 transform_matrix
1153 * Vector4::new(
1154 v[face[1]][0],
1155 v[face[1]][1],
1156 v[face[1]][2],
1157 1.0,
1158 ),
1159 transform_matrix
1160 * Vector4::new(
1161 v[face[2]][0],
1162 v[face[2]][1],
1163 v[face[2]][2],
1164 1.0,
1165 ),
1166 ];
1167 self.emit_clipped(clip, color, &mut callback);
1168 }
1169 } else {
1170 for (face, normal) in geometry.faces.iter().zip(geometry.normals) {
1171 let normal = Vector3::new(normal[0], normal[1], normal[2]);
1172 let transformed_normal = mesh.model_matrix.transform_vector(&normal);
1173 if self.is_backface(
1174 face,
1175 geometry.vertices,
1176 mesh.model_matrix,
1177 &transformed_normal,
1178 ) {
1179 continue;
1180 }
1181 let color = if !self.point_lights.is_empty() {
1182 let wc = Self::face_world_center(
1183 face,
1184 geometry.vertices,
1185 mesh.model_matrix,
1186 );
1187 Self::add_tint(mesh.color, self.light_tint_at(wc))
1188 } else {
1189 mesh.color
1190 };
1191 let v = &geometry.vertices;
1192 let clip = [
1193 transform_matrix
1194 * Vector4::new(
1195 v[face[0]][0],
1196 v[face[0]][1],
1197 v[face[0]][2],
1198 1.0,
1199 ),
1200 transform_matrix
1201 * Vector4::new(
1202 v[face[1]][0],
1203 v[face[1]][1],
1204 v[face[1]][2],
1205 1.0,
1206 ),
1207 transform_matrix
1208 * Vector4::new(
1209 v[face[2]][0],
1210 v[face[2]][1],
1211 v[face[2]][2],
1212 1.0,
1213 ),
1214 ];
1215 self.emit_clipped(clip, color, &mut callback);
1216 }
1217 }
1218 }
1219
1220 RenderMode::SectorBright(brightness) => {
1221 if geometry.normals.is_empty() {
1222 for face in geometry.faces.iter() {
1223 let wc =
1224 Self::face_world_center(face, geometry.vertices, mesh.model_matrix);
1225 let mut color = self.sector_shaded_color(mesh.color, brightness, wc);
1226 if !self.point_lights.is_empty() {
1227 color = Self::add_tint(color, self.light_tint_at(wc));
1228 }
1229 let v = &geometry.vertices;
1230 let clip = [
1231 transform_matrix
1232 * Vector4::new(
1233 v[face[0]][0],
1234 v[face[0]][1],
1235 v[face[0]][2],
1236 1.0,
1237 ),
1238 transform_matrix
1239 * Vector4::new(
1240 v[face[1]][0],
1241 v[face[1]][1],
1242 v[face[1]][2],
1243 1.0,
1244 ),
1245 transform_matrix
1246 * Vector4::new(
1247 v[face[2]][0],
1248 v[face[2]][1],
1249 v[face[2]][2],
1250 1.0,
1251 ),
1252 ];
1253 self.emit_clipped(clip, color, &mut callback);
1254 }
1255 } else {
1256 for (face, normal) in geometry.faces.iter().zip(geometry.normals) {
1257 let normal = Vector3::new(normal[0], normal[1], normal[2]);
1258 let transformed_normal = mesh.model_matrix.transform_vector(&normal);
1259 if self.is_backface(
1260 face,
1261 geometry.vertices,
1262 mesh.model_matrix,
1263 &transformed_normal,
1264 ) {
1265 continue;
1266 }
1267 let wc =
1268 Self::face_world_center(face, geometry.vertices, mesh.model_matrix);
1269 let mut color = self.sector_shaded_color(mesh.color, brightness, wc);
1270 if !self.point_lights.is_empty() {
1271 color = Self::add_tint(color, self.light_tint_at(wc));
1272 }
1273 let v = &geometry.vertices;
1274 let clip = [
1275 transform_matrix
1276 * Vector4::new(
1277 v[face[0]][0],
1278 v[face[0]][1],
1279 v[face[0]][2],
1280 1.0,
1281 ),
1282 transform_matrix
1283 * Vector4::new(
1284 v[face[1]][0],
1285 v[face[1]][1],
1286 v[face[1]][2],
1287 1.0,
1288 ),
1289 transform_matrix
1290 * Vector4::new(
1291 v[face[2]][0],
1292 v[face[2]][1],
1293 v[face[2]][2],
1294 1.0,
1295 ),
1296 ];
1297 self.emit_clipped(clip, color, &mut callback);
1298 }
1299 }
1300 }
1301
1302 RenderMode::Textured => {
1303 let Some(texture_id) = geometry.texture_id else {
1308 continue;
1309 };
1310 if geometry.uvs.is_empty() {
1311 continue;
1312 }
1313
1314 if geometry.normals.is_empty() {
1320 for face in geometry.faces.iter() {
1321 if let Some((points, ws)) = tf_face_w(face) {
1322 callback(DrawPrimitive::TexturedTriangleWithDepth {
1323 points: [points[0].xy(), points[1].xy(), points[2].xy()],
1324 depths: [
1325 points[0].z as f32,
1326 points[1].z as f32,
1327 points[2].z as f32,
1328 ],
1329 ws,
1330 uvs: [
1331 geometry.uvs[face[0]],
1332 geometry.uvs[face[1]],
1333 geometry.uvs[face[2]],
1334 ],
1335 texture_id,
1336 });
1337 }
1338 }
1339 } else {
1340 for (face, normal) in geometry.faces.iter().zip(geometry.normals) {
1341 let normal = Vector3::new(normal[0], normal[1], normal[2]);
1342 let transformed_normal = mesh.model_matrix.transform_vector(&normal);
1343 if self.is_backface(
1344 face,
1345 geometry.vertices,
1346 mesh.model_matrix,
1347 &transformed_normal,
1348 ) {
1349 continue;
1350 }
1351 if let Some((points, ws)) = tf_face_w(face) {
1352 callback(DrawPrimitive::TexturedTriangleWithDepth {
1353 points: [points[0].xy(), points[1].xy(), points[2].xy()],
1354 depths: [
1355 points[0].z as f32,
1356 points[1].z as f32,
1357 points[2].z as f32,
1358 ],
1359 ws,
1360 uvs: [
1361 geometry.uvs[face[0]],
1362 geometry.uvs[face[1]],
1363 geometry.uvs[face[2]],
1364 ],
1365 texture_id,
1366 });
1367 }
1368 }
1369 }
1370 }
1371 }
1372 }
1373 }
1374
1375 pub fn record<'a, MS, const MAX: usize>(
1376 &self,
1377 meshes: MS,
1378 commands: &mut crate::command_buffer::CommandBuffer<MAX>,
1379 telemetry: Option<&mut crate::telemetry::RecordTelemetry>,
1380 ) -> Result<(), crate::error::RenderError>
1381 where
1382 MS: IntoIterator<Item = &'a K3dMesh<'a>>,
1383 {
1384 self.record_impl(meshes, commands, telemetry)
1385 }
1386
1387 fn record_impl<'a, MS, const MAX: usize>(
1388 &self,
1389 meshes: MS,
1390 commands: &mut crate::command_buffer::CommandBuffer<MAX>,
1391 telemetry: Option<&mut crate::telemetry::RecordTelemetry>,
1392 ) -> Result<(), crate::error::RenderError>
1393 where
1394 MS: IntoIterator<Item = &'a K3dMesh<'a>>,
1395 {
1396 use crate::command_buffer::RenderCommand;
1397 use crate::error::{BudgetKind, RenderError};
1398
1399 commands.clear();
1400 commands.push(RenderCommand::ClearDepth(u32::MAX))?;
1401 if let Some(caps) = self.caps {
1402 caps.validate_framebuffer(self.width as usize, self.height as usize)?;
1403 }
1404
1405 let mut first_error = None;
1406 let mut visible_meshes = 0usize;
1407 let mut used_texture_ids: heapless::Vec<u32, 64> = heapless::Vec::new();
1408 let mut meshes_total = 0usize;
1409
1410 for mesh in meshes {
1411 meshes_total += 1;
1412 if mesh.geometry.vertices.is_empty() {
1413 continue;
1414 }
1415 if self.should_cull_mesh(mesh) {
1416 continue;
1417 }
1418
1419 let distance = (mesh.get_position() - self.camera.position).norm();
1420 let geometry = mesh.select_lod(distance);
1421
1422 if let Some(caps) = self.caps {
1423 visible_meshes += 1;
1424 if visible_meshes > caps.max_meshes_per_frame {
1425 return Err(RenderError::OutOfBudget(BudgetKind::MeshesPerFrame {
1426 attempted: visible_meshes,
1427 max: caps.max_meshes_per_frame,
1428 }));
1429 }
1430
1431 if geometry.vertices.len() > caps.max_vertices_per_mesh {
1432 return Err(RenderError::OutOfBudget(BudgetKind::VerticesPerMesh {
1433 attempted: geometry.vertices.len(),
1434 max: caps.max_vertices_per_mesh,
1435 }));
1436 }
1437
1438 if geometry.faces.len() > caps.max_triangles_per_mesh {
1439 return Err(RenderError::OutOfBudget(BudgetKind::TrianglesPerMesh {
1440 attempted: geometry.faces.len(),
1441 max: caps.max_triangles_per_mesh,
1442 }));
1443 }
1444
1445 if let Some(texture_id) = geometry.texture_id
1446 && !used_texture_ids.iter().any(|id| *id == texture_id)
1447 {
1448 let attempted = used_texture_ids.len() + 1;
1449 if attempted > caps.max_textures {
1450 return Err(RenderError::OutOfBudget(BudgetKind::Textures {
1451 attempted,
1452 max: caps.max_textures,
1453 }));
1454 }
1455
1456 if used_texture_ids.push(texture_id).is_err() {
1457 return Err(RenderError::OutOfBudget(BudgetKind::Textures {
1458 attempted,
1459 max: caps.max_textures,
1460 }));
1461 }
1462 }
1463 }
1464
1465 self.render(core::iter::once(mesh), |primitive| {
1466 if first_error.is_none()
1467 && let Err(e) = commands.push(RenderCommand::Draw(primitive))
1468 {
1469 first_error = Some(e);
1470 }
1471 });
1472 if let Some(err) = first_error {
1473 return Err(err);
1474 }
1475 }
1476
1477 if let Some(t) = telemetry {
1478 t.meshes_total = meshes_total;
1479 t.meshes_visible = visible_meshes;
1480 t.unique_textures = used_texture_ids.len();
1481 t.draw_commands = commands
1482 .iter()
1483 .filter(|cmd| matches!(cmd, RenderCommand::Draw(_)))
1484 .count();
1485 t.fallback_used = false;
1486 t.degradation_steps_applied = 0;
1487 t.dropped_meshes = 0;
1488 }
1489
1490 Ok(())
1491 }
1492
1493 pub fn record_with_fallback<'a, MS, FS, const MAX: usize>(
1494 &self,
1495 primary: MS,
1496 fallback: FS,
1497 commands: &mut crate::command_buffer::CommandBuffer<MAX>,
1498 telemetry: Option<&mut crate::telemetry::RecordTelemetry>,
1499 ) -> Result<BudgetFallbackOutcome, crate::error::RenderError>
1500 where
1501 MS: IntoIterator<Item = &'a K3dMesh<'a>>,
1502 FS: IntoIterator<Item = &'a K3dMesh<'a>>,
1503 {
1504 use crate::error::RenderError;
1505
1506 let mut local_telemetry = crate::telemetry::RecordTelemetry::default();
1507 match self.record_impl(primary, commands, Some(&mut local_telemetry)) {
1508 Ok(()) => {
1509 if let Some(t) = telemetry {
1510 *t = local_telemetry;
1511 t.fallback_used = false;
1512 }
1513 Ok(BudgetFallbackOutcome {
1514 used_fallback: false,
1515 primary_budget_error: None,
1516 })
1517 }
1518 Err(RenderError::OutOfBudget(kind)) => {
1519 let mut fallback_telemetry = crate::telemetry::RecordTelemetry::default();
1520 self.record_impl(fallback, commands, Some(&mut fallback_telemetry))?;
1521 if let Some(t) = telemetry {
1522 *t = fallback_telemetry;
1523 t.fallback_used = true;
1524 }
1525 Ok(BudgetFallbackOutcome {
1526 used_fallback: true,
1527 primary_budget_error: Some(kind),
1528 })
1529 }
1530 Err(e) => Err(e),
1531 }
1532 }
1533
1534 fn downgraded_quality_tier(tier: crate::config::QualityTier) -> crate::config::QualityTier {
1535 use crate::config::QualityTier;
1536 match tier {
1537 QualityTier::Quality => QualityTier::Balanced,
1538 QualityTier::Balanced => QualityTier::Fastest,
1539 QualityTier::Fastest => QualityTier::Fastest,
1540 }
1541 }
1542
1543 pub fn record_with_degradation<'a, const MAX: usize>(
1544 &mut self,
1545 meshes: &[&'a K3dMesh<'a>],
1546 commands: &mut crate::command_buffer::CommandBuffer<MAX>,
1547 policy: crate::config::DegradationPolicy<'_>,
1548 telemetry: Option<&mut crate::telemetry::RecordTelemetry>,
1549 ) -> Result<DegradationOutcome, crate::error::RenderError> {
1550 use crate::config::DegradationStep;
1551 use crate::error::RenderError;
1552
1553 let original_quality = self.quality_tier;
1554 let mut active_quality = self.quality_tier;
1555
1556 let mut outcome = DegradationOutcome {
1557 used_degradation: false,
1558 steps_applied: 0,
1559 dropped_meshes: 0,
1560 final_quality_tier: active_quality,
1561 primary_budget_error: None,
1562 };
1563
1564 let mut local_telemetry = crate::telemetry::RecordTelemetry::default();
1565 match self.record_impl(meshes.iter().copied(), commands, Some(&mut local_telemetry)) {
1566 Ok(()) => {
1567 if let Some(t) = telemetry {
1568 *t = local_telemetry;
1569 }
1570 return Ok(outcome);
1571 }
1572 Err(RenderError::OutOfBudget(kind)) => {
1573 outcome.primary_budget_error = Some(kind);
1574 }
1575 Err(e) => return Err(e),
1576 }
1577
1578 for step in policy.steps {
1579 outcome.used_degradation = true;
1580 outcome.steps_applied += 1;
1581
1582 let mut selected: heapless::Vec<&K3dMesh<'_>, 512> = heapless::Vec::new();
1583 match *step {
1584 DegradationStep::RaisePriorityFloor(min_priority) => {
1585 for mesh in meshes {
1586 if mesh.priority >= min_priority {
1587 let _ = selected.push(*mesh);
1588 } else {
1589 outcome.dropped_meshes += 1;
1590 }
1591 }
1592 }
1593 DegradationStep::MeshDecimationStride(stride) => {
1594 if stride == 0 {
1595 self.quality_tier = original_quality;
1596 return Err(RenderError::InvalidInput(
1597 "mesh decimation stride must be >= 1",
1598 ));
1599 }
1600 for (idx, mesh) in meshes.iter().enumerate() {
1601 if idx % stride == 0 {
1602 let _ = selected.push(*mesh);
1603 } else {
1604 outcome.dropped_meshes += 1;
1605 }
1606 }
1607 }
1608 DegradationStep::DowngradeQuality => {
1609 active_quality = Self::downgraded_quality_tier(active_quality);
1610 self.quality_tier = active_quality;
1611 for mesh in meshes {
1612 let _ = selected.push(*mesh);
1613 }
1614 }
1615 }
1616
1617 if selected.is_empty() {
1618 continue;
1619 }
1620
1621 let mut step_telemetry = crate::telemetry::RecordTelemetry::default();
1622 let attempt = self.record_impl(
1623 selected.iter().copied(),
1624 commands,
1625 Some(&mut step_telemetry),
1626 );
1627
1628 if let Ok(()) = attempt {
1629 outcome.final_quality_tier = self.quality_tier;
1630 if let Some(t) = telemetry {
1631 *t = step_telemetry;
1632 t.fallback_used = true;
1633 t.degradation_steps_applied = outcome.steps_applied;
1634 t.dropped_meshes = outcome.dropped_meshes;
1635 }
1636 self.quality_tier = original_quality;
1637 return Ok(outcome);
1638 }
1639 }
1640
1641 self.quality_tier = original_quality;
1642 Err(crate::error::RenderError::Recoverable {
1643 fault: crate::error::RuntimeFaultKind::Budget(outcome.primary_budget_error.unwrap_or(
1644 crate::error::BudgetKind::DrawPrimitives {
1645 attempted: commands.len(),
1646 max: MAX,
1647 },
1648 )),
1649 action: crate::error::RecoveryAction::SkipFrame,
1650 })
1651 }
1652
1653 pub fn execute<D, const MAX: usize>(
1654 &self,
1655 fb: &mut D,
1656 frame: &mut crate::renderer::FrameCtx<'_>,
1657 commands: &crate::command_buffer::CommandBuffer<MAX>,
1658 telemetry: Option<&mut crate::telemetry::ExecuteTelemetry>,
1659 ) -> Result<Option<crate::renderer::DirtyRegion>, crate::error::RenderError>
1660 where
1661 D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>
1662 + embedded_graphics_core::prelude::OriginDimensions,
1663 <D as embedded_graphics_core::draw_target::DrawTarget>::Error: core::fmt::Debug,
1664 {
1665 if let Some(t) = telemetry {
1666 t.commands_total = commands.len();
1667 t.draw_commands = commands
1668 .iter()
1669 .filter(|cmd| matches!(cmd, crate::command_buffer::RenderCommand::Draw(_)))
1670 .count();
1671 t.clear_color_commands = commands
1672 .iter()
1673 .filter(|cmd| matches!(cmd, crate::command_buffer::RenderCommand::ClearColor(_)))
1674 .count();
1675 t.clear_depth_commands = commands
1676 .iter()
1677 .filter(|cmd| matches!(cmd, crate::command_buffer::RenderCommand::ClearDepth(_)))
1678 .count();
1679 }
1680 let camera_dir = self.camera.get_direction();
1681 crate::renderer::execute_commands_with_dirty_region_effects(
1682 fb,
1683 frame,
1684 commands,
1685 self.fog.as_ref(),
1686 self.dither.as_ref(),
1687 self.screen_tint,
1688 self.stipple_mode,
1689 self.palette_mode,
1690 self.sky,
1691 [camera_dir.x, camera_dir.y, camera_dir.z],
1692 )
1693 }
1694
1695 pub fn execute_with_textures<D, const MAX: usize, const N: usize>(
1706 &self,
1707 fb: &mut D,
1708 frame: &mut crate::renderer::FrameCtx<'_>,
1709 commands: &crate::command_buffer::CommandBuffer<MAX>,
1710 texture_manager: &crate::texture::TextureManager<N>,
1711 telemetry: Option<&mut crate::telemetry::ExecuteTelemetry>,
1712 ) -> Result<Option<crate::renderer::DirtyRegion>, crate::error::RenderError>
1713 where
1714 D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>
1715 + embedded_graphics_core::prelude::OriginDimensions,
1716 <D as embedded_graphics_core::draw_target::DrawTarget>::Error: core::fmt::Debug,
1717 {
1718 if let Some(t) = telemetry {
1719 t.commands_total = commands.len();
1720 t.draw_commands = commands
1721 .iter()
1722 .filter(|cmd| matches!(cmd, crate::command_buffer::RenderCommand::Draw(_)))
1723 .count();
1724 t.clear_color_commands = commands
1725 .iter()
1726 .filter(|cmd| matches!(cmd, crate::command_buffer::RenderCommand::ClearColor(_)))
1727 .count();
1728 t.clear_depth_commands = commands
1729 .iter()
1730 .filter(|cmd| matches!(cmd, crate::command_buffer::RenderCommand::ClearDepth(_)))
1731 .count();
1732 }
1733 let camera_dir = self.camera.get_direction();
1734 crate::renderer::execute_commands_with_dirty_region_effects_textured(
1735 fb,
1736 frame,
1737 commands,
1738 texture_manager,
1739 self.fog.as_ref(),
1740 self.dither.as_ref(),
1741 self.screen_tint,
1742 self.stipple_mode,
1743 self.palette_mode,
1744 self.sky,
1745 [camera_dir.x, camera_dir.y, camera_dir.z],
1746 )
1747 }
1748
1749 pub fn execute_tiled<D, const MAX: usize, const BIN_CAP: usize>(
1750 &self,
1751 fb: &mut D,
1752 frame: &mut crate::renderer::FrameCtx<'_>,
1753 commands: &crate::command_buffer::CommandBuffer<MAX>,
1754 tile: crate::tilebin::TileConfig,
1755 ) -> Result<crate::tilebin::TileBinStats, crate::error::RenderError>
1756 where
1757 D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>
1758 + embedded_graphics_core::prelude::OriginDimensions,
1759 <D as embedded_graphics_core::draw_target::DrawTarget>::Error: core::fmt::Debug,
1760 {
1761 let camera_dir = self.camera.get_direction();
1762 crate::renderer::execute_commands_tiled_effects::<D, MAX, BIN_CAP>(
1763 fb,
1764 frame,
1765 commands,
1766 tile,
1767 self.fog.as_ref(),
1768 self.dither.as_ref(),
1769 self.screen_tint,
1770 self.stipple_mode,
1771 self.palette_mode,
1772 self.sky,
1773 [camera_dir.x, camera_dir.y, camera_dir.z],
1774 )
1775 }
1776}
1777
1778#[derive(Debug, Clone, Copy)]
1780pub struct MeshRayCastHit {
1781 pub distance: f32,
1783 pub point: Vector3<f32>,
1785 pub normal: Vector3<f32>,
1787 pub face_index: usize,
1789 pub uv: [f32; 2],
1791}
1792
1793pub fn mesh_ray_cast(
1798 ray_origin: Vector3<f32>,
1799 ray_dir: Vector3<f32>,
1800 geometry: &mesh::Geometry<'_>,
1801 model_matrix: &Matrix4<f32>,
1802 max_distance: f32,
1803) -> Option<MeshRayCastHit> {
1804 let mut nearest: Option<MeshRayCastHit> = None;
1805 let mut min_dist = max_distance;
1806
1807 for (face_index, face) in geometry.faces.iter().enumerate() {
1808 let raw_v0 = geometry.vertices[face[0]];
1809 let raw_v1 = geometry.vertices[face[1]];
1810 let raw_v2 = geometry.vertices[face[2]];
1811
1812 let v0 = model_matrix
1814 .transform_point(&Point3::new(raw_v0[0], raw_v0[1], raw_v0[2]))
1815 .coords;
1816 let v1 = model_matrix
1817 .transform_point(&Point3::new(raw_v1[0], raw_v1[1], raw_v1[2]))
1818 .coords;
1819 let v2 = model_matrix
1820 .transform_point(&Point3::new(raw_v2[0], raw_v2[1], raw_v2[2]))
1821 .coords;
1822
1823 let edge1 = v1 - v0;
1825 let edge2 = v2 - v0;
1826 let h = ray_dir.cross(&edge2);
1827 let det = edge1.dot(&h);
1828
1829 if det.abs() < 1e-6 {
1831 continue;
1832 }
1833
1834 let inv_det = 1.0 / det;
1835 let s = ray_origin - v0;
1836 let bary_u = inv_det * s.dot(&h);
1837 if bary_u < 0.0 || bary_u > 1.0 {
1838 continue;
1839 }
1840
1841 let q = s.cross(&edge1);
1842 let bary_v = inv_det * ray_dir.dot(&q);
1843 if bary_v < 0.0 || bary_u + bary_v > 1.0 {
1844 continue;
1845 }
1846
1847 let t = inv_det * edge2.dot(&q);
1848 if t <= 0.0 || t >= min_dist {
1849 continue;
1850 }
1851
1852 let normal = edge1.cross(&edge2).normalize();
1854
1855 let bary_w = 1.0 - bary_u - bary_v;
1857
1858 let uv = if geometry.uvs.len() > face[0]
1860 && geometry.uvs.len() > face[1]
1861 && geometry.uvs.len() > face[2]
1862 {
1863 let uv0 = geometry.uvs[face[0]];
1864 let uv1 = geometry.uvs[face[1]];
1865 let uv2 = geometry.uvs[face[2]];
1866 [
1867 bary_w * uv0[0] + bary_u * uv1[0] + bary_v * uv2[0],
1868 bary_w * uv0[1] + bary_u * uv1[1] + bary_v * uv2[1],
1869 ]
1870 } else {
1871 [0.0, 0.0]
1872 };
1873
1874 let point = ray_origin + ray_dir * t;
1875 min_dist = t;
1876 nearest = Some(MeshRayCastHit {
1877 distance: t,
1878 point,
1879 normal,
1880 face_index,
1881 uv,
1882 });
1883 }
1884
1885 nearest
1886}
1887
1888#[cfg(test)]
1889mod tests {
1890 extern crate std;
1891 use super::*;
1892
1893 #[test]
1894 fn test_engine_creation() {
1895 let engine = K3dengine::new(640, 480);
1896 assert_eq!(engine.width, 640);
1897 assert_eq!(engine.height, 480);
1898 assert!((engine.camera.get_aspect_ratio() - 640.0 / 480.0).abs() < 0.001);
1899 }
1900
1901 #[test]
1902 fn test_transform_point_basic() {
1903 let engine = K3dengine::new(640, 480);
1904 let transform_matrix = engine.camera.vp_matrix;
1906
1907 let point = [0.0, 0.0, -5.0];
1910 let result = engine.transform_point(&point, transform_matrix);
1911
1912 if let Some(transformed) = result {
1913 assert!(transformed.x >= 0 && transformed.x < 640);
1915 assert!(transformed.y >= 0 && transformed.y < 480);
1916 }
1917 }
1919
1920 #[test]
1921 fn test_transform_point_clamps_out_of_bounds() {
1922 let engine = K3dengine::new(640, 480);
1923 let model_matrix = nalgebra::Matrix4::identity();
1924
1925 let point = [100.0, 100.0, -5.0];
1927 let result = engine.transform_point(&point, model_matrix);
1928 assert!(result.is_none());
1930 }
1931
1932 #[test]
1933 fn test_transform_point_behind_camera() {
1934 let engine = K3dengine::new(640, 480);
1935 let transform_matrix = engine.camera.vp_matrix;
1936
1937 let point = [0.0, 0.0, 1.0];
1939 let _result = engine.transform_point(&point, transform_matrix);
1940 }
1944
1945 #[test]
1946 fn test_transform_point_near_plane_clipping() {
1947 let engine = K3dengine::new(640, 480);
1948 let transform_matrix = engine.camera.vp_matrix;
1954
1955 let point = [0.0, 0.0, -0.1];
1957 let result = engine.transform_point(&point, transform_matrix);
1958 assert!(result.is_none());
1959 }
1960
1961 #[test]
1962 fn test_transform_point_far_plane_clipping() {
1963 let engine = K3dengine::new(640, 480);
1964 let transform_matrix = engine.camera.vp_matrix;
1965
1966 let point = [0.0, 0.0, -1000.0];
1968 let result = engine.transform_point(&point, transform_matrix);
1969 assert!(result.is_none());
1970 }
1971
1972 #[test]
1973 fn test_transform_point_within_near_far_not_culled() {
1974 let engine = K3dengine::new(640, 480);
1980 let transform_matrix = engine.camera.vp_matrix;
1981
1982 let point = [0.0, 0.0, -0.8];
1983 let result = engine.transform_point(&point, transform_matrix);
1984 assert!(
1985 result.is_some(),
1986 "a point at distance 0.8 (within [near=0.4, far=20]) should not be culled"
1987 );
1988 }
1989
1990 #[test]
1991 fn test_transform_points_array() {
1992 let engine = K3dengine::new(640, 480);
1993 let transform_matrix = engine.camera.vp_matrix;
1994
1995 let vertices = [[0.0, 0.0, -5.0], [0.1, 0.0, -5.0], [0.0, 0.1, -5.0]];
1996 let indices = [0, 1, 2];
1997
1998 let result = engine.transform_points(&indices, &vertices, transform_matrix);
1999
2000 if let Some(points) = result {
2002 assert_eq!(points.len(), 3);
2003 }
2004 }
2006
2007 #[test]
2008 fn test_render_empty_faces_mesh() {
2009 let engine = K3dengine::new(640, 480);
2010 let vertices = [[0.0, 0.0, -5.0]]; let geometry = mesh::Geometry {
2012 vertices: &vertices,
2013 faces: &[],
2014 colors: &[],
2015 lines: &[],
2016 normals: &[],
2017 vertex_normals: &[],
2018 uvs: &[],
2019 texture_id: None,
2020 };
2021 let mesh = mesh::K3dMesh::new(geometry);
2022
2023 let mut callback_count = 0;
2024 engine.render(std::iter::once(&mesh), |_| {
2025 callback_count += 1;
2026 });
2027
2028 assert!(callback_count > 0);
2030 }
2031
2032 #[test]
2033 fn test_render_points_mode() {
2034 let engine = K3dengine::new(640, 480);
2035
2036 let vertices = [[0.0, 0.0, -5.0], [0.5, 0.0, -5.0]];
2037
2038 let geometry = mesh::Geometry {
2039 vertices: &vertices,
2040 faces: &[],
2041 colors: &[],
2042 lines: &[],
2043 normals: &[],
2044 vertex_normals: &[],
2045 uvs: &[],
2046 texture_id: None,
2047 };
2048
2049 let mut mesh = mesh::K3dMesh::new(geometry);
2050 mesh.set_render_mode(mesh::RenderMode::Points);
2051
2052 let mut primitives = std::vec::Vec::new();
2053 engine.render(std::iter::once(&mesh), |prim| {
2054 primitives.push(prim);
2055 });
2056
2057 assert!(primitives.len() > 0);
2059 for prim in primitives {
2060 assert!(matches!(prim, DrawPrimitive::ColoredPoint(_, _)));
2061 }
2062 }
2063
2064 #[test]
2065 fn test_render_lines_mode_with_faces() {
2066 let engine = K3dengine::new(640, 480);
2067
2068 let vertices = [[0.0, 0.0, -5.0], [0.5, 0.0, -5.0], [0.0, 0.5, -5.0]];
2069
2070 let faces = [[0, 1, 2]];
2071
2072 let geometry = mesh::Geometry {
2073 vertices: &vertices,
2074 faces: &faces,
2075 colors: &[],
2076 lines: &[],
2077 normals: &[],
2078 vertex_normals: &[],
2079 uvs: &[],
2080 texture_id: None,
2081 };
2082
2083 let mut mesh = mesh::K3dMesh::new(geometry);
2084 mesh.set_render_mode(mesh::RenderMode::Lines);
2085
2086 let mut primitives = std::vec::Vec::new();
2087 engine.render(std::iter::once(&mesh), |prim| {
2088 primitives.push(prim);
2089 });
2090
2091 assert_eq!(primitives.len(), 3);
2093 for prim in primitives {
2094 assert!(matches!(prim, DrawPrimitive::Line(_, _)));
2095 }
2096 }
2097
2098 #[test]
2099 fn test_render_gouraud_light_dir() {
2100 let mut engine = K3dengine::new(640, 480);
2101 engine.camera.set_position(Point3::new(0.0, 0.0, -10.0));
2102 engine.camera.set_target(Point3::new(0.0, 0.0, 0.0));
2103
2104 let vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
2105 let faces = [[0, 1, 2]];
2106 let normals = [[0.0, 0.0, -1.0]]; let vertex_normals = [[0.0, 0.0, -1.0], [0.0, 0.0, -1.0], [0.0, 0.0, -1.0]];
2108
2109 let geometry = mesh::Geometry {
2110 vertices: &vertices,
2111 faces: &faces,
2112 colors: &[],
2113 lines: &[],
2114 normals: &normals,
2115 vertex_normals: &vertex_normals,
2116 uvs: &[],
2117 texture_id: None,
2118 };
2119
2120 let mut mesh = mesh::K3dMesh::new(geometry);
2121 mesh.set_render_mode(mesh::RenderMode::GouraudLightDir(Vector3::new(
2122 0.0, 0.0, 1.0,
2123 )));
2124
2125 let mut primitives = std::vec::Vec::new();
2126 engine.render(std::iter::once(&mesh), |prim| {
2127 primitives.push(prim);
2128 });
2129
2130 assert!(!primitives.is_empty());
2132 for prim in &primitives {
2133 assert!(matches!(
2134 prim,
2135 DrawPrimitive::GouraudTriangleWithDepth { .. }
2136 ));
2137 }
2138 }
2139
2140 #[test]
2147 fn test_solid_inward_normals_interior_camera() {
2148 let mut engine = K3dengine::new(320, 240);
2149 engine
2151 .camera
2152 .set_position(nalgebra::Point3::new(0.0, 0.0, 0.0));
2153 engine
2154 .camera
2155 .set_target(nalgebra::Point3::new(0.0, 0.0, -1.0));
2156
2157 #[rustfmt::skip]
2160 let vertices: &[[f32; 3]] = &[
2161 [-1.0, -1.0, -2.0],
2162 [ 1.0, -1.0, -2.0],
2163 [ 1.0, 1.0, -2.0],
2164 [-1.0, 1.0, -2.0],
2165 ];
2166 let faces: &[[usize; 3]] = &[[0, 1, 2], [0, 2, 3]];
2167 let normals: &[[f32; 3]] = &[[0.0, 0.0, 1.0], [0.0, 0.0, 1.0]]; let geometry = mesh::Geometry {
2170 vertices,
2171 faces,
2172 normals,
2173 colors: &[],
2174 lines: &[],
2175 vertex_normals: &[],
2176 uvs: &[],
2177 texture_id: None,
2178 };
2179 let mut m = mesh::K3dMesh::new(geometry);
2180 m.set_render_mode(mesh::RenderMode::Solid);
2181
2182 let mut count = 0usize;
2183 engine.render(std::iter::once(&m), |_| count += 1);
2184
2185 assert!(
2186 count > 0,
2187 "interior Solid-with-inward-normals emitted 0 primitives — culling is wrong"
2188 );
2189 }
2190}