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;
13
14#[allow(unused_imports)]
17use nalgebra::ComplexField;
18
19pub mod animation;
20pub mod billboard;
21pub mod bridge;
22pub mod camera;
23pub mod command_buffer;
24pub mod config;
25pub mod display_backend;
26pub mod draw;
27pub mod error;
28pub mod fixed_math;
29pub mod hardware_profile;
30pub mod hud;
31pub mod lut;
32pub mod mesh;
33#[cfg(feature = "std")]
34pub mod painters;
35#[cfg(feature = "perfcounter")]
36pub mod perfcounter;
37pub mod physics;
38pub mod renderer;
39pub mod scene_format;
40pub mod scene_stream;
41pub mod skeleton;
42pub mod softbody;
43pub mod swapchain;
44pub mod telemetry;
45pub mod texture;
46pub mod tilebin;
47pub mod transform_anim;
48pub mod tween;
49
50pub use embedded_graphics_framebuf::{
52 FrameBuf,
53 backends::{DMACapableFrameBufferBackend, EndianCorrectedBuffer, EndianCorrection},
54};
55
56#[cfg(feature = "aa")]
57pub use draw::ReadPixel;
58
59pub use bridge::{
60 AsEgPoint, AsNalgebraPoint, draw_to, eg_to_nalgebra, nalgebra_to_eg, render_drawable_to_buffer,
61};
62pub use renderer::{DirtyRegion, FrameCtx};
63pub use tilebin::{TileBinStats, TileConfig};
64pub use transform_anim::{AnimationPlayer, SampledTransform, TransformKeyframe, TransformTrack};
65pub use tween::{Easing, Tween, Tween3, apply_easing, lerp, lerp3, scale_rgb565};
66
67#[derive(Debug, Clone)]
68pub enum DrawPrimitive {
69 ColoredPoint(Point2<i32>, Rgb565),
70 Line([Point2<i32>; 2], Rgb565),
71 ColoredTriangle([Point2<i32>; 3], Rgb565),
72 ColoredTriangleWithDepth {
73 points: [Point2<i32>; 3],
74 depths: [f32; 3],
75 color: Rgb565,
76 },
77 GouraudTriangle {
78 points: [Point2<i32>; 3],
79 colors: [Rgb565; 3],
80 },
81 GouraudTriangleWithDepth {
82 points: [Point2<i32>; 3],
83 depths: [f32; 3],
84 colors: [Rgb565; 3],
85 },
86 TexturedTriangle {
87 points: [Point2<i32>; 3],
88 uvs: [[f32; 2]; 3],
89 texture_id: u32,
90 },
91 TexturedTriangleWithDepth {
92 points: [Point2<i32>; 3],
93 depths: [f32; 3],
94 ws: [f32; 3],
95 uvs: [[f32; 2]; 3],
96 texture_id: u32,
97 },
98}
99
100pub struct K3dengine {
101 pub camera: Camera,
102 width: u16,
103 height: u16,
104 caps: Option<crate::config::ProfileCaps>,
105 quality_tier: crate::config::QualityTier,
106 material_profile: crate::config::MaterialProfile,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub struct BudgetFallbackOutcome {
111 pub used_fallback: bool,
112 pub primary_budget_error: Option<crate::error::BudgetKind>,
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub struct DegradationOutcome {
117 pub used_degradation: bool,
118 pub steps_applied: usize,
119 pub dropped_meshes: usize,
120 pub final_quality_tier: crate::config::QualityTier,
121 pub primary_budget_error: Option<crate::error::BudgetKind>,
122}
123
124impl K3dengine {
125 pub fn new(width: u16, height: u16) -> K3dengine {
126 K3dengine {
127 camera: Camera::new(width as f32 / height as f32),
128 width,
129 height,
130 caps: None,
131 quality_tier: crate::config::QualityTier::Balanced,
132 material_profile: crate::config::MaterialProfile::Lambert,
133 }
134 }
135
136 pub fn set_caps(&mut self, caps: crate::config::ProfileCaps) {
137 self.caps = Some(caps);
138 self.apply_render_defaults(crate::config::render_defaults_for_profile(caps));
139 }
140
141 pub fn clear_caps(&mut self) {
142 self.caps = None;
143 }
144
145 pub fn set_quality_tier(&mut self, tier: crate::config::QualityTier) {
146 self.quality_tier = tier;
147 }
148
149 pub fn set_material_profile(&mut self, profile: crate::config::MaterialProfile) {
150 self.material_profile = profile;
151 }
152
153 pub fn apply_render_defaults(&mut self, defaults: crate::config::RenderDefaults) {
154 self.quality_tier = defaults.quality_tier;
155 self.material_profile = defaults.material_profile;
156 }
157
158 fn resolve_render_mode(&self, mode: &RenderMode) -> RenderMode {
159 use crate::config::{MaterialProfile, QualityTier};
160 match self.quality_tier {
161 QualityTier::Fastest => match mode {
162 RenderMode::BlinnPhong { .. }
163 | RenderMode::GouraudLightDir(_)
164 | RenderMode::SolidLightDir(_) => RenderMode::Solid,
165 _ => mode.clone(),
166 },
167 QualityTier::Balanced => match (self.material_profile, mode) {
168 (MaterialProfile::Unlit, RenderMode::BlinnPhong { .. })
169 | (MaterialProfile::Unlit, RenderMode::GouraudLightDir(_))
170 | (MaterialProfile::Unlit, RenderMode::SolidLightDir(_)) => RenderMode::Solid,
171 (MaterialProfile::Lambert, RenderMode::BlinnPhong { light_dir, .. }) => {
172 RenderMode::SolidLightDir(light_dir.clone())
173 }
174 _ => mode.clone(),
175 },
176 QualityTier::Quality => match (self.material_profile, mode) {
177 (MaterialProfile::Unlit, RenderMode::BlinnPhong { .. })
178 | (MaterialProfile::Unlit, RenderMode::GouraudLightDir(_))
179 | (MaterialProfile::Unlit, RenderMode::SolidLightDir(_)) => RenderMode::Solid,
180 (MaterialProfile::Lambert, RenderMode::BlinnPhong { light_dir, .. }) => {
181 RenderMode::SolidLightDir(light_dir.clone())
182 }
183 _ => mode.clone(),
184 },
185 }
186 }
187
188 #[inline]
191 fn should_cull_mesh(&self, mesh: &K3dMesh) -> bool {
192 let mesh_pos = mesh.get_position();
194
195 let to_mesh = mesh_pos - self.camera.position;
197 let distance = to_mesh.norm(); let radius_sq = mesh.compute_bounding_radius_sq();
202 let radius = radius_sq.sqrt(); if distance - radius > self.camera.far {
206 return true;
207 }
208
209 if distance + radius < self.camera.near {
211 return true;
212 }
213
214 false
216 }
217
218 #[inline(always)]
219 fn transform_point(&self, point: &[f32; 3], model_matrix: Matrix4<f32>) -> Option<Point3<i32>> {
220 #[cfg(feature = "fixed-transform")]
221 {
222 return self.transform_point_fixed(point, model_matrix);
223 }
224 #[cfg(not(feature = "fixed-transform"))]
225 {
226 let point = nalgebra::Vector4::new(point[0], point[1], point[2], 1.0);
227 let point = model_matrix * point;
228
229 if point.w < 0.0 {
230 return None;
231 }
232 if point.z < self.camera.near || point.z > self.camera.far {
233 return None;
234 }
235
236 let point = Point3::from_homogeneous(point)?;
237
238 let x = ((1.0 + point.x) * 0.5 * self.width as f32) as i32;
239 let y = ((1.0 - point.y) * 0.5 * self.height as f32) as i32;
240
241 if x < 0 || x >= self.width as i32 || y < 0 || y >= self.height as i32 {
242 return None;
243 }
244
245 Some(Point3::new(
246 x,
247 y,
248 (point.z * (self.camera.far - self.camera.near) + self.camera.near) as i32,
249 ))
250 }
251 }
252
253 #[cfg(feature = "fixed-transform")]
254 #[inline(always)]
255 fn transform_point_fixed(
256 &self,
257 point: &[f32; 3],
258 model_matrix: Matrix4<f32>,
259 ) -> Option<Point3<i32>> {
260 use crate::fixed_math::{div_fp, from_fp, to_fp};
261
262 let point = nalgebra::Vector4::new(point[0], point[1], point[2], 1.0);
263 let point = model_matrix * point;
264
265 if point.w <= 0.0 {
266 return None;
267 }
268
269 let x_fp = div_fp(to_fp(point.x), to_fp(point.w))?;
270 let y_fp = div_fp(to_fp(point.y), to_fp(point.w))?;
271 let z_ndc = from_fp(div_fp(to_fp(point.z), to_fp(point.w))?);
272 if z_ndc < self.camera.near || z_ndc > self.camera.far {
273 return None;
274 }
275
276 let x = ((1.0 + from_fp(x_fp)) * 0.5 * self.width as f32) as i32;
277 let y = ((1.0 - from_fp(y_fp)) * 0.5 * self.height as f32) as i32;
278
279 if x < 0 || x >= self.width as i32 || y < 0 || y >= self.height as i32 {
280 return None;
281 }
282
283 Some(Point3::new(
284 x,
285 y,
286 (z_ndc * (self.camera.far - self.camera.near) + self.camera.near) as i32,
287 ))
288 }
289
290 #[inline(always)]
291 pub fn transform_points<const N: usize>(
292 &self,
293 indices: &[usize; N],
294 vertices: &[[f32; 3]],
295 model_matrix: Matrix4<f32>,
296 ) -> Option<[Point3<i32>; N]> {
297 let mut ret = [Point3::new(0, 0, 0); N];
298
299 for i in 0..N {
300 ret[i] = self.transform_point(&vertices[indices[i]], model_matrix)?;
301 }
302
303 Some(ret)
304 }
305
306 fn transform_point_with_w(
309 &self,
310 point: &[f32; 3],
311 model_matrix: Matrix4<f32>,
312 ) -> Option<(Point3<i32>, f32)> {
313 let v = nalgebra::Vector4::new(point[0], point[1], point[2], 1.0);
314 let clip = model_matrix * v;
315 if clip.w <= 0.0 {
316 return None;
317 }
318 let ndc_x = clip.x / clip.w;
319 let ndc_y = clip.y / clip.w;
320 let ndc_z = clip.z / clip.w;
321 if ndc_z < self.camera.near || ndc_z > self.camera.far {
322 return None;
323 }
324 let x = ((1.0 + ndc_x) * 0.5 * self.width as f32) as i32;
325 let y = ((1.0 - ndc_y) * 0.5 * self.height as f32) as i32;
326 if x < 0 || x >= self.width as i32 || y < 0 || y >= self.height as i32 {
327 return None;
328 }
329 let z = (ndc_z * (self.camera.far - self.camera.near) + self.camera.near) as i32;
330 Some((Point3::new(x, y, z), clip.w))
331 }
332
333 #[inline(always)]
335 pub fn transform_points_with_w<const N: usize>(
336 &self,
337 indices: &[usize; N],
338 vertices: &[[f32; 3]],
339 model_matrix: Matrix4<f32>,
340 ) -> Option<([Point3<i32>; N], [f32; N])> {
341 let mut pts = [Point3::new(0, 0, 0); N];
342 let mut ws = [1.0f32; N];
343 for i in 0..N {
344 let (p, w) = self.transform_point_with_w(&vertices[indices[i]], model_matrix)?;
345 pts[i] = p;
346 ws[i] = w;
347 }
348 Some((pts, ws))
349 }
350
351 fn render<'a, MS, F>(&self, meshes: MS, mut callback: F)
352 where
353 MS: IntoIterator<Item = &'a K3dMesh<'a>>,
354 F: FnMut(DrawPrimitive),
355 {
356 for mesh in meshes {
357 if mesh.geometry.vertices.is_empty() {
358 continue;
359 }
360
361 if self.should_cull_mesh(mesh) {
365 continue;
366 }
367
368 let mesh_pos = mesh.get_position();
370 let distance = (mesh_pos - self.camera.position).norm();
371 let geometry = mesh.select_lod(distance);
372
373 let transform_matrix = self.camera.vp_matrix * mesh.model_matrix;
374
375 let render_mode = self.resolve_render_mode(&mesh.render_mode);
376 match render_mode {
377 RenderMode::Points => {
378 let screen_space_points = geometry
379 .vertices
380 .iter()
381 .filter_map(|v| self.transform_point(v, transform_matrix));
382
383 if geometry.colors.len() == geometry.vertices.len() {
384 for (point, color) in screen_space_points.zip(geometry.colors) {
385 callback(DrawPrimitive::ColoredPoint(point.xy(), *color));
386 }
387 } else {
388 for point in screen_space_points {
389 callback(DrawPrimitive::ColoredPoint(point.xy(), mesh.color));
390 }
391 }
392 }
393
394 RenderMode::Lines if !geometry.lines.is_empty() => {
395 for line in geometry.lines {
396 if let Some([p1, p2]) =
397 self.transform_points(line, geometry.vertices, transform_matrix)
398 {
399 callback(DrawPrimitive::Line([p1.xy(), p2.xy()], mesh.color));
400 }
401 }
402 }
403
404 RenderMode::Lines if !geometry.faces.is_empty() => {
405 for face in geometry.faces {
406 if let Some([p1, p2, p3]) =
407 self.transform_points(face, geometry.vertices, transform_matrix)
408 {
409 callback(DrawPrimitive::Line([p1.xy(), p2.xy()], mesh.color));
410 callback(DrawPrimitive::Line([p2.xy(), p3.xy()], mesh.color));
411 callback(DrawPrimitive::Line([p3.xy(), p1.xy()], mesh.color));
412 }
413 }
414 }
415
416 RenderMode::Lines => {}
417
418 RenderMode::SolidLightDir(direction) => {
419 let color_as_float = Vector3::new(
422 mesh.color.r() as f32 / 32.0,
423 mesh.color.g() as f32 / 64.0,
424 mesh.color.b() as f32 / 32.0,
425 );
426
427 let ambient_color = color_as_float * 0.1;
429
430 let adjusted_dir = Vector3::new(direction.x, direction.y, -direction.z);
433
434 for (face, normal) in geometry.faces.iter().zip(geometry.normals.iter()) {
435 let normal = Vector3::new(normal[0], normal[1], normal[2]);
437
438 let transformed_normal = mesh.model_matrix.transform_vector(&normal);
439
440 if self.camera.get_direction().dot(&transformed_normal) < 0.0 {
444 continue;
445 }
446
447 if let Some([p1, p2, p3]) =
448 self.transform_points(face, geometry.vertices, transform_matrix)
449 {
450 let intensity = transformed_normal.dot(&adjusted_dir).max(0.0);
452
453 let final_color = color_as_float * intensity + ambient_color;
455
456 let final_color = Vector3::new(
457 final_color.x.clamp(0.0, 1.0),
458 final_color.y.clamp(0.0, 1.0),
459 final_color.z.clamp(0.0, 1.0),
460 );
461
462 let color = Rgb565::new(
463 (final_color.x * 31.0) as u8,
464 (final_color.y * 63.0) as u8,
465 (final_color.z * 31.0) as u8,
466 );
467 callback(DrawPrimitive::ColoredTriangleWithDepth {
468 points: [p1.xy(), p2.xy(), p3.xy()],
469 depths: [p1.z as f32, p2.z as f32, p3.z as f32],
470 color,
471 });
472 }
473 }
474 }
475
476 RenderMode::GouraudLightDir(direction) => {
477 let color_as_float = Vector3::new(
478 mesh.color.r() as f32 / 32.0,
479 mesh.color.g() as f32 / 64.0,
480 mesh.color.b() as f32 / 32.0,
481 );
482 let ambient_color = color_as_float * 0.1;
483 let adjusted_dir = Vector3::new(direction.x, direction.y, -direction.z);
484
485 for (face, face_normal) in geometry.faces.iter().zip(geometry.normals.iter()) {
486 let fn_vec = Vector3::new(face_normal[0], face_normal[1], face_normal[2]);
487 let transformed_fn = mesh.model_matrix.transform_vector(&fn_vec);
488
489 if self.camera.get_direction().dot(&transformed_fn) < 0.0 {
490 continue;
491 }
492
493 if let Some([p1, p2, p3]) =
494 self.transform_points(face, geometry.vertices, transform_matrix)
495 {
496 let vertex_colors: [Rgb565; 3] = core::array::from_fn(|k| {
498 let vn = if !geometry.vertex_normals.is_empty() {
499 let vn_arr = geometry.vertex_normals[face[k]];
500 let vn_vec = Vector3::new(vn_arr[0], vn_arr[1], vn_arr[2]);
501 mesh.model_matrix.transform_vector(&vn_vec)
502 } else {
503 transformed_fn
504 };
505
506 let intensity = vn.dot(&adjusted_dir).max(0.0);
507 let c = color_as_float * intensity + ambient_color;
508 Rgb565::new(
509 (c.x.clamp(0.0, 1.0) * 31.0) as u8,
510 (c.y.clamp(0.0, 1.0) * 63.0) as u8,
511 (c.z.clamp(0.0, 1.0) * 31.0) as u8,
512 )
513 });
514
515 callback(DrawPrimitive::GouraudTriangleWithDepth {
516 points: [p1.xy(), p2.xy(), p3.xy()],
517 depths: [p1.z as f32, p2.z as f32, p3.z as f32],
518 colors: vertex_colors,
519 });
520 }
521 }
522 }
523
524 RenderMode::BlinnPhong {
525 light_dir,
526 specular_intensity,
527 shininess,
528 } => {
529 let color_as_float = Vector3::new(
531 mesh.color.r() as f32 / 32.0,
532 mesh.color.g() as f32 / 64.0,
533 mesh.color.b() as f32 / 32.0,
534 );
535
536 let ambient_color = color_as_float * 0.1;
538
539 let adjusted_light_dir = Vector3::new(light_dir.x, light_dir.y, -light_dir.z);
542
543 let light_dir_normalized = adjusted_light_dir.normalize();
545
546 for (face, normal) in geometry.faces.iter().zip(geometry.normals.iter()) {
547 let normal = Vector3::new(normal[0], normal[1], normal[2]);
549 let transformed_normal = mesh.model_matrix.transform_vector(&normal);
550 let normalized_normal = transformed_normal.normalize();
551
552 if self.camera.get_direction().dot(&normalized_normal) < 0.0 {
554 continue;
555 }
556
557 if let Some([p1, p2, p3]) =
558 self.transform_points(face, geometry.vertices, transform_matrix)
559 {
560 let v0 = geometry.vertices[face[0]];
562 let v1 = geometry.vertices[face[1]];
563 let v2 = geometry.vertices[face[2]];
564 let face_center = Point3::new(
565 (v0[0] + v1[0] + v2[0]) / 3.0,
566 (v0[1] + v1[1] + v2[1]) / 3.0,
567 (v0[2] + v1[2] + v2[2]) / 3.0,
568 );
569 let face_center_world = mesh.model_matrix.transform_point(&face_center);
570
571 let view_dir = (self.camera.position - face_center_world).normalize();
573
574 let half_vector = (light_dir_normalized + view_dir).normalize();
576
577 let diffuse_intensity =
579 normalized_normal.dot(&light_dir_normalized).max(0.0);
580
581 let specular_term =
583 normalized_normal.dot(&half_vector).max(0.0).powf(shininess);
584
585 let diffuse_color = color_as_float * diffuse_intensity;
587 let specular_color =
588 Vector3::new(1.0, 1.0, 1.0) * specular_term * specular_intensity;
589 let final_color = ambient_color + diffuse_color + specular_color;
590
591 let final_color = Vector3::new(
592 final_color.x.clamp(0.0, 1.0),
593 final_color.y.clamp(0.0, 1.0),
594 final_color.z.clamp(0.0, 1.0),
595 );
596
597 let color = Rgb565::new(
598 (final_color.x * 31.0) as u8,
599 (final_color.y * 63.0) as u8,
600 (final_color.z * 31.0) as u8,
601 );
602 callback(DrawPrimitive::ColoredTriangleWithDepth {
603 points: [p1.xy(), p2.xy(), p3.xy()],
604 depths: [p1.z as f32, p2.z as f32, p3.z as f32],
605 color,
606 });
607 }
608 }
609 }
610
611 RenderMode::Solid => {
612 if geometry.normals.is_empty() {
613 for face in geometry.faces.iter() {
614 if let Some([p1, p2, p3]) =
615 self.transform_points(face, geometry.vertices, transform_matrix)
616 {
617 callback(DrawPrimitive::ColoredTriangleWithDepth {
618 points: [p1.xy(), p2.xy(), p3.xy()],
619 depths: [p1.z as f32, p2.z as f32, p3.z as f32],
620 color: mesh.color,
621 });
622 }
623 }
624 } else {
625 for (face, normal) in geometry.faces.iter().zip(geometry.normals) {
626 let normal = Vector3::new(normal[0], normal[1], normal[2]);
628
629 let transformed_normal = mesh.model_matrix.transform_vector(&normal);
630
631 if self.camera.get_direction().dot(&transformed_normal) < 0.0 {
633 continue;
634 }
635
636 if let Some([p1, p2, p3]) =
637 self.transform_points(face, geometry.vertices, transform_matrix)
638 {
639 callback(DrawPrimitive::ColoredTriangleWithDepth {
640 points: [p1.xy(), p2.xy(), p3.xy()],
641 depths: [p1.z as f32, p2.z as f32, p3.z as f32],
642 color: mesh.color,
643 });
644 }
645 }
646 }
647 }
648
649 RenderMode::SectorBright(brightness) => {
650 let factor = brightness as f32 / 255.0;
652 let scaled_r = (mesh.color.r() as f32 * factor) as u8;
653 let scaled_g = (mesh.color.g() as f32 * factor) as u8;
654 let scaled_b = (mesh.color.b() as f32 * factor) as u8;
655 let scaled_color = Rgb565::new(scaled_r, scaled_g, scaled_b);
656
657 if geometry.normals.is_empty() {
658 for face in geometry.faces.iter() {
659 if let Some([p1, p2, p3]) =
660 self.transform_points(face, geometry.vertices, transform_matrix)
661 {
662 callback(DrawPrimitive::ColoredTriangleWithDepth {
663 points: [p1.xy(), p2.xy(), p3.xy()],
664 depths: [p1.z as f32, p2.z as f32, p3.z as f32],
665 color: scaled_color,
666 });
667 }
668 }
669 } else {
670 for (face, normal) in geometry.faces.iter().zip(geometry.normals) {
671 let normal = Vector3::new(normal[0], normal[1], normal[2]);
673 let transformed_normal = mesh.model_matrix.transform_vector(&normal);
674
675 if self.camera.get_direction().dot(&transformed_normal) < 0.0 {
676 continue;
677 }
678
679 if let Some([p1, p2, p3]) =
680 self.transform_points(face, geometry.vertices, transform_matrix)
681 {
682 callback(DrawPrimitive::ColoredTriangleWithDepth {
683 points: [p1.xy(), p2.xy(), p3.xy()],
684 depths: [p1.z as f32, p2.z as f32, p3.z as f32],
685 color: scaled_color,
686 });
687 }
688 }
689 }
690 }
691 }
692 }
693 }
694
695 pub fn record<'a, MS, const MAX: usize>(
696 &self,
697 meshes: MS,
698 commands: &mut crate::command_buffer::CommandBuffer<MAX>,
699 telemetry: Option<&mut crate::telemetry::RecordTelemetry>,
700 ) -> Result<(), crate::error::RenderError>
701 where
702 MS: IntoIterator<Item = &'a K3dMesh<'a>>,
703 {
704 self.record_impl(meshes, commands, telemetry)
705 }
706
707 fn record_impl<'a, MS, const MAX: usize>(
708 &self,
709 meshes: MS,
710 commands: &mut crate::command_buffer::CommandBuffer<MAX>,
711 telemetry: Option<&mut crate::telemetry::RecordTelemetry>,
712 ) -> Result<(), crate::error::RenderError>
713 where
714 MS: IntoIterator<Item = &'a K3dMesh<'a>>,
715 {
716 use crate::command_buffer::RenderCommand;
717 use crate::error::{BudgetKind, RenderError};
718
719 commands.clear();
720 commands.push(RenderCommand::ClearDepth(u32::MAX))?;
721 if let Some(caps) = self.caps {
722 caps.validate_framebuffer(self.width as usize, self.height as usize)?;
723 }
724
725 let mut first_error = None;
726 let mut visible_meshes = 0usize;
727 let mut used_texture_ids: heapless::Vec<u32, 64> = heapless::Vec::new();
728 let mut meshes_total = 0usize;
729
730 for mesh in meshes {
731 meshes_total += 1;
732 if mesh.geometry.vertices.is_empty() {
733 continue;
734 }
735 if self.should_cull_mesh(mesh) {
736 continue;
737 }
738
739 let distance = (mesh.get_position() - self.camera.position).norm();
740 let geometry = mesh.select_lod(distance);
741
742 if let Some(caps) = self.caps {
743 visible_meshes += 1;
744 if visible_meshes > caps.max_meshes_per_frame {
745 return Err(RenderError::OutOfBudget(BudgetKind::MeshesPerFrame {
746 attempted: visible_meshes,
747 max: caps.max_meshes_per_frame,
748 }));
749 }
750
751 if geometry.vertices.len() > caps.max_vertices_per_mesh {
752 return Err(RenderError::OutOfBudget(BudgetKind::VerticesPerMesh {
753 attempted: geometry.vertices.len(),
754 max: caps.max_vertices_per_mesh,
755 }));
756 }
757
758 if geometry.faces.len() > caps.max_triangles_per_mesh {
759 return Err(RenderError::OutOfBudget(BudgetKind::TrianglesPerMesh {
760 attempted: geometry.faces.len(),
761 max: caps.max_triangles_per_mesh,
762 }));
763 }
764
765 if let Some(texture_id) = geometry.texture_id
766 && !used_texture_ids.iter().any(|id| *id == texture_id)
767 {
768 let attempted = used_texture_ids.len() + 1;
769 if attempted > caps.max_textures {
770 return Err(RenderError::OutOfBudget(BudgetKind::Textures {
771 attempted,
772 max: caps.max_textures,
773 }));
774 }
775
776 if used_texture_ids.push(texture_id).is_err() {
777 return Err(RenderError::OutOfBudget(BudgetKind::Textures {
778 attempted,
779 max: caps.max_textures,
780 }));
781 }
782 }
783 }
784
785 self.render(core::iter::once(mesh), |primitive| {
786 if first_error.is_none()
787 && let Err(e) = commands.push(RenderCommand::Draw(primitive))
788 {
789 first_error = Some(e);
790 }
791 });
792 if let Some(err) = first_error {
793 return Err(err);
794 }
795 }
796
797 if let Some(t) = telemetry {
798 t.meshes_total = meshes_total;
799 t.meshes_visible = visible_meshes;
800 t.unique_textures = used_texture_ids.len();
801 t.draw_commands = commands
802 .iter()
803 .filter(|cmd| matches!(cmd, RenderCommand::Draw(_)))
804 .count();
805 t.fallback_used = false;
806 t.degradation_steps_applied = 0;
807 t.dropped_meshes = 0;
808 }
809
810 Ok(())
811 }
812
813 pub fn record_with_fallback<'a, MS, FS, const MAX: usize>(
814 &self,
815 primary: MS,
816 fallback: FS,
817 commands: &mut crate::command_buffer::CommandBuffer<MAX>,
818 telemetry: Option<&mut crate::telemetry::RecordTelemetry>,
819 ) -> Result<BudgetFallbackOutcome, crate::error::RenderError>
820 where
821 MS: IntoIterator<Item = &'a K3dMesh<'a>>,
822 FS: IntoIterator<Item = &'a K3dMesh<'a>>,
823 {
824 use crate::error::RenderError;
825
826 let mut local_telemetry = crate::telemetry::RecordTelemetry::default();
827 match self.record_impl(primary, commands, Some(&mut local_telemetry)) {
828 Ok(()) => {
829 if let Some(t) = telemetry {
830 *t = local_telemetry;
831 t.fallback_used = false;
832 }
833 Ok(BudgetFallbackOutcome {
834 used_fallback: false,
835 primary_budget_error: None,
836 })
837 }
838 Err(RenderError::OutOfBudget(kind)) => {
839 let mut fallback_telemetry = crate::telemetry::RecordTelemetry::default();
840 self.record_impl(fallback, commands, Some(&mut fallback_telemetry))?;
841 if let Some(t) = telemetry {
842 *t = fallback_telemetry;
843 t.fallback_used = true;
844 }
845 Ok(BudgetFallbackOutcome {
846 used_fallback: true,
847 primary_budget_error: Some(kind),
848 })
849 }
850 Err(e) => Err(e),
851 }
852 }
853
854 fn downgraded_quality_tier(tier: crate::config::QualityTier) -> crate::config::QualityTier {
855 use crate::config::QualityTier;
856 match tier {
857 QualityTier::Quality => QualityTier::Balanced,
858 QualityTier::Balanced => QualityTier::Fastest,
859 QualityTier::Fastest => QualityTier::Fastest,
860 }
861 }
862
863 pub fn record_with_degradation<'a, const MAX: usize>(
864 &mut self,
865 meshes: &[&'a K3dMesh<'a>],
866 commands: &mut crate::command_buffer::CommandBuffer<MAX>,
867 policy: crate::config::DegradationPolicy<'_>,
868 telemetry: Option<&mut crate::telemetry::RecordTelemetry>,
869 ) -> Result<DegradationOutcome, crate::error::RenderError> {
870 use crate::config::DegradationStep;
871 use crate::error::RenderError;
872
873 let original_quality = self.quality_tier;
874 let mut active_quality = self.quality_tier;
875
876 let mut outcome = DegradationOutcome {
877 used_degradation: false,
878 steps_applied: 0,
879 dropped_meshes: 0,
880 final_quality_tier: active_quality,
881 primary_budget_error: None,
882 };
883
884 let mut local_telemetry = crate::telemetry::RecordTelemetry::default();
885 match self.record_impl(meshes.iter().copied(), commands, Some(&mut local_telemetry)) {
886 Ok(()) => {
887 if let Some(t) = telemetry {
888 *t = local_telemetry;
889 }
890 return Ok(outcome);
891 }
892 Err(RenderError::OutOfBudget(kind)) => {
893 outcome.primary_budget_error = Some(kind);
894 }
895 Err(e) => return Err(e),
896 }
897
898 for step in policy.steps {
899 outcome.used_degradation = true;
900 outcome.steps_applied += 1;
901
902 let mut selected: heapless::Vec<&K3dMesh<'_>, 512> = heapless::Vec::new();
903 match *step {
904 DegradationStep::RaisePriorityFloor(min_priority) => {
905 for mesh in meshes {
906 if mesh.priority >= min_priority {
907 let _ = selected.push(*mesh);
908 } else {
909 outcome.dropped_meshes += 1;
910 }
911 }
912 }
913 DegradationStep::MeshDecimationStride(stride) => {
914 if stride == 0 {
915 self.quality_tier = original_quality;
916 return Err(RenderError::InvalidInput(
917 "mesh decimation stride must be >= 1",
918 ));
919 }
920 for (idx, mesh) in meshes.iter().enumerate() {
921 if idx % stride == 0 {
922 let _ = selected.push(*mesh);
923 } else {
924 outcome.dropped_meshes += 1;
925 }
926 }
927 }
928 DegradationStep::DowngradeQuality => {
929 active_quality = Self::downgraded_quality_tier(active_quality);
930 self.quality_tier = active_quality;
931 for mesh in meshes {
932 let _ = selected.push(*mesh);
933 }
934 }
935 }
936
937 if selected.is_empty() {
938 continue;
939 }
940
941 let mut step_telemetry = crate::telemetry::RecordTelemetry::default();
942 let attempt = self.record_impl(
943 selected.iter().copied(),
944 commands,
945 Some(&mut step_telemetry),
946 );
947
948 if let Ok(()) = attempt {
949 outcome.final_quality_tier = self.quality_tier;
950 if let Some(t) = telemetry {
951 *t = step_telemetry;
952 t.fallback_used = true;
953 t.degradation_steps_applied = outcome.steps_applied;
954 t.dropped_meshes = outcome.dropped_meshes;
955 }
956 self.quality_tier = original_quality;
957 return Ok(outcome);
958 }
959 }
960
961 self.quality_tier = original_quality;
962 Err(crate::error::RenderError::Recoverable {
963 fault: crate::error::RuntimeFaultKind::Budget(outcome.primary_budget_error.unwrap_or(
964 crate::error::BudgetKind::DrawPrimitives {
965 attempted: commands.len(),
966 max: MAX,
967 },
968 )),
969 action: crate::error::RecoveryAction::SkipFrame,
970 })
971 }
972
973 pub fn execute<D, const MAX: usize>(
974 &self,
975 fb: &mut D,
976 frame: &mut crate::renderer::FrameCtx<'_>,
977 commands: &crate::command_buffer::CommandBuffer<MAX>,
978 telemetry: Option<&mut crate::telemetry::ExecuteTelemetry>,
979 ) -> Result<Option<crate::renderer::DirtyRegion>, crate::error::RenderError>
980 where
981 D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>
982 + embedded_graphics_core::prelude::OriginDimensions,
983 <D as embedded_graphics_core::draw_target::DrawTarget>::Error: core::fmt::Debug,
984 {
985 if let Some(t) = telemetry {
986 t.commands_total = commands.len();
987 t.draw_commands = commands
988 .iter()
989 .filter(|cmd| matches!(cmd, crate::command_buffer::RenderCommand::Draw(_)))
990 .count();
991 t.clear_color_commands = commands
992 .iter()
993 .filter(|cmd| matches!(cmd, crate::command_buffer::RenderCommand::ClearColor(_)))
994 .count();
995 t.clear_depth_commands = commands
996 .iter()
997 .filter(|cmd| matches!(cmd, crate::command_buffer::RenderCommand::ClearDepth(_)))
998 .count();
999 }
1000 crate::renderer::execute_commands_with_dirty_region(fb, frame, commands)
1001 }
1002
1003 pub fn execute_tiled<D, const MAX: usize, const BIN_CAP: usize>(
1004 &self,
1005 fb: &mut D,
1006 frame: &mut crate::renderer::FrameCtx<'_>,
1007 commands: &crate::command_buffer::CommandBuffer<MAX>,
1008 tile: crate::tilebin::TileConfig,
1009 ) -> Result<crate::tilebin::TileBinStats, crate::error::RenderError>
1010 where
1011 D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>
1012 + embedded_graphics_core::prelude::OriginDimensions,
1013 <D as embedded_graphics_core::draw_target::DrawTarget>::Error: core::fmt::Debug,
1014 {
1015 crate::renderer::execute_commands_tiled::<D, MAX, BIN_CAP>(fb, frame, commands, tile)
1016 }
1017}
1018
1019#[derive(Debug, Clone, Copy)]
1021pub struct MeshRayCastHit {
1022 pub distance: f32,
1024 pub point: Vector3<f32>,
1026 pub normal: Vector3<f32>,
1028 pub face_index: usize,
1030 pub uv: [f32; 2],
1032}
1033
1034pub fn mesh_ray_cast(
1039 ray_origin: Vector3<f32>,
1040 ray_dir: Vector3<f32>,
1041 geometry: &mesh::Geometry<'_>,
1042 model_matrix: &Matrix4<f32>,
1043 max_distance: f32,
1044) -> Option<MeshRayCastHit> {
1045 let mut nearest: Option<MeshRayCastHit> = None;
1046 let mut min_dist = max_distance;
1047
1048 for (face_index, face) in geometry.faces.iter().enumerate() {
1049 let raw_v0 = geometry.vertices[face[0]];
1050 let raw_v1 = geometry.vertices[face[1]];
1051 let raw_v2 = geometry.vertices[face[2]];
1052
1053 let v0 = model_matrix
1055 .transform_point(&Point3::new(raw_v0[0], raw_v0[1], raw_v0[2]))
1056 .coords;
1057 let v1 = model_matrix
1058 .transform_point(&Point3::new(raw_v1[0], raw_v1[1], raw_v1[2]))
1059 .coords;
1060 let v2 = model_matrix
1061 .transform_point(&Point3::new(raw_v2[0], raw_v2[1], raw_v2[2]))
1062 .coords;
1063
1064 let edge1 = v1 - v0;
1066 let edge2 = v2 - v0;
1067 let h = ray_dir.cross(&edge2);
1068 let det = edge1.dot(&h);
1069
1070 if det.abs() < 1e-6 {
1072 continue;
1073 }
1074
1075 let inv_det = 1.0 / det;
1076 let s = ray_origin - v0;
1077 let bary_u = inv_det * s.dot(&h);
1078 if bary_u < 0.0 || bary_u > 1.0 {
1079 continue;
1080 }
1081
1082 let q = s.cross(&edge1);
1083 let bary_v = inv_det * ray_dir.dot(&q);
1084 if bary_v < 0.0 || bary_u + bary_v > 1.0 {
1085 continue;
1086 }
1087
1088 let t = inv_det * edge2.dot(&q);
1089 if t <= 0.0 || t >= min_dist {
1090 continue;
1091 }
1092
1093 let normal = edge1.cross(&edge2).normalize();
1095
1096 let bary_w = 1.0 - bary_u - bary_v;
1098
1099 let uv = if geometry.uvs.len() > face[0]
1101 && geometry.uvs.len() > face[1]
1102 && geometry.uvs.len() > face[2]
1103 {
1104 let uv0 = geometry.uvs[face[0]];
1105 let uv1 = geometry.uvs[face[1]];
1106 let uv2 = geometry.uvs[face[2]];
1107 [
1108 bary_w * uv0[0] + bary_u * uv1[0] + bary_v * uv2[0],
1109 bary_w * uv0[1] + bary_u * uv1[1] + bary_v * uv2[1],
1110 ]
1111 } else {
1112 [0.0, 0.0]
1113 };
1114
1115 let point = ray_origin + ray_dir * t;
1116 min_dist = t;
1117 nearest = Some(MeshRayCastHit {
1118 distance: t,
1119 point,
1120 normal,
1121 face_index,
1122 uv,
1123 });
1124 }
1125
1126 nearest
1127}
1128
1129#[cfg(test)]
1130mod tests {
1131 extern crate std;
1132 use super::*;
1133
1134 #[test]
1135 fn test_engine_creation() {
1136 let engine = K3dengine::new(640, 480);
1137 assert_eq!(engine.width, 640);
1138 assert_eq!(engine.height, 480);
1139 assert!((engine.camera.get_aspect_ratio() - 640.0 / 480.0).abs() < 0.001);
1140 }
1141
1142 #[test]
1143 fn test_transform_point_basic() {
1144 let engine = K3dengine::new(640, 480);
1145 let transform_matrix = engine.camera.vp_matrix;
1147
1148 let point = [0.0, 0.0, -5.0];
1151 let result = engine.transform_point(&point, transform_matrix);
1152
1153 if let Some(transformed) = result {
1154 assert!(transformed.x >= 0 && transformed.x < 640);
1156 assert!(transformed.y >= 0 && transformed.y < 480);
1157 }
1158 }
1160
1161 #[test]
1162 fn test_transform_point_clamps_out_of_bounds() {
1163 let engine = K3dengine::new(640, 480);
1164 let model_matrix = nalgebra::Matrix4::identity();
1165
1166 let point = [100.0, 100.0, -5.0];
1168 let result = engine.transform_point(&point, model_matrix);
1169 assert!(result.is_none());
1171 }
1172
1173 #[test]
1174 fn test_transform_point_behind_camera() {
1175 let engine = K3dengine::new(640, 480);
1176 let transform_matrix = engine.camera.vp_matrix;
1177
1178 let point = [0.0, 0.0, 1.0];
1180 let _result = engine.transform_point(&point, transform_matrix);
1181 }
1185
1186 #[test]
1187 fn test_transform_point_near_plane_clipping() {
1188 let engine = K3dengine::new(640, 480);
1189 let model_matrix = nalgebra::Matrix4::identity();
1190
1191 let point = [0.0, 0.0, -0.01];
1193 let result = engine.transform_point(&point, model_matrix);
1194 assert!(result.is_none());
1195 }
1196
1197 #[test]
1198 fn test_transform_point_far_plane_clipping() {
1199 let engine = K3dengine::new(640, 480);
1200 let model_matrix = nalgebra::Matrix4::identity();
1201
1202 let point = [0.0, 0.0, -1000.0];
1204 let result = engine.transform_point(&point, model_matrix);
1205 assert!(result.is_none());
1206 }
1207
1208 #[test]
1209 fn test_transform_points_array() {
1210 let engine = K3dengine::new(640, 480);
1211 let transform_matrix = engine.camera.vp_matrix;
1212
1213 let vertices = [[0.0, 0.0, -5.0], [0.1, 0.0, -5.0], [0.0, 0.1, -5.0]];
1214 let indices = [0, 1, 2];
1215
1216 let result = engine.transform_points(&indices, &vertices, transform_matrix);
1217
1218 if let Some(points) = result {
1220 assert_eq!(points.len(), 3);
1221 }
1222 }
1224
1225 #[test]
1226 fn test_render_empty_faces_mesh() {
1227 let engine = K3dengine::new(640, 480);
1228 let vertices = [[0.0, 0.0, -5.0]]; let geometry = mesh::Geometry {
1230 vertices: &vertices,
1231 faces: &[],
1232 colors: &[],
1233 lines: &[],
1234 normals: &[],
1235 vertex_normals: &[],
1236 uvs: &[],
1237 texture_id: None,
1238 };
1239 let mesh = mesh::K3dMesh::new(geometry);
1240
1241 let mut callback_count = 0;
1242 engine.render(std::iter::once(&mesh), |_| {
1243 callback_count += 1;
1244 });
1245
1246 assert!(callback_count > 0);
1248 }
1249
1250 #[test]
1251 fn test_render_points_mode() {
1252 let engine = K3dengine::new(640, 480);
1253
1254 let vertices = [[0.0, 0.0, -5.0], [0.5, 0.0, -5.0]];
1255
1256 let geometry = mesh::Geometry {
1257 vertices: &vertices,
1258 faces: &[],
1259 colors: &[],
1260 lines: &[],
1261 normals: &[],
1262 vertex_normals: &[],
1263 uvs: &[],
1264 texture_id: None,
1265 };
1266
1267 let mut mesh = mesh::K3dMesh::new(geometry);
1268 mesh.set_render_mode(mesh::RenderMode::Points);
1269
1270 let mut primitives = std::vec::Vec::new();
1271 engine.render(std::iter::once(&mesh), |prim| {
1272 primitives.push(prim);
1273 });
1274
1275 assert!(primitives.len() > 0);
1277 for prim in primitives {
1278 assert!(matches!(prim, DrawPrimitive::ColoredPoint(_, _)));
1279 }
1280 }
1281
1282 #[test]
1283 fn test_render_lines_mode_with_faces() {
1284 let engine = K3dengine::new(640, 480);
1285
1286 let vertices = [[0.0, 0.0, -5.0], [0.5, 0.0, -5.0], [0.0, 0.5, -5.0]];
1287
1288 let faces = [[0, 1, 2]];
1289
1290 let geometry = mesh::Geometry {
1291 vertices: &vertices,
1292 faces: &faces,
1293 colors: &[],
1294 lines: &[],
1295 normals: &[],
1296 vertex_normals: &[],
1297 uvs: &[],
1298 texture_id: None,
1299 };
1300
1301 let mut mesh = mesh::K3dMesh::new(geometry);
1302 mesh.set_render_mode(mesh::RenderMode::Lines);
1303
1304 let mut primitives = std::vec::Vec::new();
1305 engine.render(std::iter::once(&mesh), |prim| {
1306 primitives.push(prim);
1307 });
1308
1309 assert_eq!(primitives.len(), 3);
1311 for prim in primitives {
1312 assert!(matches!(prim, DrawPrimitive::Line(_, _)));
1313 }
1314 }
1315
1316 #[test]
1317 fn test_render_gouraud_light_dir() {
1318 let mut engine = K3dengine::new(640, 480);
1319 engine.camera.set_position(Point3::new(0.0, 0.0, -10.0));
1320 engine.camera.set_target(Point3::new(0.0, 0.0, 0.0));
1321
1322 let vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1323 let faces = [[0, 1, 2]];
1324 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]];
1326
1327 let geometry = mesh::Geometry {
1328 vertices: &vertices,
1329 faces: &faces,
1330 colors: &[],
1331 lines: &[],
1332 normals: &normals,
1333 vertex_normals: &vertex_normals,
1334 uvs: &[],
1335 texture_id: None,
1336 };
1337
1338 let mut mesh = mesh::K3dMesh::new(geometry);
1339 mesh.set_render_mode(mesh::RenderMode::GouraudLightDir(Vector3::new(
1340 0.0, 0.0, 1.0,
1341 )));
1342
1343 let mut primitives = std::vec::Vec::new();
1344 engine.render(std::iter::once(&mesh), |prim| {
1345 primitives.push(prim);
1346 });
1347
1348 assert!(!primitives.is_empty());
1350 for prim in &primitives {
1351 assert!(matches!(
1352 prim,
1353 DrawPrimitive::GouraudTriangleWithDepth { .. }
1354 ));
1355 }
1356 }
1357}