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