1#![no_std]
2#[cfg(feature = "std")]
3extern crate std;
4
5#[cfg(feature = "depth-u16")]
6pub type ZDepth = u16;
7#[cfg(feature = "depth-u16")]
8pub const Z_MAX_VALUE: ZDepth = u16::MAX;
9#[cfg(feature = "depth-u16")]
10pub const DEPTH_EPSILON: ZDepth = 1;
11
12#[cfg(not(feature = "depth-u16"))]
13pub type ZDepth = u32;
14#[cfg(not(feature = "depth-u16"))]
15pub const Z_MAX_VALUE: ZDepth = u32::MAX;
16#[cfg(not(feature = "depth-u16"))]
17pub const DEPTH_EPSILON: ZDepth = 128;
18
19#[inline(always)]
20pub const fn to_zdepth(z: u32) -> ZDepth {
21 #[cfg(feature = "depth-u16")]
22 {
23 (z >> 16) as u16
24 }
25 #[cfg(not(feature = "depth-u16"))]
26 {
27 z
28 }
29}
30
31#[cfg(feature = "dma2d")]
32unsafe extern "Rust" {
33 #[cfg(feature = "depth-u16")]
34 fn dma2d_clear_zbuffer_u16(ptr: *mut u16, len: usize, value: u16);
35 #[cfg(not(feature = "depth-u16"))]
36 fn dma2d_clear_zbuffer_u32(ptr: *mut u32, len: usize, value: u32);
37}
38
39#[inline(always)]
40pub fn clear_zbuffer(zbuffer: &mut [ZDepth], value: ZDepth) {
41 #[cfg(feature = "dma2d")]
42 {
43 #[cfg(feature = "depth-u16")]
44 unsafe {
45 dma2d_clear_zbuffer_u16(zbuffer.as_mut_ptr(), zbuffer.len(), value);
46 }
47 #[cfg(not(feature = "depth-u16"))]
48 unsafe {
49 dma2d_clear_zbuffer_u32(zbuffer.as_mut_ptr(), zbuffer.len(), value);
50 }
51 }
52 #[cfg(not(feature = "dma2d"))]
53 {
54 zbuffer.fill(value);
55 }
56}
57
58use camera::Camera;
59use embedded_graphics_core::pixelcolor::Rgb565;
60use embedded_graphics_core::pixelcolor::RgbColor;
61use mesh::K3dMesh;
62use mesh::RenderMode;
63use nalgebra::Matrix4;
64use nalgebra::Point2;
65use nalgebra::Point3;
66use nalgebra::Vector3;
67use nalgebra::Vector4;
68
69#[allow(unused_imports)]
72use nalgebra::ComplexField;
73
74pub mod animation;
75pub mod billboard;
76#[cfg(feature = "aabb-cull")]
77pub mod bounds;
78pub mod bridge;
79pub mod bsp;
80pub mod camera;
81pub mod character;
82pub mod command_buffer;
83pub mod completion;
84pub mod config;
85pub mod display_backend;
86pub mod draw;
87#[cfg(feature = "embassy")]
88pub mod embassy;
89pub mod error;
90#[cfg(feature = "gizmos")]
91pub mod gizmos;
92pub mod hardware_profile;
93pub mod hud;
94pub mod input;
95pub mod lights;
96pub mod mesh;
97pub mod painters;
98pub mod particles;
99#[cfg(feature = "perfcounter")]
100pub mod perfcounter;
101#[cfg(feature = "physics")]
102pub mod physics;
103pub mod raycast;
104#[cfg(feature = "render-layers")]
105pub mod render_layers;
106pub mod renderer;
107pub mod retro;
108pub mod scene_format;
109pub mod scene_stream;
110pub mod sector_lights;
111pub mod skeleton;
112#[cfg(feature = "physics")]
113pub mod softbody;
114pub mod swapchain;
115pub mod telemetry;
116pub mod texture;
117pub mod tilebin;
118pub mod transform_anim;
119pub mod tween;
120
121pub use embedded_graphics_framebuf::{
123 FrameBuf,
124 backends::{DMACapableFrameBufferBackend, EndianCorrectedBuffer, EndianCorrection},
125};
126
127pub use draw::PixelRead;
128#[cfg(feature = "aa")]
129pub use draw::ReadPixel;
130
131#[cfg(feature = "aabb-cull")]
132pub use bounds::Aabb;
133pub use bridge::{
134 AsEgPoint, AsNalgebraPoint, draw_to, eg_to_nalgebra, nalgebra_to_eg, render_drawable_to_buffer,
135};
136pub use character::CharacterController;
137pub use completion::{CompletionSlot, WaitTransfer, WaitTransferFuture};
138pub use display_backend::{
139 AsyncDmaTransfer, DisplayBackend, DisplayError, DisplayRegion, DmaTransfer, SimulatorBackend,
140 TransferError,
141};
142#[cfg(feature = "aa")]
143pub use draw::draw_zbuffered_2xssaa;
144pub use draw::{
145 DitherConfig, FogConfig, fast_blend_rgb565, fast_blend_rgba8888, fast_blend_rgba8888_to_rgb565,
146 reverse_color_rgb565, reverse_color_rgba8888,
147};
148#[cfg(feature = "embassy")]
149pub use embassy::{EmbassyWaitTransfer, EmbassyWaitTransferFuture, FrameClock};
150pub use swapchain::{StandardSwapChain, SwapChain};
151#[cfg(feature = "triple-buffering")]
152pub use swapchain::{StandardTripleSwapChain, TripleSwapChain};
153#[cfg(feature = "fixed-transform")]
157pub use embedded_dsp::fixed_point::{
158 FP_ONE, Q16, Q16_MAX, Q16_MIN, ScanlineInterp, abs_q16, angle_to_q16, div_f_q16, div_n_q16,
159 div_q16, from_i16_q16, from_q16, lerp_q16, mul_f_q16, mul_n_q16, mul_q16, q16_to_q31,
160 q31_to_q16, qadd_q16, qsub_q16, recip_q16, to_i16_q16, to_q16,
161};
162pub use input::InputState;
163pub use lights::{PointLight, PointLightSet};
164pub use particles::{ParticleSpawn, ParticleSystem};
165#[cfg(feature = "render-layers")]
166pub use render_layers::RenderLayers;
167pub use renderer::{DirtyRegion, FrameCtx};
168pub use retro::{
169 LightLevels, PaletteMode, RetroStyle, ScreenTint, SkyConfig, StippleMode, TextureMapping,
170};
171pub use sector_lights::{LightEffectKind, SectorLight, light_level_at, light_level_u8_at};
172pub use tilebin::{TileBinStats, TileConfig};
173pub use transform_anim::{AnimationPlayer, SampledTransform, TransformKeyframe, TransformTrack};
174pub use tween::{Easing, Tween, Tween3, apply_easing, lerp, lerp3, scale_rgb565};
175
176#[derive(Debug, Clone)]
177pub enum DrawPrimitive {
178 ColoredPoint(Point2<i32>, Rgb565),
179 Line([Point2<i32>; 2], Rgb565),
180 ColoredTriangle([Point2<i32>; 3], Rgb565),
181 ColoredTriangleWithDepth {
182 points: [Point2<i32>; 3],
183 depths: [f32; 3],
184 color: Rgb565,
185 },
186 TranslucentTriangleWithDepth {
187 points: [Point2<i32>; 3],
188 depths: [f32; 3],
189 color: Rgb565,
190 alpha: u8,
191 },
192 GouraudTriangle {
193 points: [Point2<i32>; 3],
194 colors: [Rgb565; 3],
195 },
196 GouraudTriangleWithDepth {
197 points: [Point2<i32>; 3],
198 depths: [f32; 3],
199 colors: [Rgb565; 3],
200 },
201 TexturedTriangle {
202 points: [Point2<i32>; 3],
203 uvs: [[f32; 2]; 3],
204 texture_id: u32,
205 },
206 TexturedTriangleWithDepth {
207 points: [Point2<i32>; 3],
208 depths: [f32; 3],
209 ws: [f32; 3],
210 uvs: [[f32; 2]; 3],
211 texture_id: u32,
212 },
213 TexturedGouraudTriangleWithDepth {
214 points: [Point2<i32>; 3],
215 depths: [f32; 3],
216 ws: [f32; 3],
217 uvs: [[f32; 2]; 3],
218 colors: [Rgb565; 3],
219 texture_id: u32,
220 },
221 LightmappedTriangle {
229 points: [Point2<i32>; 3],
230 depths: [f32; 3],
231 ws: [f32; 3],
232 surface_uvs: [[f32; 2]; 3],
233 lm_uvs: [[f32; 2]; 3],
234 texture_id: u32,
235 lightmap_id: u32,
236 brightness: u8,
238 dynamic_tint: Rgb565,
240 },
241}
242
243pub struct K3dengine {
244 pub camera: Camera,
245 width: u16,
246 height: u16,
247 caps: Option<crate::config::ProfileCaps>,
248 quality_tier: crate::config::QualityTier,
249 material_profile: crate::config::MaterialProfile,
250 fog: Option<crate::draw::FogConfig>,
252 dither: Option<crate::draw::DitherConfig>,
254 vertex_snap_bits: u8,
256 texture_mapping: crate::retro::TextureMapping,
258 light_levels: crate::retro::LightLevels,
260 stipple_mode: crate::retro::StippleMode,
262 screen_tint: Option<crate::retro::ScreenTint>,
264 palette_mode: crate::retro::PaletteMode,
266 sky: Option<crate::retro::SkyConfig>,
268 point_lights: heapless::Vec<crate::lights::PointLight, 16>,
271}
272
273#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274pub struct BudgetFallbackOutcome {
275 pub used_fallback: bool,
276 pub primary_budget_error: Option<crate::error::BudgetKind>,
277}
278
279#[derive(Debug, Clone, Copy, PartialEq, Eq)]
280pub struct DegradationOutcome {
281 pub used_degradation: bool,
282 pub steps_applied: usize,
283 pub dropped_meshes: usize,
284 pub final_quality_tier: crate::config::QualityTier,
285 pub primary_budget_error: Option<crate::error::BudgetKind>,
286}
287
288impl K3dengine {
289 pub fn new(width: u16, height: u16) -> K3dengine {
290 K3dengine {
291 camera: Camera::new(width as f32 / height as f32),
292 width,
293 height,
294 caps: None,
295 quality_tier: crate::config::QualityTier::Balanced,
296 material_profile: crate::config::MaterialProfile::Lambert,
297 fog: None,
298 dither: None,
299 vertex_snap_bits: 0,
300 texture_mapping: crate::retro::TextureMapping::PerspectiveCorrect,
301 light_levels: crate::retro::LightLevels::Linear,
302 stipple_mode: crate::retro::StippleMode::Off,
303 screen_tint: None,
304 palette_mode: crate::retro::PaletteMode::Off,
305 sky: None,
306 point_lights: heapless::Vec::new(),
307 }
308 }
309
310 pub fn set_fog(&mut self, fog: crate::draw::FogConfig) {
312 self.fog = Some(fog);
313 }
314
315 pub fn clear_fog(&mut self) {
317 self.fog = None;
318 }
319
320 pub fn set_dither(&mut self, dither: crate::draw::DitherConfig) {
322 self.dither = Some(dither);
323 }
324
325 pub fn clear_dither(&mut self) {
327 self.dither = None;
328 }
329
330 pub fn set_vertex_snap_bits(&mut self, bits: u8) {
332 self.vertex_snap_bits = bits.min(16);
333 }
334
335 pub fn set_texture_mapping(&mut self, mapping: crate::retro::TextureMapping) {
337 self.texture_mapping = mapping;
338 }
339
340 pub fn set_light_levels(&mut self, levels: crate::retro::LightLevels) {
342 self.light_levels = levels;
343 }
344
345 pub fn set_stipple_mode(&mut self, mode: crate::retro::StippleMode) {
347 self.stipple_mode = mode;
348 }
349
350 pub fn set_screen_tint(&mut self, tint: crate::retro::ScreenTint) {
352 self.screen_tint = Some(tint);
353 }
354
355 pub fn clear_screen_tint(&mut self) {
357 self.screen_tint = None;
358 }
359
360 pub fn set_palette_mode(&mut self, mode: crate::retro::PaletteMode) {
362 self.palette_mode = mode;
363 }
364
365 pub fn set_sky(&mut self, sky: crate::retro::SkyConfig) {
367 self.sky = Some(sky);
368 }
369
370 pub fn clear_sky(&mut self) {
372 self.sky = None;
373 }
374
375 pub fn apply_retro_style(&mut self, style: crate::retro::RetroStyle) {
377 self.fog = style.fog;
378 self.dither = style.dither;
379 self.set_vertex_snap_bits(style.vertex_snap_bits);
380 self.texture_mapping = style.texture_mapping;
381 self.light_levels = style.light_levels;
382 self.stipple_mode = style.stipple_mode;
383 self.screen_tint = style.screen_tint;
384 self.palette_mode = style.palette_mode;
385 self.sky = style.sky;
386 }
387
388 pub fn add_point_light(&mut self, light: crate::lights::PointLight) -> bool {
391 self.point_lights.push(light).is_ok()
392 }
393
394 pub fn clear_point_lights(&mut self) {
396 self.point_lights.clear();
397 }
398
399 #[inline]
402 fn light_tint_at(&self, world_pos: Point3<f32>) -> Rgb565 {
403 let mut r = 0u32;
404 let mut g = 0u32;
405 let mut b = 0u32;
406 for light in &self.point_lights {
407 let c = light.contribution_at(world_pos);
408 r += c.r() as u32;
409 g += c.g() as u32;
410 b += c.b() as u32;
411 }
412 Rgb565::new(r.min(31) as u8, g.min(63) as u8, b.min(31) as u8)
413 }
414
415 #[inline]
417 fn add_tint(base: Rgb565, tint: Rgb565) -> Rgb565 {
418 Rgb565::new(
419 (base.r() as u16 + tint.r() as u16).min(31) as u8,
420 (base.g() as u16 + tint.g() as u16).min(63) as u8,
421 (base.b() as u16 + tint.b() as u16).min(31) as u8,
422 )
423 }
424
425 const DOOM_LIGHT_TABLE: [u8; 32] = [
427 8, 12, 16, 20, 24, 28, 34, 40, 48, 56, 64, 72, 82, 92, 102, 112, 124, 136, 148, 160, 172,
428 184, 196, 206, 216, 224, 232, 238, 244, 248, 252, 255,
429 ];
430
431 #[inline]
432 fn sector_shaded_color(
433 &self,
434 base: Rgb565,
435 brightness: u8,
436 face_center: Point3<f32>,
437 ) -> Rgb565 {
438 let level_u8 = match self.light_levels {
439 crate::retro::LightLevels::Linear => brightness,
440 crate::retro::LightLevels::Doom32 => {
441 let base_level = (brightness as usize * 31) / 255;
442 let distance = (face_center - self.camera.position).norm();
443 let distance_drop = (distance * 2.0) as usize;
445 let idx = base_level.saturating_sub(distance_drop).min(31);
446 Self::DOOM_LIGHT_TABLE[idx]
447 }
448 };
449
450 let factor = level_u8 as f32 / 255.0;
451 Rgb565::new(
452 (base.r() as f32 * factor) as u8,
453 (base.g() as f32 * factor) as u8,
454 (base.b() as f32 * factor) as u8,
455 )
456 }
457
458 #[inline]
460 fn face_world_center(
461 face: &[usize; 3],
462 vertices: &[[f32; 3]],
463 model_matrix: Matrix4<f32>,
464 ) -> Point3<f32> {
465 let v0 = vertices[face[0]];
466 let v1 = vertices[face[1]];
467 let v2 = vertices[face[2]];
468 let cx = (v0[0] + v1[0] + v2[0]) / 3.0;
469 let cy = (v0[1] + v1[1] + v2[1]) / 3.0;
470 let cz = (v0[2] + v1[2] + v2[2]) / 3.0;
471 model_matrix.transform_point(&Point3::new(cx, cy, cz))
472 }
473
474 pub fn set_caps(&mut self, caps: crate::config::ProfileCaps) {
475 self.caps = Some(caps);
476 self.apply_render_defaults(crate::config::render_defaults_for_profile(caps));
477 }
478
479 pub fn clear_caps(&mut self) {
480 self.caps = None;
481 }
482
483 pub fn set_quality_tier(&mut self, tier: crate::config::QualityTier) {
484 self.quality_tier = tier;
485 }
486
487 pub fn set_material_profile(&mut self, profile: crate::config::MaterialProfile) {
488 self.material_profile = profile;
489 }
490
491 pub fn apply_render_defaults(&mut self, defaults: crate::config::RenderDefaults) {
492 self.quality_tier = defaults.quality_tier;
493 self.material_profile = defaults.material_profile;
494 }
495
496 fn resolve_render_mode(&self, mode: &RenderMode) -> RenderMode {
497 use crate::config::{MaterialProfile, QualityTier};
498 match self.quality_tier {
499 QualityTier::Fastest => match mode {
500 RenderMode::BlinnPhong { .. }
501 | RenderMode::GouraudLightDir(_)
502 | RenderMode::Toon(_, _)
503 | RenderMode::SolidLightDir(_) => RenderMode::Solid,
504 _ => mode.clone(),
505 },
506 QualityTier::Balanced => match (self.material_profile, mode) {
507 (MaterialProfile::Unlit, RenderMode::BlinnPhong { .. })
508 | (MaterialProfile::Unlit, RenderMode::GouraudLightDir(_))
509 | (MaterialProfile::Unlit, RenderMode::Toon(_, _))
510 | (MaterialProfile::Unlit, RenderMode::SolidLightDir(_)) => RenderMode::Solid,
511 (MaterialProfile::Lambert, RenderMode::BlinnPhong { light_dir, .. }) => {
512 RenderMode::SolidLightDir(*light_dir)
513 }
514 _ => mode.clone(),
515 },
516 QualityTier::Quality => match (self.material_profile, mode) {
517 (MaterialProfile::Unlit, RenderMode::BlinnPhong { .. })
518 | (MaterialProfile::Unlit, RenderMode::GouraudLightDir(_))
519 | (MaterialProfile::Unlit, RenderMode::Toon(_, _))
520 | (MaterialProfile::Unlit, RenderMode::SolidLightDir(_)) => RenderMode::Solid,
521 (MaterialProfile::Lambert, RenderMode::BlinnPhong { light_dir, .. }) => {
522 RenderMode::SolidLightDir(*light_dir)
523 }
524 _ => mode.clone(),
525 },
526 }
527 }
528
529 #[inline]
531 fn should_cull_mesh(&self, mesh: &K3dMesh) -> bool {
532 #[cfg(feature = "render-layers")]
533 if !self.camera.layers.intersects(mesh.layers) {
534 return true;
535 }
536
537 #[cfg(feature = "aabb-cull")]
538 {
539 let aabb = mesh.model_aabb();
540 let world_center = mesh
541 .model_matrix
542 .transform_point(&nalgebra::Point3::from(aabb.center));
543 let scale = mesh.similarity.scaling();
544 let radius = aabb.radius() * scale;
545 let m = self.camera.vp_matrix;
546 let planes = Self::frustum_planes_from_vp(&m);
547
548 for plane in &planes {
549 let (a, b, c, d) = (plane[0], plane[1], plane[2], plane[3]);
550 let len = (a * a + b * b + c * c).sqrt();
551 if len <= 0.0 {
552 continue;
553 }
554 let dist = (a * world_center.x + b * world_center.y + c * world_center.z + d) / len;
555 if dist < -radius {
556 return true;
557 }
558 }
559 for plane in &planes {
560 let (a, b, c, d) = (plane[0], plane[1], plane[2], plane[3]);
561 let len = (a * a + b * b + c * c).sqrt();
562 if len <= 0.0 {
563 continue;
564 }
565 if aabb.plane_signed_overshoot(a, b, c, d, len, &mesh.model_matrix) < 0.0 {
566 return true;
567 }
568 }
569 return false;
570 }
571
572 #[cfg(not(feature = "aabb-cull"))]
573 {
574 let mesh_pos = mesh.get_position();
575 let radius_sq = mesh.compute_bounding_radius_sq();
576 let radius = radius_sq.sqrt();
577 let planes = Self::frustum_planes_from_vp(&self.camera.vp_matrix);
578 for plane in &planes {
579 let (a, b, c, d) = (plane[0], plane[1], plane[2], plane[3]);
580 let len = (a * a + b * b + c * c).sqrt();
581 if len > 0.0 {
582 let dist = (a * mesh_pos.x + b * mesh_pos.y + c * mesh_pos.z + d) / len;
583 if dist < -radius {
584 return true;
585 }
586 }
587 }
588 false
589 }
590 }
591
592 #[inline]
593 fn frustum_planes_from_vp(m: &Matrix4<f32>) -> [[f32; 4]; 6] {
594 [
595 [
596 m[(3, 0)] + m[(0, 0)],
597 m[(3, 1)] + m[(0, 1)],
598 m[(3, 2)] + m[(0, 2)],
599 m[(3, 3)] + m[(0, 3)],
600 ],
601 [
602 m[(3, 0)] - m[(0, 0)],
603 m[(3, 1)] - m[(0, 1)],
604 m[(3, 2)] - m[(0, 2)],
605 m[(3, 3)] - m[(0, 3)],
606 ],
607 [
608 m[(3, 0)] + m[(1, 0)],
609 m[(3, 1)] + m[(1, 1)],
610 m[(3, 2)] + m[(1, 2)],
611 m[(3, 3)] + m[(1, 3)],
612 ],
613 [
614 m[(3, 0)] - m[(1, 0)],
615 m[(3, 1)] - m[(1, 1)],
616 m[(3, 2)] - m[(1, 2)],
617 m[(3, 3)] - m[(1, 3)],
618 ],
619 [
620 m[(3, 0)] + m[(2, 0)],
621 m[(3, 1)] + m[(2, 1)],
622 m[(3, 2)] + m[(2, 2)],
623 m[(3, 3)] + m[(2, 3)],
624 ],
625 [
626 m[(3, 0)] - m[(2, 0)],
627 m[(3, 1)] - m[(2, 1)],
628 m[(3, 2)] - m[(2, 2)],
629 m[(3, 3)] - m[(2, 3)],
630 ],
631 ]
632 }
633
634 #[inline(always)]
635 fn transform_point(&self, point: &[f32; 3], model_matrix: Matrix4<f32>) -> Option<Point3<i32>> {
636 #[cfg(feature = "fixed-transform")]
637 {
638 return self.transform_point_fixed(point, model_matrix);
639 }
640 #[cfg(not(feature = "fixed-transform"))]
641 {
642 let point = nalgebra::Vector4::new(point[0], point[1], point[2], 1.0);
643 let point = model_matrix * point;
644
645 if point.w < 0.0 {
646 return None;
647 }
648 if point.w < self.camera.near || point.w > self.camera.far {
657 return None;
658 }
659
660 let point = Point3::from_homogeneous(point)?;
661
662 let x = ((1.0 + point.x) * 0.5 * self.width as f32) as i32;
663 let y = ((1.0 - point.y) * 0.5 * self.height as f32) as i32;
664
665 if x < 0 || x >= self.width as i32 || y < 0 || y >= self.height as i32 {
666 return None;
667 }
668
669 Some(Point3::new(
670 x,
671 y,
672 (point.z * (self.camera.far - self.camera.near) + self.camera.near) as i32,
673 ))
674 }
675 }
676
677 #[cfg(feature = "fixed-transform")]
678 #[inline(always)]
679 fn transform_point_fixed(
680 &self,
681 point: &[f32; 3],
682 model_matrix: Matrix4<f32>,
683 ) -> Option<Point3<i32>> {
684 use embedded_dsp::fixed_point::{Q16, from_q16, to_q16};
685
686 #[inline(always)]
690 fn div_checked(a: Q16, b: Q16) -> Option<Q16> {
691 if b == 0 {
692 None
693 } else {
694 Some(embedded_dsp::fixed_point::div_q16(a, b))
695 }
696 }
697
698 let point = nalgebra::Vector4::new(point[0], point[1], point[2], 1.0);
699 let point = model_matrix * point;
700
701 if point.w <= 0.0 {
702 return None;
703 }
704 if point.w < self.camera.near || point.w > self.camera.far {
709 return None;
710 }
711
712 let x_fp = div_checked(to_q16(point.x), to_q16(point.w))?;
713 let y_fp = div_checked(to_q16(point.y), to_q16(point.w))?;
714 let z_ndc = from_q16(div_checked(to_q16(point.z), to_q16(point.w))?);
715
716 let x = ((1.0 + from_q16(x_fp)) * 0.5 * self.width as f32) as i32;
717 let y = ((1.0 - from_q16(y_fp)) * 0.5 * self.height as f32) as i32;
718
719 if x < 0 || x >= self.width as i32 || y < 0 || y >= self.height as i32 {
720 return None;
721 }
722
723 Some(Point3::new(
724 x,
725 y,
726 (z_ndc * (self.camera.far - self.camera.near) + self.camera.near) as i32,
727 ))
728 }
729
730 #[inline(always)]
731 pub fn transform_points<const N: usize>(
732 &self,
733 indices: &[usize; N],
734 vertices: &[[f32; 3]],
735 model_matrix: Matrix4<f32>,
736 ) -> Option<[Point3<i32>; N]> {
737 let mut ret = [Point3::new(0, 0, 0); N];
738
739 for i in 0..N {
740 ret[i] = self.transform_point(&vertices[indices[i]], model_matrix)?;
741 }
742
743 Some(ret)
744 }
745
746 fn transform_point_with_w(
749 &self,
750 point: &[f32; 3],
751 model_matrix: Matrix4<f32>,
752 ) -> Option<(Point3<i32>, f32)> {
753 let v = nalgebra::Vector4::new(point[0], point[1], point[2], 1.0);
754 let clip = model_matrix * v;
755 if clip.w < self.camera.near || clip.w > self.camera.far {
760 return None;
761 }
762 let ndc_x = clip.x / clip.w;
763 let ndc_y = clip.y / clip.w;
764 let ndc_z = clip.z / clip.w;
765 let x = ((1.0 + ndc_x) * 0.5 * self.width as f32) as i32;
766 let y = ((1.0 - ndc_y) * 0.5 * self.height as f32) as i32;
767 if x < 0 || x >= self.width as i32 || y < 0 || y >= self.height as i32 {
768 return None;
769 }
770 let z = (ndc_z * (self.camera.far - self.camera.near) + self.camera.near) as i32;
771 Some((Point3::new(x, y, z), clip.w))
772 }
773
774 #[inline(always)]
776 pub fn transform_points_with_w<const N: usize>(
777 &self,
778 indices: &[usize; N],
779 vertices: &[[f32; 3]],
780 model_matrix: Matrix4<f32>,
781 ) -> Option<([Point3<i32>; N], [f32; N])> {
782 let mut pts = [Point3::new(0, 0, 0); N];
783 let mut ws = [1.0f32; N];
784 for i in 0..N {
785 let (p, w) = self.transform_point_with_w(&vertices[indices[i]], model_matrix)?;
786 pts[i] = p;
787 ws[i] = w;
788 }
789 Some((pts, ws))
790 }
791
792 #[inline]
803 fn is_backface(
804 &self,
805 face: &[usize; 3],
806 vertices: &[[f32; 3]],
807 model_matrix: Matrix4<f32>,
808 world_normal: &Vector3<f32>,
809 ) -> bool {
810 let v0 = vertices[face[0]];
811 let v0_world = if model_matrix == Matrix4::identity() {
812 Point3::new(v0[0], v0[1], v0[2])
813 } else {
814 model_matrix.transform_point(&Point3::new(v0[0], v0[1], v0[2]))
815 };
816 (self.camera.position - v0_world).dot(world_normal) < 0.0
817 }
818
819 #[inline]
837 fn clip_to_screen(&self, c: Vector4<f32>) -> Option<Point3<i32>> {
838 if c.w <= 0.0 {
839 return None;
840 }
841 let mut ndc = Point3::from_homogeneous(c)?;
842 if self.vertex_snap_bits > 0 {
843 let scale = (1u32 << self.vertex_snap_bits) as f32;
844 ndc.x = (ndc.x * scale).round() / scale;
845 ndc.y = (ndc.y * scale).round() / scale;
846 }
847 let w = self.width as f32;
848 let h = self.height as f32;
849 let x = ((1.0 + ndc.x) * 0.5 * w).clamp(-w * 8.0, w * 9.0) as i32;
850 let y = ((1.0 - ndc.y) * 0.5 * h).clamp(-h * 8.0, h * 9.0) as i32;
851 let depth = (ndc.z * (self.camera.far - self.camera.near) + self.camera.near) as i32;
852 Some(Point3::new(x, y, depth))
853 }
854
855 #[inline]
858 fn project_and_emit<F>(
859 &self,
860 c0: Vector4<f32>,
861 c1: Vector4<f32>,
862 c2: Vector4<f32>,
863 color: Rgb565,
864 callback: &mut F,
865 ) where
866 F: FnMut(DrawPrimitive),
867 {
868 if let (Some(p0), Some(p1), Some(p2)) = (
869 self.clip_to_screen(c0),
870 self.clip_to_screen(c1),
871 self.clip_to_screen(c2),
872 ) {
873 callback(DrawPrimitive::ColoredTriangleWithDepth {
874 points: [p0.xy(), p1.xy(), p2.xy()],
875 depths: [p0.z as f32, p1.z as f32, p2.z as f32],
876 color,
877 });
878 }
879 }
880
881 fn clip_polygon_plane(
886 input: &[Vector4<f32>],
887 output: &mut [Vector4<f32>; 8],
888 dist: impl Fn(Vector4<f32>) -> f32,
889 ) -> usize {
890 let n = input.len();
891 let mut m = 0usize;
892 for i in 0..n {
893 let prev = input[(n + i - 1) % n];
894 let curr = input[i];
895 let d_prev = dist(prev);
896 let d_curr = dist(curr);
897 if d_curr >= 0.0 {
898 if d_prev < 0.0 {
899 let t = d_prev / (d_prev - d_curr);
901 if m < 8 {
902 output[m] = prev + (curr - prev) * t;
903 m += 1;
904 }
905 }
906 if m < 8 {
907 output[m] = curr;
908 m += 1;
909 }
910 } else if d_prev >= 0.0 {
911 let t = d_prev / (d_prev - d_curr);
913 if m < 8 {
914 output[m] = prev + (curr - prev) * t;
915 m += 1;
916 }
917 }
918 }
919 m
920 }
921
922 fn emit_clipped<F>(&self, clip: [Vector4<f32>; 3], color: Rgb565, callback: &mut F)
931 where
932 F: FnMut(DrawPrimitive),
933 {
934 let nw = self.camera.near;
935
936 let mut a = [Vector4::zeros(); 8];
937 let mut b = [Vector4::zeros(); 8];
938 a[0] = clip[0];
939 a[1] = clip[1];
940 a[2] = clip[2];
941
942 let n = Self::clip_polygon_plane(&a[..3], &mut b, |v| v.w - nw);
944 if n < 3 {
945 return;
946 }
947 let n = Self::clip_polygon_plane(&b[..n], &mut a, |v| v.x + v.w);
949 if n < 3 {
950 return;
951 }
952 let n = Self::clip_polygon_plane(&a[..n], &mut b, |v| v.w - v.x);
954 if n < 3 {
955 return;
956 }
957 let n = Self::clip_polygon_plane(&b[..n], &mut a, |v| v.y + v.w);
959 if n < 3 {
960 return;
961 }
962 let n = Self::clip_polygon_plane(&a[..n], &mut b, |v| v.w - v.y);
964 if n < 3 {
965 return;
966 }
967
968 for i in 1..n - 1 {
970 self.project_and_emit(b[0], b[i], b[i + 1], color, callback);
971 }
972 }
973
974 fn render<'a, MS, F>(&self, meshes: MS, mut callback: F)
975 where
976 MS: IntoIterator<Item = &'a K3dMesh<'a>>,
977 F: FnMut(DrawPrimitive),
978 {
979 for mesh in meshes {
980 if mesh.geometry.vertices.is_empty() {
981 continue;
982 }
983
984 if self.should_cull_mesh(mesh) {
988 continue;
989 }
990
991 let mesh_pos = mesh.get_position();
993 let distance = (mesh_pos - self.camera.position).norm();
994 let geometry = mesh.select_lod(distance);
995 #[cfg(feature = "lod-crossfade")]
996 let alpha_override = mesh.draw_alpha.get();
997 #[cfg(feature = "lod-crossfade")]
998 let mut emit = |prim: DrawPrimitive| {
999 let prim = match alpha_override {
1000 Some(a) => apply_draw_alpha(prim, a),
1001 None => prim,
1002 };
1003 callback(prim);
1004 };
1005 #[cfg(not(feature = "lod-crossfade"))]
1006 let mut emit = |prim: DrawPrimitive| {
1007 callback(prim);
1008 };
1009
1010 let transform_matrix = self.camera.vp_matrix * mesh.model_matrix;
1011
1012 let render_mode = self.resolve_render_mode(&mesh.render_mode);
1013 let is_textured = matches!(
1014 render_mode,
1015 RenderMode::Textured | RenderMode::TexturedGouraud(_) | RenderMode::MatCap
1016 );
1017
1018 let mut v_cache_plain: [Option<Point3<i32>>; 256] = [None; 256];
1019 let mut v_cache_w: [Option<(Point3<i32>, f32)>; 256] = [None; 256];
1020
1021 let cache_limit = geometry.vertices.len().min(256);
1022 if is_textured {
1023 for i in 0..cache_limit {
1024 v_cache_w[i] =
1025 self.transform_point_with_w(&geometry.vertices[i], transform_matrix);
1026 }
1027 } else {
1028 for i in 0..cache_limit {
1029 v_cache_plain[i] =
1030 self.transform_point(&geometry.vertices[i], transform_matrix);
1031 }
1032 }
1033
1034 let mut get_pt = |idx: usize| -> Option<Point3<i32>> {
1035 if idx < 256 {
1036 v_cache_plain[idx]
1037 } else {
1038 self.transform_point(&geometry.vertices[idx], transform_matrix)
1039 }
1040 };
1041
1042 let get_pt_w = |idx: usize| -> Option<(Point3<i32>, f32)> {
1043 if idx < 256 {
1044 v_cache_w[idx]
1045 } else {
1046 self.transform_point_with_w(&geometry.vertices[idx], transform_matrix)
1047 }
1048 };
1049
1050 let tf_face = |face: &[usize; 3]| -> Option<[Point3<i32>; 3]> {
1051 Some([get_pt(face[0])?, get_pt(face[1])?, get_pt(face[2])?])
1052 };
1053
1054 let tf_face_w = |face: &[usize; 3]| -> Option<([Point3<i32>; 3], [f32; 3])> {
1055 let (p0, w0) = get_pt_w(face[0])?;
1056 let (p1, w1) = get_pt_w(face[1])?;
1057 let (p2, w2) = get_pt_w(face[2])?;
1058 Some(([p0, p1, p2], [w0, w1, w2]))
1059 };
1060
1061 if let Some(out_color) = mesh.outline_color {
1062 if mesh.outline_width > 0.0 {
1063 let has_vertex_normals = !geometry.vertex_normals.is_empty();
1064 let has_face_normals = !geometry.normals.is_empty();
1065
1066 for (face_idx, face) in geometry.faces.iter().enumerate() {
1067 let face_normal = if has_face_normals {
1068 Vector3::new(
1069 geometry.normals[face_idx][0],
1070 geometry.normals[face_idx][1],
1071 geometry.normals[face_idx][2],
1072 )
1073 } else {
1074 let v0 = Vector3::new(
1075 geometry.vertices[face[0]][0],
1076 geometry.vertices[face[0]][1],
1077 geometry.vertices[face[0]][2],
1078 );
1079 let v1 = Vector3::new(
1080 geometry.vertices[face[1]][0],
1081 geometry.vertices[face[1]][1],
1082 geometry.vertices[face[1]][2],
1083 );
1084 let v2 = Vector3::new(
1085 geometry.vertices[face[2]][0],
1086 geometry.vertices[face[2]][1],
1087 geometry.vertices[face[2]][2],
1088 );
1089 (v1 - v0).cross(&(v2 - v0)).normalize()
1090 };
1091
1092 let transformed_normal = mesh.model_matrix.transform_vector(&face_normal);
1093 if !self.is_backface(
1094 face,
1095 geometry.vertices,
1096 mesh.model_matrix,
1097 &transformed_normal,
1098 ) {
1099 continue;
1100 }
1101
1102 let mut pts = [Point3::origin(); 3];
1103 let mut valid = true;
1104 for i in 0..3 {
1105 let vn = if has_vertex_normals {
1106 Vector3::new(
1107 geometry.vertex_normals[face[i]][0],
1108 geometry.vertex_normals[face[i]][1],
1109 geometry.vertex_normals[face[i]][2],
1110 )
1111 } else {
1112 face_normal
1113 };
1114 let vpos = geometry.vertices[face[i]];
1115 let ext_v = [
1116 vpos[0] + vn.x * mesh.outline_width,
1117 vpos[1] + vn.y * mesh.outline_width,
1118 vpos[2] + vn.z * mesh.outline_width,
1119 ];
1120 if let Some(pt) = self.transform_point(&ext_v, transform_matrix) {
1121 pts[i] = pt;
1122 } else {
1123 valid = false;
1124 break;
1125 }
1126 }
1127
1128 if valid {
1129 emit(DrawPrimitive::ColoredTriangleWithDepth {
1130 points: [pts[0].xy(), pts[1].xy(), pts[2].xy()],
1131 depths: [pts[0].z as f32, pts[1].z as f32, pts[2].z as f32],
1132 color: out_color,
1133 });
1134 }
1135 }
1136 }
1137 }
1138
1139 let render_mode = self.resolve_render_mode(&mesh.render_mode);
1140 match render_mode {
1141 RenderMode::Points => {
1142 let screen_space_points = (0..geometry.vertices.len()).filter_map(&mut get_pt);
1143
1144 if geometry.colors.len() == geometry.vertices.len() {
1145 for (point, color) in screen_space_points.zip(geometry.colors) {
1146 emit(DrawPrimitive::ColoredPoint(point.xy(), *color));
1147 }
1148 } else {
1149 for point in screen_space_points {
1150 emit(DrawPrimitive::ColoredPoint(point.xy(), mesh.color));
1151 }
1152 }
1153 }
1154
1155 RenderMode::Lines if !geometry.lines.is_empty() => {
1156 for line in geometry.lines {
1157 if let (Some(p1), Some(p2)) = (get_pt(line[0]), get_pt(line[1])) {
1158 emit(DrawPrimitive::Line([p1.xy(), p2.xy()], mesh.color));
1159 }
1160 }
1161 }
1162
1163 RenderMode::Lines if !geometry.faces.is_empty() => {
1164 for face in geometry.faces {
1165 if let Some([p1, p2, p3]) = tf_face(face) {
1166 emit(DrawPrimitive::Line([p1.xy(), p2.xy()], mesh.color));
1167 emit(DrawPrimitive::Line([p2.xy(), p3.xy()], mesh.color));
1168 emit(DrawPrimitive::Line([p3.xy(), p1.xy()], mesh.color));
1169 }
1170 }
1171 }
1172
1173 RenderMode::Lines => {}
1174
1175 RenderMode::SolidLightDir(direction) => {
1176 let color_as_float = Vector3::new(
1177 mesh.color.r() as f32 / 32.0,
1178 mesh.color.g() as f32 / 64.0,
1179 mesh.color.b() as f32 / 32.0,
1180 );
1181 let ambient_color = color_as_float * 0.1;
1182 let adjusted_dir = Vector3::new(direction.x, direction.y, -direction.z);
1183
1184 for (face, normal) in geometry.faces.iter().zip(geometry.normals.iter()) {
1185 let normal = Vector3::new(normal[0], normal[1], normal[2]);
1186 let transformed_normal = mesh.model_matrix.transform_vector(&normal);
1187 if self.is_backface(
1188 face,
1189 geometry.vertices,
1190 mesh.model_matrix,
1191 &transformed_normal,
1192 ) {
1193 continue;
1194 }
1195
1196 if let Some([p1, p2, p3]) = tf_face(face) {
1197 let intensity = transformed_normal.dot(&adjusted_dir).max(0.0);
1198 let final_color = color_as_float * intensity + ambient_color;
1199 let final_color = Vector3::new(
1200 final_color.x.clamp(0.0, 1.0),
1201 final_color.y.clamp(0.0, 1.0),
1202 final_color.z.clamp(0.0, 1.0),
1203 );
1204 let mut color = Rgb565::new(
1205 (final_color.x * 31.0) as u8,
1206 (final_color.y * 63.0) as u8,
1207 (final_color.z * 31.0) as u8,
1208 );
1209 if !self.point_lights.is_empty() {
1210 let wc = Self::face_world_center(
1211 face,
1212 geometry.vertices,
1213 mesh.model_matrix,
1214 );
1215 color = Self::add_tint(color, self.light_tint_at(wc));
1216 }
1217 emit(DrawPrimitive::ColoredTriangleWithDepth {
1218 points: [p1.xy(), p2.xy(), p3.xy()],
1219 depths: [p1.z as f32, p2.z as f32, p3.z as f32],
1220 color,
1221 });
1222 }
1223 }
1224 }
1225
1226 RenderMode::GouraudLightDir(direction) => {
1227 let color_as_float = Vector3::new(
1228 mesh.color.r() as f32 / 32.0,
1229 mesh.color.g() as f32 / 64.0,
1230 mesh.color.b() as f32 / 32.0,
1231 );
1232 let ambient_color = color_as_float * 0.1;
1233 let adjusted_dir = Vector3::new(direction.x, direction.y, -direction.z);
1234
1235 for (face, face_normal) in geometry.faces.iter().zip(geometry.normals.iter()) {
1236 let fn_vec = Vector3::new(face_normal[0], face_normal[1], face_normal[2]);
1237 let transformed_fn = mesh.model_matrix.transform_vector(&fn_vec);
1238
1239 if self.is_backface(
1240 face,
1241 geometry.vertices,
1242 mesh.model_matrix,
1243 &transformed_fn,
1244 ) {
1245 continue;
1246 }
1247
1248 if let Some([p1, p2, p3]) = tf_face(face) {
1249 let vertex_colors: [Rgb565; 3] = core::array::from_fn(|k| {
1250 let vn = if !geometry.vertex_normals.is_empty() {
1251 let vn_arr = geometry.vertex_normals[face[k]];
1252 let vn_vec = Vector3::new(vn_arr[0], vn_arr[1], vn_arr[2]);
1253 mesh.model_matrix.transform_vector(&vn_vec)
1254 } else {
1255 transformed_fn
1256 };
1257
1258 let intensity = vn.dot(&adjusted_dir).max(0.0);
1259 let c = color_as_float * intensity + ambient_color;
1260 let mut vc = Rgb565::new(
1261 (c.x.clamp(0.0, 1.0) * 31.0) as u8,
1262 (c.y.clamp(0.0, 1.0) * 63.0) as u8,
1263 (c.z.clamp(0.0, 1.0) * 31.0) as u8,
1264 );
1265 if !self.point_lights.is_empty() {
1266 let vpos = geometry.vertices[face[k]];
1267 let wp = mesh
1268 .model_matrix
1269 .transform_point(&Point3::new(vpos[0], vpos[1], vpos[2]));
1270 vc = Self::add_tint(vc, self.light_tint_at(wp));
1271 }
1272 vc
1273 });
1274
1275 emit(DrawPrimitive::GouraudTriangleWithDepth {
1276 points: [p1.xy(), p2.xy(), p3.xy()],
1277 depths: [p1.z as f32, p2.z as f32, p3.z as f32],
1278 colors: vertex_colors,
1279 });
1280 }
1281 }
1282 }
1283
1284 RenderMode::Toon(direction, bands) => {
1285 let color_as_float = Vector3::new(
1286 mesh.color.r() as f32 / 32.0,
1287 mesh.color.g() as f32 / 64.0,
1288 mesh.color.b() as f32 / 32.0,
1289 );
1290 let ambient_color = color_as_float * 0.15;
1291 let adjusted_dir = Vector3::new(direction.x, direction.y, -direction.z);
1292 let bands_f = bands.max(1) as f32;
1293
1294 for (face, normal) in geometry.faces.iter().zip(geometry.normals.iter()) {
1295 let normal = Vector3::new(normal[0], normal[1], normal[2]);
1296 let transformed_normal = mesh.model_matrix.transform_vector(&normal);
1297 if self.is_backface(
1298 face,
1299 geometry.vertices,
1300 mesh.model_matrix,
1301 &transformed_normal,
1302 ) {
1303 continue;
1304 }
1305
1306 if let Some([p1, p2, p3]) = tf_face(face) {
1307 let raw_intensity = transformed_normal.dot(&adjusted_dir).max(0.0);
1308 let intensity =
1309 ((raw_intensity * bands_f).round() / bands_f).clamp(0.0, 1.0);
1310
1311 let final_color = color_as_float * intensity + ambient_color;
1312 let final_color = Vector3::new(
1313 final_color.x.clamp(0.0, 1.0),
1314 final_color.y.clamp(0.0, 1.0),
1315 final_color.z.clamp(0.0, 1.0),
1316 );
1317 let mut color = Rgb565::new(
1318 (final_color.x * 31.0) as u8,
1319 (final_color.y * 63.0) as u8,
1320 (final_color.z * 31.0) as u8,
1321 );
1322 if !self.point_lights.is_empty() {
1323 let wc = Self::face_world_center(
1324 face,
1325 geometry.vertices,
1326 mesh.model_matrix,
1327 );
1328 color = Self::add_tint(color, self.light_tint_at(wc));
1329 }
1330 emit(DrawPrimitive::ColoredTriangleWithDepth {
1331 points: [p1.xy(), p2.xy(), p3.xy()],
1332 depths: [p1.z as f32, p2.z as f32, p3.z as f32],
1333 color,
1334 });
1335 }
1336 }
1337 }
1338
1339 RenderMode::BlinnPhong {
1340 light_dir,
1341 specular_intensity,
1342 shininess,
1343 } => {
1344 let color_as_float = Vector3::new(
1346 mesh.color.r() as f32 / 32.0,
1347 mesh.color.g() as f32 / 64.0,
1348 mesh.color.b() as f32 / 32.0,
1349 );
1350
1351 let ambient_color = color_as_float * 0.1;
1353
1354 let adjusted_light_dir = Vector3::new(light_dir.x, light_dir.y, -light_dir.z);
1357
1358 let light_dir_normalized = adjusted_light_dir.normalize();
1360
1361 for (face, normal) in geometry.faces.iter().zip(geometry.normals.iter()) {
1362 let normal = Vector3::new(normal[0], normal[1], normal[2]);
1364 let transformed_normal = mesh.model_matrix.transform_vector(&normal);
1365 let normalized_normal = transformed_normal.normalize();
1366
1367 if self.is_backface(
1369 face,
1370 geometry.vertices,
1371 mesh.model_matrix,
1372 &normalized_normal,
1373 ) {
1374 continue;
1375 }
1376
1377 if let Some([p1, p2, p3]) = tf_face(face) {
1378 let v0 = geometry.vertices[face[0]];
1380 let v1 = geometry.vertices[face[1]];
1381 let v2 = geometry.vertices[face[2]];
1382 let face_center = Point3::new(
1383 (v0[0] + v1[0] + v2[0]) / 3.0,
1384 (v0[1] + v1[1] + v2[1]) / 3.0,
1385 (v0[2] + v1[2] + v2[2]) / 3.0,
1386 );
1387 let face_center_world = mesh.model_matrix.transform_point(&face_center);
1388
1389 let view_dir = (self.camera.position - face_center_world).normalize();
1391
1392 let half_vector = (light_dir_normalized + view_dir).normalize();
1394
1395 let diffuse_intensity =
1397 normalized_normal.dot(&light_dir_normalized).max(0.0);
1398
1399 let specular_term =
1401 normalized_normal.dot(&half_vector).max(0.0).powf(shininess);
1402
1403 let diffuse_color = color_as_float * diffuse_intensity;
1405 let specular_color =
1406 Vector3::new(1.0, 1.0, 1.0) * specular_term * specular_intensity;
1407 let final_color = ambient_color + diffuse_color + specular_color;
1408
1409 let final_color = Vector3::new(
1410 final_color.x.clamp(0.0, 1.0),
1411 final_color.y.clamp(0.0, 1.0),
1412 final_color.z.clamp(0.0, 1.0),
1413 );
1414
1415 let mut color = Rgb565::new(
1416 (final_color.x * 31.0) as u8,
1417 (final_color.y * 63.0) as u8,
1418 (final_color.z * 31.0) as u8,
1419 );
1420 if !self.point_lights.is_empty() {
1421 color =
1422 Self::add_tint(color, self.light_tint_at(face_center_world));
1423 }
1424 emit(DrawPrimitive::ColoredTriangleWithDepth {
1425 points: [p1.xy(), p2.xy(), p3.xy()],
1426 depths: [p1.z as f32, p2.z as f32, p3.z as f32],
1427 color,
1428 });
1429 }
1430 }
1431 }
1432
1433 RenderMode::Solid => {
1434 if geometry.normals.is_empty() {
1435 for face in geometry.faces.iter() {
1436 let color = if !self.point_lights.is_empty() {
1437 let wc = Self::face_world_center(
1438 face,
1439 geometry.vertices,
1440 mesh.model_matrix,
1441 );
1442 Self::add_tint(mesh.color, self.light_tint_at(wc))
1443 } else {
1444 mesh.color
1445 };
1446 let v = &geometry.vertices;
1447 let clip = [
1448 transform_matrix
1449 * Vector4::new(
1450 v[face[0]][0],
1451 v[face[0]][1],
1452 v[face[0]][2],
1453 1.0,
1454 ),
1455 transform_matrix
1456 * Vector4::new(
1457 v[face[1]][0],
1458 v[face[1]][1],
1459 v[face[1]][2],
1460 1.0,
1461 ),
1462 transform_matrix
1463 * Vector4::new(
1464 v[face[2]][0],
1465 v[face[2]][1],
1466 v[face[2]][2],
1467 1.0,
1468 ),
1469 ];
1470 self.emit_clipped(clip, color, &mut callback);
1471 }
1472 } else {
1473 for (face, normal) in geometry.faces.iter().zip(geometry.normals) {
1474 let normal = Vector3::new(normal[0], normal[1], normal[2]);
1475 let transformed_normal = mesh.model_matrix.transform_vector(&normal);
1476 if self.is_backface(
1477 face,
1478 geometry.vertices,
1479 mesh.model_matrix,
1480 &transformed_normal,
1481 ) {
1482 continue;
1483 }
1484 let color = if !self.point_lights.is_empty() {
1485 let wc = Self::face_world_center(
1486 face,
1487 geometry.vertices,
1488 mesh.model_matrix,
1489 );
1490 Self::add_tint(mesh.color, self.light_tint_at(wc))
1491 } else {
1492 mesh.color
1493 };
1494 let v = &geometry.vertices;
1495 let clip = [
1496 transform_matrix
1497 * Vector4::new(
1498 v[face[0]][0],
1499 v[face[0]][1],
1500 v[face[0]][2],
1501 1.0,
1502 ),
1503 transform_matrix
1504 * Vector4::new(
1505 v[face[1]][0],
1506 v[face[1]][1],
1507 v[face[1]][2],
1508 1.0,
1509 ),
1510 transform_matrix
1511 * Vector4::new(
1512 v[face[2]][0],
1513 v[face[2]][1],
1514 v[face[2]][2],
1515 1.0,
1516 ),
1517 ];
1518 self.emit_clipped(clip, color, &mut callback);
1519 }
1520 }
1521 }
1522
1523 RenderMode::SectorBright(brightness) => {
1524 if geometry.normals.is_empty() {
1525 for face in geometry.faces.iter() {
1526 let wc =
1527 Self::face_world_center(face, geometry.vertices, mesh.model_matrix);
1528 let mut color = self.sector_shaded_color(mesh.color, brightness, wc);
1529 if !self.point_lights.is_empty() {
1530 color = Self::add_tint(color, self.light_tint_at(wc));
1531 }
1532 let v = &geometry.vertices;
1533 let clip = [
1534 transform_matrix
1535 * Vector4::new(
1536 v[face[0]][0],
1537 v[face[0]][1],
1538 v[face[0]][2],
1539 1.0,
1540 ),
1541 transform_matrix
1542 * Vector4::new(
1543 v[face[1]][0],
1544 v[face[1]][1],
1545 v[face[1]][2],
1546 1.0,
1547 ),
1548 transform_matrix
1549 * Vector4::new(
1550 v[face[2]][0],
1551 v[face[2]][1],
1552 v[face[2]][2],
1553 1.0,
1554 ),
1555 ];
1556 self.emit_clipped(clip, color, &mut callback);
1557 }
1558 } else {
1559 for (face, normal) in geometry.faces.iter().zip(geometry.normals) {
1560 let normal = Vector3::new(normal[0], normal[1], normal[2]);
1561 let transformed_normal = mesh.model_matrix.transform_vector(&normal);
1562 if self.is_backface(
1563 face,
1564 geometry.vertices,
1565 mesh.model_matrix,
1566 &transformed_normal,
1567 ) {
1568 continue;
1569 }
1570 let wc =
1571 Self::face_world_center(face, geometry.vertices, mesh.model_matrix);
1572 let mut color = self.sector_shaded_color(mesh.color, brightness, wc);
1573 if !self.point_lights.is_empty() {
1574 color = Self::add_tint(color, self.light_tint_at(wc));
1575 }
1576 let v = &geometry.vertices;
1577 let clip = [
1578 transform_matrix
1579 * Vector4::new(
1580 v[face[0]][0],
1581 v[face[0]][1],
1582 v[face[0]][2],
1583 1.0,
1584 ),
1585 transform_matrix
1586 * Vector4::new(
1587 v[face[1]][0],
1588 v[face[1]][1],
1589 v[face[1]][2],
1590 1.0,
1591 ),
1592 transform_matrix
1593 * Vector4::new(
1594 v[face[2]][0],
1595 v[face[2]][1],
1596 v[face[2]][2],
1597 1.0,
1598 ),
1599 ];
1600 self.emit_clipped(clip, color, &mut callback);
1601 }
1602 }
1603 }
1604
1605 RenderMode::Textured => {
1606 let Some(texture_id) = geometry.texture_id else {
1611 continue;
1612 };
1613 if geometry.uvs.is_empty() {
1614 continue;
1615 }
1616
1617 if geometry.normals.is_empty() {
1623 for face in geometry.faces.iter() {
1624 if let Some((points, ws)) = tf_face_w(face) {
1625 emit(DrawPrimitive::TexturedTriangleWithDepth {
1626 points: [points[0].xy(), points[1].xy(), points[2].xy()],
1627 depths: [
1628 points[0].z as f32,
1629 points[1].z as f32,
1630 points[2].z as f32,
1631 ],
1632 ws,
1633 uvs: [
1634 geometry.uvs[face[0]],
1635 geometry.uvs[face[1]],
1636 geometry.uvs[face[2]],
1637 ],
1638 texture_id,
1639 });
1640 }
1641 }
1642 } else {
1643 for (face, normal) in geometry.faces.iter().zip(geometry.normals) {
1644 let normal = Vector3::new(normal[0], normal[1], normal[2]);
1645 let transformed_normal = mesh.model_matrix.transform_vector(&normal);
1646 if self.is_backface(
1647 face,
1648 geometry.vertices,
1649 mesh.model_matrix,
1650 &transformed_normal,
1651 ) {
1652 continue;
1653 }
1654 if let Some((points, ws)) = tf_face_w(face) {
1655 emit(DrawPrimitive::TexturedTriangleWithDepth {
1656 points: [points[0].xy(), points[1].xy(), points[2].xy()],
1657 depths: [
1658 points[0].z as f32,
1659 points[1].z as f32,
1660 points[2].z as f32,
1661 ],
1662 ws,
1663 uvs: [
1664 geometry.uvs[face[0]],
1665 geometry.uvs[face[1]],
1666 geometry.uvs[face[2]],
1667 ],
1668 texture_id,
1669 });
1670 }
1671 }
1672 }
1673 }
1674 RenderMode::TexturedGouraud(direction) => {
1675 let Some(texture_id) = geometry.texture_id else {
1676 continue;
1677 };
1678 if geometry.uvs.is_empty() {
1679 continue;
1680 }
1681 let color_as_float = Vector3::new(
1682 mesh.color.r() as f32 / 32.0,
1683 mesh.color.g() as f32 / 64.0,
1684 mesh.color.b() as f32 / 32.0,
1685 );
1686 let ambient_color = color_as_float * 0.1;
1687 let adjusted_dir = Vector3::new(direction.x, direction.y, -direction.z);
1688
1689 if geometry.normals.is_empty() {
1690 for face in geometry.faces.iter() {
1691 if let Some((points, ws)) = tf_face_w(face) {
1692 let vertex_colors = [mesh.color, mesh.color, mesh.color];
1693 emit(DrawPrimitive::TexturedGouraudTriangleWithDepth {
1694 points: [points[0].xy(), points[1].xy(), points[2].xy()],
1695 depths: [
1696 points[0].z as f32,
1697 points[1].z as f32,
1698 points[2].z as f32,
1699 ],
1700 ws,
1701 uvs: [
1702 geometry.uvs[face[0]],
1703 geometry.uvs[face[1]],
1704 geometry.uvs[face[2]],
1705 ],
1706 colors: vertex_colors,
1707 texture_id,
1708 });
1709 }
1710 }
1711 } else {
1712 for (face, face_normal) in
1713 geometry.faces.iter().zip(geometry.normals.iter())
1714 {
1715 let fn_vec =
1716 Vector3::new(face_normal[0], face_normal[1], face_normal[2]);
1717 let transformed_fn = mesh.model_matrix.transform_vector(&fn_vec);
1718
1719 if self.is_backface(
1720 face,
1721 geometry.vertices,
1722 mesh.model_matrix,
1723 &transformed_fn,
1724 ) {
1725 continue;
1726 }
1727
1728 if let Some((points, ws)) = tf_face_w(face) {
1729 let vertex_colors: [Rgb565; 3] = core::array::from_fn(|k| {
1730 let vn = if !geometry.vertex_normals.is_empty() {
1731 let vn_arr = geometry.vertex_normals[face[k]];
1732 let vn_vec = Vector3::new(vn_arr[0], vn_arr[1], vn_arr[2]);
1733 mesh.model_matrix.transform_vector(&vn_vec)
1734 } else {
1735 transformed_fn
1736 };
1737
1738 let intensity = vn.dot(&adjusted_dir).max(0.0);
1739 let c = color_as_float * intensity + ambient_color;
1740 let mut vc = Rgb565::new(
1741 (c.x.clamp(0.0, 1.0) * 31.0) as u8,
1742 (c.y.clamp(0.0, 1.0) * 63.0) as u8,
1743 (c.z.clamp(0.0, 1.0) * 31.0) as u8,
1744 );
1745 if !self.point_lights.is_empty() {
1746 let vpos = geometry.vertices[face[k]];
1747 let wp = mesh.model_matrix.transform_point(&Point3::new(
1748 vpos[0], vpos[1], vpos[2],
1749 ));
1750 vc = Self::add_tint(vc, self.light_tint_at(wp));
1751 }
1752 vc
1753 });
1754
1755 emit(DrawPrimitive::TexturedGouraudTriangleWithDepth {
1756 points: [points[0].xy(), points[1].xy(), points[2].xy()],
1757 depths: [
1758 points[0].z as f32,
1759 points[1].z as f32,
1760 points[2].z as f32,
1761 ],
1762 ws,
1763 uvs: [
1764 geometry.uvs[face[0]],
1765 geometry.uvs[face[1]],
1766 geometry.uvs[face[2]],
1767 ],
1768 colors: vertex_colors,
1769 texture_id,
1770 });
1771 }
1772 }
1773 }
1774 }
1775 RenderMode::MatCap => {
1776 let Some(texture_id) = geometry.texture_id else {
1777 continue;
1778 };
1779 let has_vertex_normals = !geometry.vertex_normals.is_empty();
1780 let has_face_normals = !geometry.normals.is_empty();
1781 if !has_vertex_normals && !has_face_normals {
1782 continue;
1783 }
1784
1785 let mv = self.camera.view_matrix * mesh.model_matrix;
1786 let normal_matrix = nalgebra::Matrix3::new(
1787 mv[(0, 0)],
1788 mv[(0, 1)],
1789 mv[(0, 2)],
1790 mv[(1, 0)],
1791 mv[(1, 1)],
1792 mv[(1, 2)],
1793 mv[(2, 0)],
1794 mv[(2, 1)],
1795 mv[(2, 2)],
1796 );
1797
1798 for (face_idx, face) in geometry.faces.iter().enumerate() {
1799 let face_normal = if has_face_normals {
1800 Vector3::new(
1801 geometry.normals[face_idx][0],
1802 geometry.normals[face_idx][1],
1803 geometry.normals[face_idx][2],
1804 )
1805 } else {
1806 let v0 = Vector3::new(
1807 geometry.vertices[face[0]][0],
1808 geometry.vertices[face[0]][1],
1809 geometry.vertices[face[0]][2],
1810 );
1811 let v1 = Vector3::new(
1812 geometry.vertices[face[1]][0],
1813 geometry.vertices[face[1]][1],
1814 geometry.vertices[face[1]][2],
1815 );
1816 let v2 = Vector3::new(
1817 geometry.vertices[face[2]][0],
1818 geometry.vertices[face[2]][1],
1819 geometry.vertices[face[2]][2],
1820 );
1821 (v1 - v0).cross(&(v2 - v0)).normalize()
1822 };
1823
1824 let transformed_normal = mesh.model_matrix.transform_vector(&face_normal);
1825 if self.is_backface(
1826 face,
1827 geometry.vertices,
1828 mesh.model_matrix,
1829 &transformed_normal,
1830 ) {
1831 continue;
1832 }
1833
1834 if let Some((points, ws)) = tf_face_w(face) {
1835 let mut uvs = [[0.0f32; 2]; 3];
1836 for i in 0..3 {
1837 let vertex_normal = if has_vertex_normals {
1838 Vector3::new(
1839 geometry.vertex_normals[face[i]][0],
1840 geometry.vertex_normals[face[i]][1],
1841 geometry.vertex_normals[face[i]][2],
1842 )
1843 } else {
1844 face_normal
1845 };
1846 let view_normal = (normal_matrix * vertex_normal).normalize();
1847 let u = view_normal.x * 0.5 + 0.5;
1848 let v = -view_normal.y * 0.5 + 0.5;
1849 uvs[i] = [u, v];
1850 }
1851
1852 emit(DrawPrimitive::TexturedTriangleWithDepth {
1853 points: [points[0].xy(), points[1].xy(), points[2].xy()],
1854 depths: [
1855 points[0].z as f32,
1856 points[1].z as f32,
1857 points[2].z as f32,
1858 ],
1859 ws,
1860 uvs,
1861 texture_id,
1862 });
1863 }
1864 }
1865 }
1866 }
1867 }
1868 }
1869
1870 pub fn record<'a, MS, const MAX: usize>(
1871 &self,
1872 meshes: MS,
1873 commands: &mut crate::command_buffer::CommandBuffer<MAX>,
1874 telemetry: Option<&mut crate::telemetry::RecordTelemetry>,
1875 ) -> Result<(), crate::error::RenderError>
1876 where
1877 MS: IntoIterator<Item = &'a K3dMesh<'a>>,
1878 {
1879 self.record_impl(meshes, commands, telemetry)
1880 }
1881
1882 pub fn record_drop_shadow<const MAX: usize>(
1885 &self,
1886 mesh: &K3dMesh,
1887 floor_y: f32,
1888 shadow_radius: f32,
1889 max_fade_distance: f32,
1890 shadow_opacity: u8,
1891 color: Rgb565,
1892 commands: &mut crate::command_buffer::CommandBuffer<MAX>,
1893 ) -> Result<(), crate::error::RenderError> {
1894 let pos = mesh.get_position();
1895 let height = pos.y - floor_y;
1896 if height < 0.0 || height >= max_fade_distance {
1897 return Ok(());
1898 }
1899
1900 let fade = 1.0 - (height / max_fade_distance).clamp(0.0, 1.0);
1901 let radius = shadow_radius * fade;
1902 let opacity = (shadow_opacity as f32 * fade) as u8;
1903 if opacity == 0 {
1904 return Ok(());
1905 }
1906
1907 let y_pos = floor_y + 0.01;
1908 let center_world = [pos.x, y_pos, pos.z];
1909 let center_proj = self.transform_point_with_w(¢er_world, self.camera.vp_matrix);
1910 let Some((c_pt, _c_w)) = center_proj else {
1911 return Ok(());
1912 };
1913
1914 let mut outer_proj: [Option<(Point3<i32>, f32)>; 8] = [None; 8];
1915 for i in 0..8 {
1916 let angle = (i as f32) * (core::f32::consts::PI / 4.0);
1917 let px = pos.x + radius * micromath::F32Ext::cos(angle);
1918 let pz = pos.z + radius * micromath::F32Ext::sin(angle);
1919 outer_proj[i] = self.transform_point_with_w(&[px, y_pos, pz], self.camera.vp_matrix);
1920 }
1921
1922 for i in 0..8 {
1923 let next_idx = (i + 1) % 8;
1924 if let (Some((p1, _w1)), Some((p2, _w2))) = (outer_proj[i], outer_proj[next_idx]) {
1925 commands.push(crate::command_buffer::RenderCommand::Draw(
1926 DrawPrimitive::TranslucentTriangleWithDepth {
1927 points: [c_pt.xy(), p1.xy(), p2.xy()],
1928 depths: [c_pt.z as f32, p1.z as f32, p2.z as f32],
1929 color,
1930 alpha: opacity,
1931 },
1932 ))?;
1933 }
1934 }
1935
1936 Ok(())
1937 }
1938
1939 fn record_impl<'a, MS, const MAX: usize>(
1940 &self,
1941 meshes: MS,
1942 commands: &mut crate::command_buffer::CommandBuffer<MAX>,
1943 telemetry: Option<&mut crate::telemetry::RecordTelemetry>,
1944 ) -> Result<(), crate::error::RenderError>
1945 where
1946 MS: IntoIterator<Item = &'a K3dMesh<'a>>,
1947 {
1948 use crate::command_buffer::RenderCommand;
1949
1950 commands.clear();
1951 commands.push(RenderCommand::ClearDepth(crate::Z_MAX_VALUE))?;
1952 if let Some(caps) = self.caps {
1953 caps.validate_framebuffer(self.width as usize, self.height as usize)?;
1954 }
1955
1956 let mut first_error = None;
1957 let mut visible_meshes = 0usize;
1958 let mut used_texture_ids: heapless::Vec<u32, 64> = heapless::Vec::new();
1959 let mut meshes_total = 0usize;
1960
1961 #[cfg(feature = "record-sort")]
1962 {
1963 let mut sorted: heapless::Vec<(u8, i32, &K3dMesh<'a>), 256> = heapless::Vec::new();
1964 for mesh in meshes {
1965 meshes_total += 1;
1966 if mesh.geometry.vertices.is_empty() {
1967 continue;
1968 }
1969 if self.should_cull_mesh(mesh) {
1970 continue;
1971 }
1972 let distance = (mesh.get_position() - self.camera.position).norm();
1973 let dist_key = (distance * 1000.0) as i32;
1974 if sorted.push((mesh.priority, dist_key, mesh)).is_err() {
1975 break;
1976 }
1977 }
1978 sorted.sort_unstable_by(|a, b| b.0.cmp(&a.0).then_with(|| a.1.cmp(&b.1)));
1979
1980 for &(_, _, mesh) in sorted.iter() {
1981 Self::record_one_mesh(
1982 self,
1983 mesh,
1984 commands,
1985 &mut first_error,
1986 &mut visible_meshes,
1987 &mut used_texture_ids,
1988 )?;
1989 if let Some(err) = first_error.take() {
1990 return Err(err);
1991 }
1992 }
1993 }
1994
1995 #[cfg(not(feature = "record-sort"))]
1996 {
1997 for mesh in meshes {
1998 meshes_total += 1;
1999 if mesh.geometry.vertices.is_empty() {
2000 continue;
2001 }
2002 if self.should_cull_mesh(mesh) {
2003 continue;
2004 }
2005 Self::record_one_mesh(
2006 self,
2007 mesh,
2008 commands,
2009 &mut first_error,
2010 &mut visible_meshes,
2011 &mut used_texture_ids,
2012 )?;
2013 if let Some(err) = first_error.take() {
2014 return Err(err);
2015 }
2016 }
2017 }
2018
2019 if let Some(t) = telemetry {
2020 t.meshes_total = meshes_total;
2021 t.meshes_visible = visible_meshes;
2022 t.unique_textures = used_texture_ids.len();
2023 t.draw_commands = commands
2024 .iter()
2025 .filter(|cmd| matches!(cmd, RenderCommand::Draw(_)))
2026 .count();
2027 t.fallback_used = false;
2028 t.degradation_steps_applied = 0;
2029 t.dropped_meshes = 0;
2030 }
2031
2032 Ok(())
2033 }
2034
2035 fn record_one_mesh<'a, const MAX: usize>(
2036 &self,
2037 mesh: &'a K3dMesh<'a>,
2038 commands: &mut crate::command_buffer::CommandBuffer<MAX>,
2039 first_error: &mut Option<crate::error::RenderError>,
2040 visible_meshes: &mut usize,
2041 used_texture_ids: &mut heapless::Vec<u32, 64>,
2042 ) -> Result<(), crate::error::RenderError> {
2043 use crate::command_buffer::RenderCommand;
2044 use crate::error::{BudgetKind, RenderError};
2045
2046 let distance = (mesh.get_position() - self.camera.position).norm();
2047 let geometry = mesh.select_lod(distance);
2048
2049 if let Some(caps) = self.caps {
2050 *visible_meshes += 1;
2051 if *visible_meshes > caps.max_meshes_per_frame {
2052 return Err(RenderError::OutOfBudget(BudgetKind::MeshesPerFrame {
2053 attempted: *visible_meshes,
2054 max: caps.max_meshes_per_frame,
2055 }));
2056 }
2057
2058 if geometry.vertices.len() > caps.max_vertices_per_mesh {
2059 return Err(RenderError::OutOfBudget(BudgetKind::VerticesPerMesh {
2060 attempted: geometry.vertices.len(),
2061 max: caps.max_vertices_per_mesh,
2062 }));
2063 }
2064
2065 if geometry.faces.len() > caps.max_triangles_per_mesh {
2066 return Err(RenderError::OutOfBudget(BudgetKind::TrianglesPerMesh {
2067 attempted: geometry.faces.len(),
2068 max: caps.max_triangles_per_mesh,
2069 }));
2070 }
2071
2072 if let Some(texture_id) = geometry.texture_id
2073 && !used_texture_ids.contains(&texture_id)
2074 {
2075 let attempted = used_texture_ids.len() + 1;
2076 if attempted > caps.max_textures {
2077 return Err(RenderError::OutOfBudget(BudgetKind::Textures {
2078 attempted,
2079 max: caps.max_textures,
2080 }));
2081 }
2082
2083 if used_texture_ids.push(texture_id).is_err() {
2084 return Err(RenderError::OutOfBudget(BudgetKind::Textures {
2085 attempted,
2086 max: caps.max_textures,
2087 }));
2088 }
2089 }
2090 } else {
2091 *visible_meshes += 1;
2092 }
2093
2094 let mut push_draw = |primitive: DrawPrimitive, first_error: &mut Option<RenderError>| {
2095 if first_error.is_none()
2096 && let Err(e) = commands.push(RenderCommand::Draw(primitive))
2097 {
2098 *first_error = Some(e);
2099 }
2100 };
2101
2102 #[cfg(feature = "lod-crossfade")]
2103 {
2104 match mesh.select_lod_pick(distance) {
2105 mesh::LodPick::Single(_) => {
2106 mesh.lod_force.set(None);
2107 mesh.draw_alpha.set(None);
2108 self.render(core::iter::once(mesh), |primitive| {
2109 push_draw(primitive, first_error);
2110 });
2111 }
2112 mesh::LodPick::Crossfade { near, far, t } => {
2113 let near_lvl = mesh.lod_level_of(near);
2114 let far_lvl = mesh.lod_level_of(far);
2115 if t < 0.5 {
2116 mesh.lod_force.set(Some(near_lvl));
2117 mesh.draw_alpha.set(None);
2118 self.render(core::iter::once(mesh), |primitive| {
2119 push_draw(primitive, first_error);
2120 });
2121 let a = (t * 255.0) as u8;
2122 if a > 16 {
2123 mesh.lod_force.set(Some(far_lvl));
2124 mesh.draw_alpha.set(Some(a));
2125 self.render(core::iter::once(mesh), |primitive| {
2126 push_draw(primitive, first_error);
2127 });
2128 }
2129 } else {
2130 mesh.lod_force.set(Some(far_lvl));
2131 mesh.draw_alpha.set(None);
2132 self.render(core::iter::once(mesh), |primitive| {
2133 push_draw(primitive, first_error);
2134 });
2135 let a = ((1.0 - t) * 255.0) as u8;
2136 if a > 16 {
2137 mesh.lod_force.set(Some(near_lvl));
2138 mesh.draw_alpha.set(Some(a));
2139 self.render(core::iter::once(mesh), |primitive| {
2140 push_draw(primitive, first_error);
2141 });
2142 }
2143 }
2144 mesh.lod_force.set(None);
2145 mesh.draw_alpha.set(None);
2146 }
2147 }
2148 }
2149 #[cfg(not(feature = "lod-crossfade"))]
2150 {
2151 self.render(core::iter::once(mesh), |primitive| {
2152 push_draw(primitive, first_error);
2153 });
2154 }
2155 Ok(())
2156 }
2157
2158 pub fn record_with_fallback<'a, MS, FS, const MAX: usize>(
2159 &self,
2160 primary: MS,
2161 fallback: FS,
2162 commands: &mut crate::command_buffer::CommandBuffer<MAX>,
2163 telemetry: Option<&mut crate::telemetry::RecordTelemetry>,
2164 ) -> Result<BudgetFallbackOutcome, crate::error::RenderError>
2165 where
2166 MS: IntoIterator<Item = &'a K3dMesh<'a>>,
2167 FS: IntoIterator<Item = &'a K3dMesh<'a>>,
2168 {
2169 use crate::error::RenderError;
2170
2171 let mut local_telemetry = crate::telemetry::RecordTelemetry::default();
2172 match self.record_impl(primary, commands, Some(&mut local_telemetry)) {
2173 Ok(()) => {
2174 if let Some(t) = telemetry {
2175 *t = local_telemetry;
2176 t.fallback_used = false;
2177 }
2178 Ok(BudgetFallbackOutcome {
2179 used_fallback: false,
2180 primary_budget_error: None,
2181 })
2182 }
2183 Err(RenderError::OutOfBudget(kind)) => {
2184 let mut fallback_telemetry = crate::telemetry::RecordTelemetry::default();
2185 self.record_impl(fallback, commands, Some(&mut fallback_telemetry))?;
2186 if let Some(t) = telemetry {
2187 *t = fallback_telemetry;
2188 t.fallback_used = true;
2189 }
2190 Ok(BudgetFallbackOutcome {
2191 used_fallback: true,
2192 primary_budget_error: Some(kind),
2193 })
2194 }
2195 Err(e) => Err(e),
2196 }
2197 }
2198
2199 fn downgraded_quality_tier(tier: crate::config::QualityTier) -> crate::config::QualityTier {
2200 use crate::config::QualityTier;
2201 match tier {
2202 QualityTier::Quality => QualityTier::Balanced,
2203 QualityTier::Balanced => QualityTier::Fastest,
2204 QualityTier::Fastest => QualityTier::Fastest,
2205 }
2206 }
2207
2208 pub fn record_with_degradation<'a, const MAX: usize>(
2209 &mut self,
2210 meshes: &[&'a K3dMesh<'a>],
2211 commands: &mut crate::command_buffer::CommandBuffer<MAX>,
2212 policy: crate::config::DegradationPolicy<'_>,
2213 telemetry: Option<&mut crate::telemetry::RecordTelemetry>,
2214 ) -> Result<DegradationOutcome, crate::error::RenderError> {
2215 use crate::config::DegradationStep;
2216 use crate::error::RenderError;
2217
2218 let original_quality = self.quality_tier;
2219 let mut active_quality = self.quality_tier;
2220
2221 let mut outcome = DegradationOutcome {
2222 used_degradation: false,
2223 steps_applied: 0,
2224 dropped_meshes: 0,
2225 final_quality_tier: active_quality,
2226 primary_budget_error: None,
2227 };
2228
2229 let mut local_telemetry = crate::telemetry::RecordTelemetry::default();
2230 match self.record_impl(meshes.iter().copied(), commands, Some(&mut local_telemetry)) {
2231 Ok(()) => {
2232 if let Some(t) = telemetry {
2233 *t = local_telemetry;
2234 }
2235 return Ok(outcome);
2236 }
2237 Err(RenderError::OutOfBudget(kind)) => {
2238 outcome.primary_budget_error = Some(kind);
2239 }
2240 Err(e) => return Err(e),
2241 }
2242
2243 for step in policy.steps {
2244 outcome.used_degradation = true;
2245 outcome.steps_applied += 1;
2246
2247 let mut selected: heapless::Vec<&K3dMesh<'_>, 512> = heapless::Vec::new();
2248 match *step {
2249 DegradationStep::RaisePriorityFloor(min_priority) => {
2250 for mesh in meshes {
2251 if mesh.priority >= min_priority {
2252 let _ = selected.push(*mesh);
2253 } else {
2254 outcome.dropped_meshes += 1;
2255 }
2256 }
2257 }
2258 DegradationStep::MeshDecimationStride(stride) => {
2259 if stride == 0 {
2260 self.quality_tier = original_quality;
2261 return Err(RenderError::InvalidInput(
2262 "mesh decimation stride must be >= 1",
2263 ));
2264 }
2265 for (idx, mesh) in meshes.iter().enumerate() {
2266 if idx % stride == 0 {
2267 let _ = selected.push(*mesh);
2268 } else {
2269 outcome.dropped_meshes += 1;
2270 }
2271 }
2272 }
2273 DegradationStep::DowngradeQuality => {
2274 active_quality = Self::downgraded_quality_tier(active_quality);
2275 self.quality_tier = active_quality;
2276 for mesh in meshes {
2277 let _ = selected.push(*mesh);
2278 }
2279 }
2280 }
2281
2282 if selected.is_empty() {
2283 continue;
2284 }
2285
2286 let mut step_telemetry = crate::telemetry::RecordTelemetry::default();
2287 let attempt = self.record_impl(
2288 selected.iter().copied(),
2289 commands,
2290 Some(&mut step_telemetry),
2291 );
2292
2293 if let Ok(()) = attempt {
2294 outcome.final_quality_tier = self.quality_tier;
2295 if let Some(t) = telemetry {
2296 *t = step_telemetry;
2297 t.fallback_used = true;
2298 t.degradation_steps_applied = outcome.steps_applied;
2299 t.dropped_meshes = outcome.dropped_meshes;
2300 }
2301 self.quality_tier = original_quality;
2302 return Ok(outcome);
2303 }
2304 }
2305
2306 self.quality_tier = original_quality;
2307 Err(crate::error::RenderError::Recoverable {
2308 fault: crate::error::RuntimeFaultKind::Budget(outcome.primary_budget_error.unwrap_or(
2309 crate::error::BudgetKind::DrawPrimitives {
2310 attempted: commands.len(),
2311 max: MAX,
2312 },
2313 )),
2314 action: crate::error::RecoveryAction::SkipFrame,
2315 })
2316 }
2317
2318 pub fn execute<D, const MAX: usize>(
2319 &self,
2320 fb: &mut D,
2321 frame: &mut crate::renderer::FrameCtx<'_>,
2322 commands: &crate::command_buffer::CommandBuffer<MAX>,
2323 telemetry: Option<&mut crate::telemetry::ExecuteTelemetry>,
2324 ) -> Result<Option<crate::renderer::DirtyRegion>, crate::error::RenderError>
2325 where
2326 D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>
2327 + embedded_graphics_core::prelude::OriginDimensions,
2328 <D as embedded_graphics_core::draw_target::DrawTarget>::Error: core::fmt::Debug,
2329 {
2330 if let Some(t) = telemetry {
2331 t.commands_total = commands.len();
2332 t.draw_commands = commands
2333 .iter()
2334 .filter(|cmd| matches!(cmd, crate::command_buffer::RenderCommand::Draw(_)))
2335 .count();
2336 t.clear_color_commands = commands
2337 .iter()
2338 .filter(|cmd| matches!(cmd, crate::command_buffer::RenderCommand::ClearColor(_)))
2339 .count();
2340 t.clear_depth_commands = commands
2341 .iter()
2342 .filter(|cmd| matches!(cmd, crate::command_buffer::RenderCommand::ClearDepth(_)))
2343 .count();
2344 }
2345 let camera_dir = self.camera.get_direction();
2346 crate::renderer::execute_commands_with_dirty_region_effects(
2347 fb,
2348 frame,
2349 commands,
2350 self.fog.as_ref(),
2351 self.dither.as_ref(),
2352 self.screen_tint,
2353 self.stipple_mode,
2354 self.palette_mode,
2355 self.sky,
2356 [camera_dir.x, camera_dir.y, camera_dir.z],
2357 )
2358 }
2359
2360 pub fn execute_with_textures<D, const MAX: usize, const N: usize>(
2371 &self,
2372 fb: &mut D,
2373 frame: &mut crate::renderer::FrameCtx<'_>,
2374 commands: &crate::command_buffer::CommandBuffer<MAX>,
2375 texture_manager: &crate::texture::TextureManager<N>,
2376 telemetry: Option<&mut crate::telemetry::ExecuteTelemetry>,
2377 ) -> Result<Option<crate::renderer::DirtyRegion>, crate::error::RenderError>
2378 where
2379 D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>
2380 + embedded_graphics_core::prelude::OriginDimensions,
2381 <D as embedded_graphics_core::draw_target::DrawTarget>::Error: core::fmt::Debug,
2382 {
2383 if let Some(t) = telemetry {
2384 t.commands_total = commands.len();
2385 t.draw_commands = commands
2386 .iter()
2387 .filter(|cmd| matches!(cmd, crate::command_buffer::RenderCommand::Draw(_)))
2388 .count();
2389 t.clear_color_commands = commands
2390 .iter()
2391 .filter(|cmd| matches!(cmd, crate::command_buffer::RenderCommand::ClearColor(_)))
2392 .count();
2393 t.clear_depth_commands = commands
2394 .iter()
2395 .filter(|cmd| matches!(cmd, crate::command_buffer::RenderCommand::ClearDepth(_)))
2396 .count();
2397 }
2398 let camera_dir = self.camera.get_direction();
2399 crate::renderer::execute_commands_with_dirty_region_effects_textured(
2400 fb,
2401 frame,
2402 commands,
2403 texture_manager,
2404 self.fog.as_ref(),
2405 self.dither.as_ref(),
2406 self.screen_tint,
2407 self.stipple_mode,
2408 self.palette_mode,
2409 self.sky,
2410 [camera_dir.x, camera_dir.y, camera_dir.z],
2411 )
2412 }
2413
2414 pub fn execute_tiled<D, const MAX: usize, const BIN_CAP: usize>(
2415 &self,
2416 fb: &mut D,
2417 frame: &mut crate::renderer::FrameCtx<'_>,
2418 commands: &crate::command_buffer::CommandBuffer<MAX>,
2419 tile: crate::tilebin::TileConfig,
2420 ) -> Result<crate::tilebin::TileBinStats, crate::error::RenderError>
2421 where
2422 D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>
2423 + embedded_graphics_core::prelude::OriginDimensions,
2424 <D as embedded_graphics_core::draw_target::DrawTarget>::Error: core::fmt::Debug,
2425 {
2426 let camera_dir = self.camera.get_direction();
2427 crate::renderer::execute_commands_tiled_effects::<D, MAX, BIN_CAP>(
2428 fb,
2429 frame,
2430 commands,
2431 tile,
2432 self.fog.as_ref(),
2433 self.dither.as_ref(),
2434 self.screen_tint,
2435 self.stipple_mode,
2436 self.palette_mode,
2437 self.sky,
2438 [camera_dir.x, camera_dir.y, camera_dir.z],
2439 )
2440 }
2441
2442 #[cfg(feature = "gizmos")]
2444 pub fn record_aabb_gizmo<const MAX: usize>(
2445 &self,
2446 aabb: &crate::bounds::Aabb,
2447 model_matrix: &Matrix4<f32>,
2448 color: Rgb565,
2449 commands: &mut crate::command_buffer::CommandBuffer<MAX>,
2450 ) -> Result<(), crate::error::RenderError> {
2451 let mut err = None;
2452 crate::gizmos::emit_aabb_wireframe_projected(
2453 aabb,
2454 model_matrix,
2455 |p| self.transform_point(&p, self.camera.vp_matrix),
2456 color,
2457 |prim| {
2458 if err.is_none()
2459 && let Err(e) = commands.push(crate::command_buffer::RenderCommand::Draw(prim))
2460 {
2461 err = Some(e);
2462 }
2463 },
2464 );
2465 match err {
2466 Some(e) => Err(e),
2467 None => Ok(()),
2468 }
2469 }
2470
2471 #[cfg(feature = "gizmos")]
2473 pub fn record_frustum_gizmo<const MAX: usize>(
2474 &self,
2475 color: Rgb565,
2476 commands: &mut crate::command_buffer::CommandBuffer<MAX>,
2477 ) -> Result<(), crate::error::RenderError> {
2478 let mut err = None;
2479 crate::gizmos::emit_frustum_wireframe(
2480 &self.camera,
2481 |p| self.transform_point(&p, self.camera.vp_matrix),
2482 color,
2483 |prim| {
2484 if err.is_none()
2485 && let Err(e) = commands.push(crate::command_buffer::RenderCommand::Draw(prim))
2486 {
2487 err = Some(e);
2488 }
2489 },
2490 );
2491 match err {
2492 Some(e) => Err(e),
2493 None => Ok(()),
2494 }
2495 }
2496}
2497
2498#[derive(Debug, Clone, Copy)]
2500pub struct MeshRayCastHit {
2501 pub distance: f32,
2503 pub point: Vector3<f32>,
2505 pub normal: Vector3<f32>,
2507 pub face_index: usize,
2509 pub uv: [f32; 2],
2511}
2512
2513#[cfg(feature = "lod-crossfade")]
2515fn apply_draw_alpha(prim: DrawPrimitive, alpha: u8) -> DrawPrimitive {
2516 match prim {
2517 DrawPrimitive::ColoredTriangleWithDepth {
2518 points,
2519 depths,
2520 color,
2521 } => DrawPrimitive::TranslucentTriangleWithDepth {
2522 points,
2523 depths,
2524 color,
2525 alpha,
2526 },
2527 DrawPrimitive::TranslucentTriangleWithDepth {
2528 points,
2529 depths,
2530 color,
2531 alpha: prev,
2532 } => DrawPrimitive::TranslucentTriangleWithDepth {
2533 points,
2534 depths,
2535 color,
2536 alpha: ((prev as u16 * alpha as u16) / 255) as u8,
2537 },
2538 other => other,
2539 }
2540}
2541
2542pub fn mesh_ray_cast(
2547 ray_origin: Vector3<f32>,
2548 ray_dir: Vector3<f32>,
2549 geometry: &mesh::Geometry<'_>,
2550 model_matrix: &Matrix4<f32>,
2551 max_distance: f32,
2552) -> Option<MeshRayCastHit> {
2553 #[cfg(feature = "aabb-cull")]
2554 {
2555 return mesh_ray_cast_bounded(
2556 ray_origin,
2557 ray_dir,
2558 geometry,
2559 model_matrix,
2560 max_distance,
2561 None,
2562 );
2563 }
2564 #[cfg(not(feature = "aabb-cull"))]
2565 {
2566 mesh_ray_cast_world(ray_origin, ray_dir, geometry, model_matrix, max_distance)
2567 }
2568}
2569
2570#[cfg(not(feature = "aabb-cull"))]
2571fn mesh_ray_cast_world(
2572 ray_origin: Vector3<f32>,
2573 ray_dir: Vector3<f32>,
2574 geometry: &mesh::Geometry<'_>,
2575 model_matrix: &Matrix4<f32>,
2576 max_distance: f32,
2577) -> Option<MeshRayCastHit> {
2578 let mut nearest: Option<MeshRayCastHit> = None;
2579 let mut min_dist = max_distance;
2580
2581 for (face_index, face) in geometry.faces.iter().enumerate() {
2582 let raw_v0 = geometry.vertices[face[0]];
2583 let raw_v1 = geometry.vertices[face[1]];
2584 let raw_v2 = geometry.vertices[face[2]];
2585
2586 let v0 = model_matrix
2587 .transform_point(&Point3::new(raw_v0[0], raw_v0[1], raw_v0[2]))
2588 .coords;
2589 let v1 = model_matrix
2590 .transform_point(&Point3::new(raw_v1[0], raw_v1[1], raw_v1[2]))
2591 .coords;
2592 let v2 = model_matrix
2593 .transform_point(&Point3::new(raw_v2[0], raw_v2[1], raw_v2[2]))
2594 .coords;
2595
2596 let edge1 = v1 - v0;
2597 let edge2 = v2 - v0;
2598 let h = ray_dir.cross(&edge2);
2599 let det = edge1.dot(&h);
2600 if det.abs() < 1e-6 {
2601 continue;
2602 }
2603 let inv_det = 1.0 / det;
2604 let s = ray_origin - v0;
2605 let bary_u = inv_det * s.dot(&h);
2606 if !(0.0..=1.0).contains(&bary_u) {
2607 continue;
2608 }
2609 let q = s.cross(&edge1);
2610 let bary_v = inv_det * ray_dir.dot(&q);
2611 if bary_v < 0.0 || bary_u + bary_v > 1.0 {
2612 continue;
2613 }
2614 let t = inv_det * edge2.dot(&q);
2615 if t <= 0.0 || t >= min_dist {
2616 continue;
2617 }
2618 let normal = edge1.cross(&edge2).normalize();
2619 let bary_w = 1.0 - bary_u - bary_v;
2620 let uv = if geometry.uvs.len() > face[0]
2621 && geometry.uvs.len() > face[1]
2622 && geometry.uvs.len() > face[2]
2623 {
2624 let uv0 = geometry.uvs[face[0]];
2625 let uv1 = geometry.uvs[face[1]];
2626 let uv2 = geometry.uvs[face[2]];
2627 [
2628 bary_w * uv0[0] + bary_u * uv1[0] + bary_v * uv2[0],
2629 bary_w * uv0[1] + bary_u * uv1[1] + bary_v * uv2[1],
2630 ]
2631 } else {
2632 [0.0, 0.0]
2633 };
2634 let point = ray_origin + ray_dir * t;
2635 min_dist = t;
2636 nearest = Some(MeshRayCastHit {
2637 distance: t,
2638 point,
2639 normal,
2640 face_index,
2641 uv,
2642 });
2643 }
2644 nearest
2645}
2646
2647#[cfg(feature = "aabb-cull")]
2650pub fn mesh_ray_cast_bounded(
2651 ray_origin: Vector3<f32>,
2652 ray_dir: Vector3<f32>,
2653 geometry: &mesh::Geometry<'_>,
2654 model_matrix: &Matrix4<f32>,
2655 max_distance: f32,
2656 model_aabb: Option<&Aabb>,
2657) -> Option<MeshRayCastHit> {
2658 let inv = model_matrix.try_inverse()?;
2659 let origin4 = inv * Vector4::new(ray_origin.x, ray_origin.y, ray_origin.z, 1.0);
2660 let dir4 = inv * Vector4::new(ray_dir.x, ray_dir.y, ray_dir.z, 0.0);
2661 if origin4.w.abs() < 1e-8 {
2662 return None;
2663 }
2664 let local_origin = Vector3::new(origin4.x, origin4.y, origin4.z) / origin4.w;
2665 let local_dir = Vector3::new(dir4.x, dir4.y, dir4.z);
2666 let dir_len = local_dir.norm();
2667 if dir_len < 1e-8 {
2668 return None;
2669 }
2670 let local_dir_n = local_dir / dir_len;
2671 let local_max = max_distance * dir_len;
2672
2673 if let Some(aabb) = model_aabb
2674 && aabb
2675 .intersect_ray(local_origin, local_dir_n, local_max)
2676 .is_none()
2677 {
2678 return None;
2679 }
2680
2681 let mut nearest: Option<MeshRayCastHit> = None;
2682 let mut min_dist = local_max;
2683
2684 for (face_index, face) in geometry.faces.iter().enumerate() {
2685 let v0 = Vector3::new(
2686 geometry.vertices[face[0]][0],
2687 geometry.vertices[face[0]][1],
2688 geometry.vertices[face[0]][2],
2689 );
2690 let v1 = Vector3::new(
2691 geometry.vertices[face[1]][0],
2692 geometry.vertices[face[1]][1],
2693 geometry.vertices[face[1]][2],
2694 );
2695 let v2 = Vector3::new(
2696 geometry.vertices[face[2]][0],
2697 geometry.vertices[face[2]][1],
2698 geometry.vertices[face[2]][2],
2699 );
2700
2701 let edge1 = v1 - v0;
2702 let edge2 = v2 - v0;
2703 let h = local_dir_n.cross(&edge2);
2704 let det = edge1.dot(&h);
2705 if det.abs() < 1e-6 {
2706 continue;
2707 }
2708 let inv_det = 1.0 / det;
2709 let s = local_origin - v0;
2710 let bary_u = inv_det * s.dot(&h);
2711 if !(0.0..=1.0).contains(&bary_u) {
2712 continue;
2713 }
2714 let q = s.cross(&edge1);
2715 let bary_v = inv_det * local_dir_n.dot(&q);
2716 if bary_v < 0.0 || bary_u + bary_v > 1.0 {
2717 continue;
2718 }
2719 let t_local = inv_det * edge2.dot(&q);
2720 if t_local <= 0.0 || t_local >= min_dist {
2721 continue;
2722 }
2723
2724 let normal_local = edge1.cross(&edge2).normalize();
2725 let rot = model_matrix.fixed_view::<3, 3>(0, 0);
2726 let normal = (rot * normal_local).normalize();
2727
2728 let bary_w = 1.0 - bary_u - bary_v;
2729 let uv = if geometry.uvs.len() > face[0]
2730 && geometry.uvs.len() > face[1]
2731 && geometry.uvs.len() > face[2]
2732 {
2733 let uv0 = geometry.uvs[face[0]];
2734 let uv1 = geometry.uvs[face[1]];
2735 let uv2 = geometry.uvs[face[2]];
2736 [
2737 bary_w * uv0[0] + bary_u * uv1[0] + bary_v * uv2[0],
2738 bary_w * uv0[1] + bary_u * uv1[1] + bary_v * uv2[1],
2739 ]
2740 } else {
2741 [0.0, 0.0]
2742 };
2743
2744 let local_hit = local_origin + local_dir_n * t_local;
2745 let world_hit = model_matrix
2746 .transform_point(&Point3::from(local_hit))
2747 .coords;
2748 let world_t = (world_hit - ray_origin).norm();
2749 if world_t >= max_distance {
2750 continue;
2751 }
2752
2753 min_dist = t_local;
2754 nearest = Some(MeshRayCastHit {
2755 distance: world_t,
2756 point: world_hit,
2757 normal,
2758 face_index,
2759 uv,
2760 });
2761 }
2762
2763 nearest
2764}
2765
2766#[cfg(feature = "aabb-cull")]
2768pub fn mesh_ray_cast_mesh(
2769 ray_origin: Vector3<f32>,
2770 ray_dir: Vector3<f32>,
2771 mesh: &K3dMesh<'_>,
2772 max_distance: f32,
2773) -> Option<MeshRayCastHit> {
2774 let distance = (mesh.get_position() - Point3::from(ray_origin)).norm();
2775 let geometry = mesh.select_lod(distance);
2776 let aabb = mesh.model_aabb();
2777 mesh_ray_cast_bounded(
2778 ray_origin,
2779 ray_dir,
2780 geometry,
2781 &mesh.model_matrix,
2782 max_distance,
2783 Some(&aabb),
2784 )
2785}
2786
2787#[cfg(feature = "dma2d")]
2788mod dma2d_stubs {
2789 #[cfg(feature = "depth-u16")]
2790 #[unsafe(no_mangle)]
2791 extern "Rust" fn dma2d_clear_zbuffer_u16(ptr: *mut u16, len: usize, value: u16) {
2792 let slice = unsafe { core::slice::from_raw_parts_mut(ptr, len) };
2793 slice.fill(value);
2794 }
2795
2796 #[cfg(not(feature = "depth-u16"))]
2797 #[unsafe(no_mangle)]
2798 extern "Rust" fn dma2d_clear_zbuffer_u32(ptr: *mut u32, len: usize, value: u32) {
2799 let slice = unsafe { core::slice::from_raw_parts_mut(ptr, len) };
2800 slice.fill(value);
2801 }
2802}
2803
2804#[cfg(test)]
2805mod tests {
2806 extern crate std;
2807
2808 #[cfg(feature = "depth-u16")]
2809 pub type ZDepth = u16;
2810 #[cfg(feature = "depth-u16")]
2811 pub const Z_MAX_VALUE: ZDepth = u16::MAX;
2812 #[cfg(feature = "depth-u16")]
2813 pub const DEPTH_EPSILON: ZDepth = 1;
2814
2815 #[cfg(not(feature = "depth-u16"))]
2816 pub type ZDepth = u32;
2817 #[cfg(not(feature = "depth-u16"))]
2818 pub const Z_MAX_VALUE: ZDepth = u32::MAX;
2819 #[cfg(not(feature = "depth-u16"))]
2820 pub const DEPTH_EPSILON: ZDepth = 128;
2821
2822 #[inline(always)]
2823 pub const fn to_zdepth(z: u32) -> ZDepth {
2824 #[cfg(feature = "depth-u16")]
2825 {
2826 (z >> 16) as u16
2827 }
2828 #[cfg(not(feature = "depth-u16"))]
2829 {
2830 z
2831 }
2832 }
2833
2834 use super::*;
2835
2836 #[test]
2837 fn test_engine_creation() {
2838 let engine = K3dengine::new(640, 480);
2839 assert_eq!(engine.width, 640);
2840 assert_eq!(engine.height, 480);
2841 assert!((engine.camera.get_aspect_ratio() - 640.0 / 480.0).abs() < 0.001);
2842 }
2843
2844 #[test]
2845 fn test_transform_point_basic() {
2846 let engine = K3dengine::new(640, 480);
2847 let transform_matrix = engine.camera.vp_matrix;
2849
2850 let point = [0.0, 0.0, -5.0];
2853 let result = engine.transform_point(&point, transform_matrix);
2854
2855 if let Some(transformed) = result {
2856 assert!(transformed.x >= 0 && transformed.x < 640);
2858 assert!(transformed.y >= 0 && transformed.y < 480);
2859 }
2860 }
2862
2863 #[test]
2864 fn test_transform_point_clamps_out_of_bounds() {
2865 let engine = K3dengine::new(640, 480);
2866 let model_matrix = nalgebra::Matrix4::identity();
2867
2868 let point = [100.0, 100.0, -5.0];
2870 let result = engine.transform_point(&point, model_matrix);
2871 assert!(result.is_none());
2873 }
2874
2875 #[test]
2876 fn test_transform_point_behind_camera() {
2877 let engine = K3dengine::new(640, 480);
2878 let transform_matrix = engine.camera.vp_matrix;
2879
2880 let point = [0.0, 0.0, 1.0];
2882 let _result = engine.transform_point(&point, transform_matrix);
2883 }
2887
2888 #[test]
2889 fn test_transform_point_near_plane_clipping() {
2890 let engine = K3dengine::new(640, 480);
2891 let transform_matrix = engine.camera.vp_matrix;
2897
2898 let point = [0.0, 0.0, -0.1];
2900 let result = engine.transform_point(&point, transform_matrix);
2901 assert!(result.is_none());
2902 }
2903
2904 #[test]
2905 fn test_transform_point_far_plane_clipping() {
2906 let engine = K3dengine::new(640, 480);
2907 let transform_matrix = engine.camera.vp_matrix;
2908
2909 let point = [0.0, 0.0, -1000.0];
2911 let result = engine.transform_point(&point, transform_matrix);
2912 assert!(result.is_none());
2913 }
2914
2915 #[test]
2916 fn test_transform_point_within_near_far_not_culled() {
2917 let engine = K3dengine::new(640, 480);
2923 let transform_matrix = engine.camera.vp_matrix;
2924
2925 let point = [0.0, 0.0, -0.8];
2926 let result = engine.transform_point(&point, transform_matrix);
2927 assert!(
2928 result.is_some(),
2929 "a point at distance 0.8 (within [near=0.4, far=20]) should not be culled"
2930 );
2931 }
2932
2933 #[test]
2934 fn test_transform_points_array() {
2935 let engine = K3dengine::new(640, 480);
2936 let transform_matrix = engine.camera.vp_matrix;
2937
2938 let vertices = [[0.0, 0.0, -5.0], [0.1, 0.0, -5.0], [0.0, 0.1, -5.0]];
2939 let indices = [0, 1, 2];
2940
2941 let result = engine.transform_points(&indices, &vertices, transform_matrix);
2942
2943 if let Some(points) = result {
2945 assert_eq!(points.len(), 3);
2946 }
2947 }
2949
2950 #[test]
2951 fn test_render_empty_faces_mesh() {
2952 let engine = K3dengine::new(640, 480);
2953 let vertices = [[0.0, 0.0, -5.0]]; let geometry = mesh::Geometry {
2955 vertices: &vertices,
2956 faces: &[],
2957 colors: &[],
2958 lines: &[],
2959 normals: &[],
2960 vertex_normals: &[],
2961 uvs: &[],
2962 texture_id: None,
2963 };
2964 let mesh = mesh::K3dMesh::new(geometry);
2965
2966 let mut callback_count = 0;
2967 engine.render(std::iter::once(&mesh), |_| {
2968 callback_count += 1;
2969 });
2970
2971 assert!(callback_count > 0);
2973 }
2974
2975 #[test]
2976 fn test_render_points_mode() {
2977 let engine = K3dengine::new(640, 480);
2978
2979 let vertices = [[0.0, 0.0, -5.0], [0.5, 0.0, -5.0]];
2980
2981 let geometry = mesh::Geometry {
2982 vertices: &vertices,
2983 faces: &[],
2984 colors: &[],
2985 lines: &[],
2986 normals: &[],
2987 vertex_normals: &[],
2988 uvs: &[],
2989 texture_id: None,
2990 };
2991
2992 let mut mesh = mesh::K3dMesh::new(geometry);
2993 mesh.set_render_mode(mesh::RenderMode::Points);
2994
2995 let mut primitives = std::vec::Vec::new();
2996 engine.render(std::iter::once(&mesh), |prim| {
2997 primitives.push(prim);
2998 });
2999
3000 assert!(primitives.len() > 0);
3002 for prim in primitives {
3003 assert!(matches!(prim, DrawPrimitive::ColoredPoint(_, _)));
3004 }
3005 }
3006
3007 #[test]
3008 fn test_render_lines_mode_with_faces() {
3009 let engine = K3dengine::new(640, 480);
3010
3011 let vertices = [[0.0, 0.0, -5.0], [0.5, 0.0, -5.0], [0.0, 0.5, -5.0]];
3012
3013 let faces = [[0, 1, 2]];
3014
3015 let geometry = mesh::Geometry {
3016 vertices: &vertices,
3017 faces: &faces,
3018 colors: &[],
3019 lines: &[],
3020 normals: &[],
3021 vertex_normals: &[],
3022 uvs: &[],
3023 texture_id: None,
3024 };
3025
3026 let mut mesh = mesh::K3dMesh::new(geometry);
3027 mesh.set_render_mode(mesh::RenderMode::Lines);
3028
3029 let mut primitives = std::vec::Vec::new();
3030 engine.render(std::iter::once(&mesh), |prim| {
3031 primitives.push(prim);
3032 });
3033
3034 assert_eq!(primitives.len(), 3);
3036 for prim in primitives {
3037 assert!(matches!(prim, DrawPrimitive::Line(_, _)));
3038 }
3039 }
3040
3041 #[test]
3042 fn test_render_gouraud_light_dir() {
3043 let mut engine = K3dengine::new(640, 480);
3044 engine.camera.set_position(Point3::new(0.0, 0.0, -10.0));
3045 engine.camera.set_target(Point3::new(0.0, 0.0, 0.0));
3046
3047 let vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
3048 let faces = [[0, 1, 2]];
3049 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]];
3051
3052 let geometry = mesh::Geometry {
3053 vertices: &vertices,
3054 faces: &faces,
3055 colors: &[],
3056 lines: &[],
3057 normals: &normals,
3058 vertex_normals: &vertex_normals,
3059 uvs: &[],
3060 texture_id: None,
3061 };
3062
3063 let mut mesh = mesh::K3dMesh::new(geometry);
3064 mesh.set_render_mode(mesh::RenderMode::GouraudLightDir(Vector3::new(
3065 0.0, 0.0, 1.0,
3066 )));
3067
3068 let mut primitives = std::vec::Vec::new();
3069 engine.render(std::iter::once(&mesh), |prim| {
3070 primitives.push(prim);
3071 });
3072
3073 assert!(!primitives.is_empty());
3075 for prim in &primitives {
3076 assert!(matches!(
3077 prim,
3078 DrawPrimitive::GouraudTriangleWithDepth { .. }
3079 ));
3080 }
3081 }
3082
3083 #[test]
3090 fn test_solid_inward_normals_interior_camera() {
3091 let mut engine = K3dengine::new(320, 240);
3092 engine
3094 .camera
3095 .set_position(nalgebra::Point3::new(0.0, 0.0, 0.0));
3096 engine
3097 .camera
3098 .set_target(nalgebra::Point3::new(0.0, 0.0, -1.0));
3099
3100 #[rustfmt::skip]
3103 let vertices: &[[f32; 3]] = &[
3104 [-1.0, -1.0, -2.0],
3105 [ 1.0, -1.0, -2.0],
3106 [ 1.0, 1.0, -2.0],
3107 [-1.0, 1.0, -2.0],
3108 ];
3109 let faces: &[[usize; 3]] = &[[0, 1, 2], [0, 2, 3]];
3110 let normals: &[[f32; 3]] = &[[0.0, 0.0, 1.0], [0.0, 0.0, 1.0]]; let geometry = mesh::Geometry {
3113 vertices,
3114 faces,
3115 normals,
3116 colors: &[],
3117 lines: &[],
3118 vertex_normals: &[],
3119 uvs: &[],
3120 texture_id: None,
3121 };
3122 let mut m = mesh::K3dMesh::new(geometry);
3123 m.set_render_mode(mesh::RenderMode::Solid);
3124
3125 let mut count = 0usize;
3126 engine.render(std::iter::once(&m), |_| count += 1);
3127
3128 assert!(
3129 count > 0,
3130 "interior Solid-with-inward-normals emitted 0 primitives — culling is wrong"
3131 );
3132 }
3133}