1use core::fmt::Debug;
2use embedded_graphics_core::draw_target::DrawTarget;
3use embedded_graphics_core::pixelcolor::{Rgb565, RgbColor};
4use embedded_graphics_core::prelude::Point;
5use nalgebra::{ComplexField, Matrix4, Point2, Point3, Vector3, Vector4};
6
7use crate::camera::Camera;
8use crate::command_buffer::{CommandBuffer, RenderCommand};
9use crate::config::{MaterialProfile, ProfileCaps, QualityTier};
10use crate::draw::{DitherConfig, FogConfig};
11use crate::error::{BudgetKind, RenderError};
12use crate::mesh::{self, K3dMesh, RenderMode};
13use crate::primitive::DrawPrimitive;
14use crate::renderer::{DirtyRegion, FrameCtx};
15use crate::retro::{LightLevels, PaletteMode, ScreenTint, SkyConfig, StippleMode, TextureMapping};
16use crate::tilebin::TileConfig;
17use crate::{ZDepth, clear_zbuffer, to_zdepth};
18
19pub struct K3dengine {
20 pub camera: Camera,
21 pub(crate) width: u16,
22 pub(crate) height: u16,
23 pub(crate) caps: Option<crate::config::ProfileCaps>,
24 pub(crate) quality_tier: crate::config::QualityTier,
25 pub(crate) material_profile: crate::config::MaterialProfile,
26 pub(crate) fog: Option<crate::draw::FogConfig>,
28 pub(crate) dither: Option<crate::draw::DitherConfig>,
30 pub(crate) vertex_snap_bits: u8,
32 pub(crate) texture_mapping: crate::retro::TextureMapping,
34 pub(crate) light_levels: crate::retro::LightLevels,
36 pub(crate) stipple_mode: crate::retro::StippleMode,
38 pub(crate) screen_tint: Option<crate::retro::ScreenTint>,
40 pub(crate) palette_mode: crate::retro::PaletteMode,
42 pub(crate) sky: Option<crate::retro::SkyConfig>,
44 #[cfg(feature = "lighting")]
47 pub(crate) point_lights: heapless::Vec<crate::lights::PointLight, 16>,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct BudgetFallbackOutcome {
52 pub used_fallback: bool,
53 pub primary_budget_error: Option<crate::error::BudgetKind>,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub struct DegradationOutcome {
58 pub used_degradation: bool,
59 pub steps_applied: usize,
60 pub dropped_meshes: usize,
61 pub final_quality_tier: crate::config::QualityTier,
62 pub primary_budget_error: Option<crate::error::BudgetKind>,
63}
64
65impl K3dengine {
66 pub fn new(width: u16, height: u16) -> K3dengine {
67 K3dengine {
68 camera: Camera::new(width as f32 / height as f32),
69 width,
70 height,
71 caps: None,
72 quality_tier: crate::config::QualityTier::Balanced,
73 material_profile: crate::config::MaterialProfile::Lambert,
74 fog: None,
75 dither: None,
76 vertex_snap_bits: 0,
77 texture_mapping: crate::retro::TextureMapping::PerspectiveCorrect,
78 light_levels: crate::retro::LightLevels::Linear,
79 stipple_mode: crate::retro::StippleMode::Off,
80 screen_tint: None,
81 palette_mode: crate::retro::PaletteMode::Off,
82 sky: None,
83 #[cfg(feature = "lighting")]
84 point_lights: heapless::Vec::new(),
85 }
86 }
87
88 pub fn set_fog(&mut self, fog: crate::draw::FogConfig) {
90 self.fog = Some(fog);
91 }
92
93 pub fn clear_fog(&mut self) {
95 self.fog = None;
96 }
97
98 pub fn set_dither(&mut self, dither: crate::draw::DitherConfig) {
100 self.dither = Some(dither);
101 }
102
103 pub fn clear_dither(&mut self) {
105 self.dither = None;
106 }
107
108 pub fn set_vertex_snap_bits(&mut self, bits: u8) {
110 self.vertex_snap_bits = bits.min(16);
111 }
112
113 pub fn set_texture_mapping(&mut self, mapping: crate::retro::TextureMapping) {
115 self.texture_mapping = mapping;
116 }
117
118 pub fn set_light_levels(&mut self, levels: crate::retro::LightLevels) {
120 self.light_levels = levels;
121 }
122
123 pub fn set_stipple_mode(&mut self, mode: crate::retro::StippleMode) {
125 self.stipple_mode = mode;
126 }
127
128 pub fn set_screen_tint(&mut self, tint: crate::retro::ScreenTint) {
130 self.screen_tint = Some(tint);
131 }
132
133 pub fn clear_screen_tint(&mut self) {
135 self.screen_tint = None;
136 }
137
138 pub fn set_palette_mode(&mut self, mode: crate::retro::PaletteMode) {
140 self.palette_mode = mode;
141 }
142
143 pub fn set_sky(&mut self, sky: crate::retro::SkyConfig) {
145 self.sky = Some(sky);
146 }
147
148 pub fn clear_sky(&mut self) {
150 self.sky = None;
151 }
152
153 pub fn apply_retro_style(&mut self, style: crate::retro::RetroStyle) {
155 self.fog = style.fog;
156 self.dither = style.dither;
157 self.set_vertex_snap_bits(style.vertex_snap_bits);
158 self.texture_mapping = style.texture_mapping;
159 self.light_levels = style.light_levels;
160 self.stipple_mode = style.stipple_mode;
161 self.screen_tint = style.screen_tint;
162 self.palette_mode = style.palette_mode;
163 self.sky = style.sky;
164 }
165
166 #[cfg(feature = "lighting")]
169 pub fn add_point_light(&mut self, light: crate::lights::PointLight) -> bool {
170 self.point_lights.push(light).is_ok()
171 }
172
173 #[cfg(feature = "lighting")]
175 pub fn clear_point_lights(&mut self) {
176 self.point_lights.clear();
177 }
178
179 #[cfg(feature = "lighting")]
182 #[inline]
183 pub(crate) fn light_tint_at(&self, world_pos: Point3<f32>) -> Rgb565 {
184 #[cfg(feature = "lighting")]
185 {
186 let mut acc_r = 0u16;
187 let mut acc_g = 0u16;
188 let mut acc_b = 0u16;
189 for light in &self.point_lights {
190 let tint = light.contribution_at(world_pos);
191 acc_r += tint.r() as u16;
192 acc_g += tint.g() as u16;
193 acc_b += tint.b() as u16;
194 }
195 Rgb565::new(
196 (acc_r.min(31)) as u8,
197 (acc_g.min(63)) as u8,
198 (acc_b.min(31)) as u8,
199 )
200 }
201 #[cfg(not(feature = "lighting"))]
202 {
203 let _ = world_pos;
204 Rgb565::new(0, 0, 0)
205 }
206 }
207
208 #[inline]
210 pub(crate) fn add_tint(base: Rgb565, tint: Rgb565) -> Rgb565 {
211 Rgb565::new(
212 (base.r() as u16 + tint.r() as u16).min(31) as u8,
213 (base.g() as u16 + tint.g() as u16).min(63) as u8,
214 (base.b() as u16 + tint.b() as u16).min(31) as u8,
215 )
216 }
217
218 #[cfg(feature = "lighting")]
220 const DOOM_LIGHT_TABLE: [u8; 32] = [
221 8, 12, 16, 20, 24, 28, 34, 40, 48, 56, 64, 72, 82, 92, 102, 112, 124, 136, 148, 160, 172,
222 184, 196, 206, 216, 224, 232, 238, 244, 248, 252, 255,
223 ];
224
225 #[cfg(feature = "lighting")]
226 #[inline]
227 pub(crate) fn sector_shaded_color(
228 &self,
229 base: Rgb565,
230 brightness: u8,
231 face_center: Point3<f32>,
232 ) -> Rgb565 {
233 let level_u8 = match self.light_levels {
234 crate::retro::LightLevels::Linear => brightness,
235 crate::retro::LightLevels::Doom32 => {
236 let base_level = (brightness as usize * 31) / 255;
237 let distance = (face_center - self.camera.position).norm();
238 let distance_drop = (distance * 2.0) as usize;
240 let idx = base_level.saturating_sub(distance_drop).min(31);
241 Self::DOOM_LIGHT_TABLE[idx]
242 }
243 };
244
245 let factor = level_u8 as f32 / 255.0;
246 Rgb565::new(
247 (base.r() as f32 * factor) as u8,
248 (base.g() as f32 * factor) as u8,
249 (base.b() as f32 * factor) as u8,
250 )
251 }
252
253 #[cfg(feature = "lighting")]
255 #[inline]
256 pub(crate) fn face_world_center(
257 face: &[usize; 3],
258 vertices: &[[f32; 3]],
259 model_matrix: Matrix4<f32>,
260 ) -> Point3<f32> {
261 let v0 = vertices[face[0]];
262 let v1 = vertices[face[1]];
263 let v2 = vertices[face[2]];
264 let cx = (v0[0] + v1[0] + v2[0]) / 3.0;
265 let cy = (v0[1] + v1[1] + v2[1]) / 3.0;
266 let cz = (v0[2] + v1[2] + v2[2]) / 3.0;
267 model_matrix.transform_point(&Point3::new(cx, cy, cz))
268 }
269
270 pub fn set_caps(&mut self, caps: crate::config::ProfileCaps) {
271 self.caps = Some(caps);
272 self.apply_render_defaults(crate::config::render_defaults_for_profile(caps));
273 }
274
275 pub fn clear_caps(&mut self) {
276 self.caps = None;
277 }
278
279 pub fn set_quality_tier(&mut self, tier: crate::config::QualityTier) {
280 self.quality_tier = tier;
281 }
282
283 pub fn set_material_profile(&mut self, profile: crate::config::MaterialProfile) {
284 self.material_profile = profile;
285 }
286
287 pub fn apply_render_defaults(&mut self, defaults: crate::config::RenderDefaults) {
288 self.quality_tier = defaults.quality_tier;
289 self.material_profile = defaults.material_profile;
290 }
291
292 pub(crate) fn resolve_render_mode(&self, mode: &RenderMode) -> RenderMode {
293 #[cfg(not(feature = "lighting"))]
294 {
295 let _ = self;
296 mode.clone()
297 }
298 #[cfg(feature = "lighting")]
299 {
300 use crate::config::{MaterialProfile, QualityTier};
301 match self.quality_tier {
302 QualityTier::Fastest => match mode {
303 #[cfg(feature = "lighting")]
304 RenderMode::BlinnPhong { .. }
305 | RenderMode::GouraudLightDir(_)
306 | RenderMode::Toon(_, _)
307 | RenderMode::SolidLightDir(_) => RenderMode::Solid,
308 _ => mode.clone(),
309 },
310 QualityTier::Balanced => match (self.material_profile, mode) {
311 (MaterialProfile::Unlit, RenderMode::BlinnPhong { .. })
312 | (MaterialProfile::Unlit, RenderMode::GouraudLightDir(_))
313 | (MaterialProfile::Unlit, RenderMode::Toon(_, _))
314 | (MaterialProfile::Unlit, RenderMode::SolidLightDir(_)) => RenderMode::Solid,
315 (MaterialProfile::Lambert, RenderMode::BlinnPhong { light_dir, .. }) => {
316 RenderMode::SolidLightDir(*light_dir)
317 }
318 _ => mode.clone(),
319 },
320 QualityTier::Quality => match (self.material_profile, mode) {
321 (MaterialProfile::Unlit, RenderMode::BlinnPhong { .. })
322 | (MaterialProfile::Unlit, RenderMode::GouraudLightDir(_))
323 | (MaterialProfile::Unlit, RenderMode::Toon(_, _))
324 | (MaterialProfile::Unlit, RenderMode::SolidLightDir(_)) => RenderMode::Solid,
325 (MaterialProfile::Lambert, RenderMode::BlinnPhong { light_dir, .. }) => {
326 RenderMode::SolidLightDir(*light_dir)
327 }
328 _ => mode.clone(),
329 },
330 }
331 }
332 }
333
334 #[inline]
336 pub(crate) fn should_cull_mesh(&self, mesh: &K3dMesh) -> bool {
337 #[cfg(feature = "render-layers")]
338 if !self.camera.layers.intersects(mesh.layers) {
339 return true;
340 }
341
342 #[cfg(feature = "aabb-cull")]
343 {
344 let aabb = mesh.model_aabb();
345 let world_center = mesh
346 .model_matrix
347 .transform_point(&nalgebra::Point3::from(aabb.center));
348 let scale = mesh.similarity.scaling();
349 let radius = aabb.radius() * scale;
350 let m = self.camera.vp_matrix;
351 let planes = Self::frustum_planes_from_vp(&m);
352
353 for plane in &planes {
354 let (a, b, c, d) = (plane[0], plane[1], plane[2], plane[3]);
355 let len = (a * a + b * b + c * c).sqrt();
356 if len <= 0.0 {
357 continue;
358 }
359 let dist = (a * world_center.x + b * world_center.y + c * world_center.z + d) / len;
360 if dist < -radius {
361 return true;
362 }
363 }
364 for plane in &planes {
365 let (a, b, c, d) = (plane[0], plane[1], plane[2], plane[3]);
366 let len = (a * a + b * b + c * c).sqrt();
367 if len <= 0.0 {
368 continue;
369 }
370 if aabb.plane_signed_overshoot(a, b, c, d, len, &mesh.model_matrix) < 0.0 {
371 return true;
372 }
373 }
374 return false;
375 }
376
377 #[cfg(not(feature = "aabb-cull"))]
378 {
379 let mesh_pos = mesh.get_position();
380 let radius_sq = mesh.compute_bounding_radius_sq();
381 let radius = radius_sq.sqrt();
382 let planes = Self::frustum_planes_from_vp(&self.camera.vp_matrix);
383 for plane in &planes {
384 let (a, b, c, d) = (plane[0], plane[1], plane[2], plane[3]);
385 let len = (a * a + b * b + c * c).sqrt();
386 if len > 0.0 {
387 let dist = (a * mesh_pos.x + b * mesh_pos.y + c * mesh_pos.z + d) / len;
388 if dist < -radius {
389 return true;
390 }
391 }
392 }
393 false
394 }
395 }
396
397 #[inline]
398 fn frustum_planes_from_vp(m: &Matrix4<f32>) -> [[f32; 4]; 6] {
399 [
400 [
401 m[(3, 0)] + m[(0, 0)],
402 m[(3, 1)] + m[(0, 1)],
403 m[(3, 2)] + m[(0, 2)],
404 m[(3, 3)] + m[(0, 3)],
405 ],
406 [
407 m[(3, 0)] - m[(0, 0)],
408 m[(3, 1)] - m[(0, 1)],
409 m[(3, 2)] - m[(0, 2)],
410 m[(3, 3)] - m[(0, 3)],
411 ],
412 [
413 m[(3, 0)] + m[(1, 0)],
414 m[(3, 1)] + m[(1, 1)],
415 m[(3, 2)] + m[(1, 2)],
416 m[(3, 3)] + m[(1, 3)],
417 ],
418 [
419 m[(3, 0)] - m[(1, 0)],
420 m[(3, 1)] - m[(1, 1)],
421 m[(3, 2)] - m[(1, 2)],
422 m[(3, 3)] - m[(1, 3)],
423 ],
424 [
425 m[(3, 0)] + m[(2, 0)],
426 m[(3, 1)] + m[(2, 1)],
427 m[(3, 2)] + m[(2, 2)],
428 m[(3, 3)] + m[(2, 3)],
429 ],
430 [
431 m[(3, 0)] - m[(2, 0)],
432 m[(3, 1)] - m[(2, 1)],
433 m[(3, 2)] - m[(2, 2)],
434 m[(3, 3)] - m[(2, 3)],
435 ],
436 ]
437 }
438
439 #[inline(always)]
440 fn transform_point(&self, point: &[f32; 3], model_matrix: Matrix4<f32>) -> Option<Point3<i32>> {
441 #[cfg(feature = "fixed-transform")]
442 {
443 return self.transform_point_fixed(point, model_matrix);
444 }
445 #[cfg(not(feature = "fixed-transform"))]
446 {
447 let point = nalgebra::Vector4::new(point[0], point[1], point[2], 1.0);
448 let point = model_matrix * point;
449
450 if point.w < 0.0 {
451 return None;
452 }
453 if point.w < self.camera.near || point.w > self.camera.far {
462 return None;
463 }
464
465 let point = Point3::from_homogeneous(point)?;
466
467 let x = ((1.0 + point.x) * 0.5 * self.width as f32) as i32;
468 let y = ((1.0 - point.y) * 0.5 * self.height as f32) as i32;
469
470 if x < 0 || x >= self.width as i32 || y < 0 || y >= self.height as i32 {
471 return None;
472 }
473
474 Some(Point3::new(
475 x,
476 y,
477 (point.z * (self.camera.far - self.camera.near) + self.camera.near) as i32,
478 ))
479 }
480 }
481
482 #[cfg(feature = "fixed-transform")]
483 #[inline(always)]
484 fn transform_point_fixed(
485 &self,
486 point: &[f32; 3],
487 model_matrix: Matrix4<f32>,
488 ) -> Option<Point3<i32>> {
489 use embedded_dsp::fixed_point::{Q16, from_q16, to_q16};
490
491 #[inline(always)]
495 fn div_checked(a: Q16, b: Q16) -> Option<Q16> {
496 if b == 0 {
497 None
498 } else {
499 Some(embedded_dsp::fixed_point::div_q16(a, b))
500 }
501 }
502
503 let point = nalgebra::Vector4::new(point[0], point[1], point[2], 1.0);
504 let point = model_matrix * point;
505
506 if point.w <= 0.0 {
507 return None;
508 }
509 if point.w < self.camera.near || point.w > self.camera.far {
514 return None;
515 }
516
517 let x_fp = div_checked(to_q16(point.x), to_q16(point.w))?;
518 let y_fp = div_checked(to_q16(point.y), to_q16(point.w))?;
519 let z_ndc = from_q16(div_checked(to_q16(point.z), to_q16(point.w))?);
520
521 let x = ((1.0 + from_q16(x_fp)) * 0.5 * self.width as f32) as i32;
522 let y = ((1.0 - from_q16(y_fp)) * 0.5 * self.height as f32) as i32;
523
524 if x < 0 || x >= self.width as i32 || y < 0 || y >= self.height as i32 {
525 return None;
526 }
527
528 Some(Point3::new(
529 x,
530 y,
531 (z_ndc * (self.camera.far - self.camera.near) + self.camera.near) as i32,
532 ))
533 }
534
535 #[inline(always)]
536 pub fn transform_points<const N: usize>(
537 &self,
538 indices: &[usize; N],
539 vertices: &[[f32; 3]],
540 model_matrix: Matrix4<f32>,
541 ) -> Option<[Point3<i32>; N]> {
542 let mut ret = [Point3::new(0, 0, 0); N];
543
544 for i in 0..N {
545 ret[i] = self.transform_point(&vertices[indices[i]], model_matrix)?;
546 }
547
548 Some(ret)
549 }
550
551 fn transform_point_with_w(
554 &self,
555 point: &[f32; 3],
556 model_matrix: Matrix4<f32>,
557 ) -> Option<(Point3<i32>, f32)> {
558 let v = nalgebra::Vector4::new(point[0], point[1], point[2], 1.0);
559 let clip = model_matrix * v;
560 if clip.w < self.camera.near || clip.w > self.camera.far {
565 return None;
566 }
567 let ndc_x = clip.x / clip.w;
568 let ndc_y = clip.y / clip.w;
569 let ndc_z = clip.z / clip.w;
570 let x = ((1.0 + ndc_x) * 0.5 * self.width as f32) as i32;
571 let y = ((1.0 - ndc_y) * 0.5 * self.height as f32) as i32;
572 if x < 0 || x >= self.width as i32 || y < 0 || y >= self.height as i32 {
573 return None;
574 }
575 let z = (ndc_z * (self.camera.far - self.camera.near) + self.camera.near) as i32;
576 Some((Point3::new(x, y, z), clip.w))
577 }
578
579 #[inline(always)]
581 pub fn transform_points_with_w<const N: usize>(
582 &self,
583 indices: &[usize; N],
584 vertices: &[[f32; 3]],
585 model_matrix: Matrix4<f32>,
586 ) -> Option<([Point3<i32>; N], [f32; N])> {
587 let mut pts = [Point3::new(0, 0, 0); N];
588 let mut ws = [1.0f32; N];
589 for i in 0..N {
590 let (p, w) = self.transform_point_with_w(&vertices[indices[i]], model_matrix)?;
591 pts[i] = p;
592 ws[i] = w;
593 }
594 Some((pts, ws))
595 }
596
597 #[inline]
608 pub(crate) fn is_backface(
609 &self,
610 face: &[usize; 3],
611 vertices: &[[f32; 3]],
612 model_matrix: Matrix4<f32>,
613 world_normal: &Vector3<f32>,
614 ) -> bool {
615 let v0 = vertices[face[0]];
616 let v0_world = if model_matrix == Matrix4::identity() {
617 Point3::new(v0[0], v0[1], v0[2])
618 } else {
619 model_matrix.transform_point(&Point3::new(v0[0], v0[1], v0[2]))
620 };
621 (self.camera.position - v0_world).dot(world_normal) < 0.0
622 }
623
624 #[inline]
642 fn clip_to_screen(&self, c: Vector4<f32>) -> Option<Point3<i32>> {
643 if c.w <= 0.0 {
644 return None;
645 }
646 let mut ndc = Point3::from_homogeneous(c)?;
647 if self.vertex_snap_bits > 0 {
648 let scale = (1u32 << self.vertex_snap_bits) as f32;
649 ndc.x = (ndc.x * scale).round() / scale;
650 ndc.y = (ndc.y * scale).round() / scale;
651 }
652 let w = self.width as f32;
653 let h = self.height as f32;
654 let x = ((1.0 + ndc.x) * 0.5 * w).clamp(-w * 8.0, w * 9.0) as i32;
655 let y = ((1.0 - ndc.y) * 0.5 * h).clamp(-h * 8.0, h * 9.0) as i32;
656 let depth = (ndc.z * (self.camera.far - self.camera.near) + self.camera.near) as i32;
657 Some(Point3::new(x, y, depth))
658 }
659
660 #[inline]
663 fn project_and_emit<F>(
664 &self,
665 c0: Vector4<f32>,
666 c1: Vector4<f32>,
667 c2: Vector4<f32>,
668 color: Rgb565,
669 callback: &mut F,
670 ) where
671 F: FnMut(DrawPrimitive),
672 {
673 if let (Some(p0), Some(p1), Some(p2)) = (
674 self.clip_to_screen(c0),
675 self.clip_to_screen(c1),
676 self.clip_to_screen(c2),
677 ) {
678 callback(DrawPrimitive::ColoredTriangleWithDepth {
679 points: [p0.xy(), p1.xy(), p2.xy()],
680 depths: [p0.z as f32, p1.z as f32, p2.z as f32],
681 color,
682 });
683 }
684 }
685
686 fn clip_polygon_plane(
691 input: &[Vector4<f32>],
692 output: &mut [Vector4<f32>; 8],
693 dist: impl Fn(Vector4<f32>) -> f32,
694 ) -> usize {
695 let n = input.len();
696 let mut m = 0usize;
697 for i in 0..n {
698 let prev = input[(n + i - 1) % n];
699 let curr = input[i];
700 let d_prev = dist(prev);
701 let d_curr = dist(curr);
702 if d_curr >= 0.0 {
703 if d_prev < 0.0 {
704 let t = d_prev / (d_prev - d_curr);
706 if m < 8 {
707 output[m] = prev + (curr - prev) * t;
708 m += 1;
709 }
710 }
711 if m < 8 {
712 output[m] = curr;
713 m += 1;
714 }
715 } else if d_prev >= 0.0 {
716 let t = d_prev / (d_prev - d_curr);
718 if m < 8 {
719 output[m] = prev + (curr - prev) * t;
720 m += 1;
721 }
722 }
723 }
724 m
725 }
726
727 pub(crate) fn emit_clipped<F>(&self, clip: [Vector4<f32>; 3], color: Rgb565, callback: &mut F)
736 where
737 F: FnMut(DrawPrimitive),
738 {
739 let nw = self.camera.near;
740
741 let mut a = [Vector4::zeros(); 8];
742 let mut b = [Vector4::zeros(); 8];
743 a[0] = clip[0];
744 a[1] = clip[1];
745 a[2] = clip[2];
746
747 let n = Self::clip_polygon_plane(&a[..3], &mut b, |v| v.w - nw);
749 if n < 3 {
750 return;
751 }
752 let n = Self::clip_polygon_plane(&b[..n], &mut a, |v| v.x + v.w);
754 if n < 3 {
755 return;
756 }
757 let n = Self::clip_polygon_plane(&a[..n], &mut b, |v| v.w - v.x);
759 if n < 3 {
760 return;
761 }
762 let n = Self::clip_polygon_plane(&b[..n], &mut a, |v| v.y + v.w);
764 if n < 3 {
765 return;
766 }
767 let n = Self::clip_polygon_plane(&a[..n], &mut b, |v| v.w - v.y);
769 if n < 3 {
770 return;
771 }
772
773 for i in 1..n - 1 {
775 self.project_and_emit(b[0], b[i], b[i + 1], color, callback);
776 }
777 }
778
779 fn render<'a, MS, F>(&self, meshes: MS, mut callback: F)
780 where
781 MS: IntoIterator<Item = &'a K3dMesh<'a>>,
782 F: FnMut(DrawPrimitive),
783 {
784 for mesh in meshes {
785 if mesh.geometry.vertices.is_empty() {
786 continue;
787 }
788
789 if self.should_cull_mesh(mesh) {
793 continue;
794 }
795
796 let mesh_pos = mesh.get_position();
798 let distance = (mesh_pos - self.camera.position).norm();
799 let geometry = mesh.select_lod(distance);
800 #[cfg(feature = "lod-crossfade")]
801 let alpha_override = mesh.draw_alpha.get();
802 #[cfg(feature = "lod-crossfade")]
803 let mut emit = |prim: DrawPrimitive| {
804 let prim = match alpha_override {
805 Some(a) => apply_draw_alpha(prim, a),
806 None => prim,
807 };
808 callback(prim);
809 };
810 #[cfg(not(feature = "lod-crossfade"))]
811 let mut emit = |prim: DrawPrimitive| {
812 callback(prim);
813 };
814
815 let transform_matrix = self.camera.vp_matrix * mesh.model_matrix;
816
817 #[cfg(feature = "textured")]
818 let is_textured = matches!(
819 self.resolve_render_mode(&mesh.render_mode),
820 RenderMode::Textured | RenderMode::TexturedGouraud(_) | RenderMode::MatCap
821 );
822 #[cfg(not(feature = "textured"))]
823 let is_textured = false;
824
825 let mut v_cache_plain: [Option<Point3<i32>>; 256] = [None; 256];
826 #[cfg(feature = "textured")]
827 let mut v_cache_w: [Option<(Point3<i32>, f32)>; 256] = [None; 256];
828
829 let cache_limit = geometry.vertices.len().min(256);
830 if is_textured {
831 #[cfg(feature = "textured")]
832 for i in 0..cache_limit {
833 v_cache_w[i] =
834 self.transform_point_with_w(&geometry.vertices[i], transform_matrix);
835 }
836 } else {
837 for i in 0..cache_limit {
838 v_cache_plain[i] =
839 self.transform_point(&geometry.vertices[i], transform_matrix);
840 }
841 }
842
843 let mut get_pt = |idx: usize| -> Option<Point3<i32>> {
844 if idx < 256 {
845 v_cache_plain[idx]
846 } else {
847 self.transform_point(&geometry.vertices[idx], transform_matrix)
848 }
849 };
850
851 #[cfg(feature = "textured")]
852 let get_pt_w = |idx: usize| -> Option<(Point3<i32>, f32)> {
853 if idx < 256 {
854 v_cache_w[idx]
855 } else {
856 self.transform_point_with_w(&geometry.vertices[idx], transform_matrix)
857 }
858 };
859
860 let tf_face = |face: &[usize; 3]| -> Option<[Point3<i32>; 3]> {
861 Some([get_pt(face[0])?, get_pt(face[1])?, get_pt(face[2])?])
862 };
863
864 #[cfg(feature = "textured")]
865 let tf_face_w = |face: &[usize; 3]| -> Option<([Point3<i32>; 3], [f32; 3])> {
866 let (p0, w0) = get_pt_w(face[0])?;
867 let (p1, w1) = get_pt_w(face[1])?;
868 let (p2, w2) = get_pt_w(face[2])?;
869 Some(([p0, p1, p2], [w0, w1, w2]))
870 };
871
872 if let Some(out_color) = mesh.outline_color {
873 if mesh.outline_width > 0.0 {
874 let has_vertex_normals = !geometry.vertex_normals.is_empty();
875 let has_face_normals = !geometry.normals.is_empty();
876
877 for (face_idx, face) in geometry.faces.iter().enumerate() {
878 let face_normal = if has_face_normals {
879 Vector3::new(
880 geometry.normals[face_idx][0],
881 geometry.normals[face_idx][1],
882 geometry.normals[face_idx][2],
883 )
884 } else {
885 let v0 = Vector3::new(
886 geometry.vertices[face[0]][0],
887 geometry.vertices[face[0]][1],
888 geometry.vertices[face[0]][2],
889 );
890 let v1 = Vector3::new(
891 geometry.vertices[face[1]][0],
892 geometry.vertices[face[1]][1],
893 geometry.vertices[face[1]][2],
894 );
895 let v2 = Vector3::new(
896 geometry.vertices[face[2]][0],
897 geometry.vertices[face[2]][1],
898 geometry.vertices[face[2]][2],
899 );
900 (v1 - v0).cross(&(v2 - v0)).normalize()
901 };
902
903 let transformed_normal = mesh.model_matrix.transform_vector(&face_normal);
904 if !self.is_backface(
905 face,
906 geometry.vertices,
907 mesh.model_matrix,
908 &transformed_normal,
909 ) {
910 continue;
911 }
912
913 let mut pts = [Point3::origin(); 3];
914 let mut valid = true;
915 for i in 0..3 {
916 let vn = if has_vertex_normals {
917 Vector3::new(
918 geometry.vertex_normals[face[i]][0],
919 geometry.vertex_normals[face[i]][1],
920 geometry.vertex_normals[face[i]][2],
921 )
922 } else {
923 face_normal
924 };
925 let vpos = geometry.vertices[face[i]];
926 let ext_v = [
927 vpos[0] + vn.x * mesh.outline_width,
928 vpos[1] + vn.y * mesh.outline_width,
929 vpos[2] + vn.z * mesh.outline_width,
930 ];
931 if let Some(pt) = self.transform_point(&ext_v, transform_matrix) {
932 pts[i] = pt;
933 } else {
934 valid = false;
935 break;
936 }
937 }
938
939 if valid {
940 emit(DrawPrimitive::ColoredTriangleWithDepth {
941 points: [pts[0].xy(), pts[1].xy(), pts[2].xy()],
942 depths: [pts[0].z as f32, pts[1].z as f32, pts[2].z as f32],
943 color: out_color,
944 });
945 }
946 }
947 }
948 }
949
950 let render_mode = self.resolve_render_mode(&mesh.render_mode);
951 match render_mode {
952 RenderMode::Points => {
953 let screen_space_points = (0..geometry.vertices.len()).filter_map(&mut get_pt);
954
955 if geometry.colors.len() == geometry.vertices.len() {
956 for (point, color) in screen_space_points.zip(geometry.colors) {
957 emit(DrawPrimitive::ColoredPoint(point.xy(), *color));
958 }
959 } else {
960 for point in screen_space_points {
961 emit(DrawPrimitive::ColoredPoint(point.xy(), mesh.color));
962 }
963 }
964 }
965
966 RenderMode::Lines if !geometry.lines.is_empty() => {
967 for line in geometry.lines {
968 if let (Some(p1), Some(p2)) = (get_pt(line[0]), get_pt(line[1])) {
969 emit(DrawPrimitive::Line([p1.xy(), p2.xy()], mesh.color));
970 }
971 }
972 }
973
974 RenderMode::Lines if !geometry.faces.is_empty() => {
975 for face in geometry.faces {
976 if let Some([p1, p2, p3]) = tf_face(face) {
977 emit(DrawPrimitive::Line([p1.xy(), p2.xy()], mesh.color));
978 emit(DrawPrimitive::Line([p2.xy(), p3.xy()], mesh.color));
979 emit(DrawPrimitive::Line([p3.xy(), p1.xy()], mesh.color));
980 }
981 }
982 }
983
984 RenderMode::Lines => {}
985
986 #[cfg(feature = "lighting")]
987 RenderMode::SolidLightDir(direction) => {
988 let color_as_float = Vector3::new(
989 mesh.color.r() as f32 / 32.0,
990 mesh.color.g() as f32 / 64.0,
991 mesh.color.b() as f32 / 32.0,
992 );
993 let ambient_color = color_as_float * 0.1;
994 let adjusted_dir = Vector3::new(direction.x, direction.y, -direction.z);
995
996 for (face, normal) in geometry.faces.iter().zip(geometry.normals.iter()) {
997 let normal = Vector3::new(normal[0], normal[1], normal[2]);
998 let transformed_normal = mesh.model_matrix.transform_vector(&normal);
999 if self.is_backface(
1000 face,
1001 geometry.vertices,
1002 mesh.model_matrix,
1003 &transformed_normal,
1004 ) {
1005 continue;
1006 }
1007
1008 if let Some([p1, p2, p3]) = tf_face(face) {
1009 let intensity = transformed_normal.dot(&adjusted_dir).max(0.0);
1010 let final_color = color_as_float * intensity + ambient_color;
1011 let final_color = Vector3::new(
1012 final_color.x.clamp(0.0, 1.0),
1013 final_color.y.clamp(0.0, 1.0),
1014 final_color.z.clamp(0.0, 1.0),
1015 );
1016 let mut color = Rgb565::new(
1017 (final_color.x * 31.0) as u8,
1018 (final_color.y * 63.0) as u8,
1019 (final_color.z * 31.0) as u8,
1020 );
1021 if !self.point_lights.is_empty() {
1022 let wc = Self::face_world_center(
1023 face,
1024 geometry.vertices,
1025 mesh.model_matrix,
1026 );
1027 color = Self::add_tint(color, self.light_tint_at(wc));
1028 }
1029 emit(DrawPrimitive::ColoredTriangleWithDepth {
1030 points: [p1.xy(), p2.xy(), p3.xy()],
1031 depths: [p1.z as f32, p2.z as f32, p3.z as f32],
1032 color,
1033 });
1034 }
1035 }
1036 }
1037
1038 #[cfg(feature = "lighting")]
1039 RenderMode::GouraudLightDir(direction) => {
1040 let color_as_float = Vector3::new(
1041 mesh.color.r() as f32 / 32.0,
1042 mesh.color.g() as f32 / 64.0,
1043 mesh.color.b() as f32 / 32.0,
1044 );
1045 let ambient_color = color_as_float * 0.1;
1046 let adjusted_dir = Vector3::new(direction.x, direction.y, -direction.z);
1047
1048 for (face, face_normal) in geometry.faces.iter().zip(geometry.normals.iter()) {
1049 let fn_vec = Vector3::new(face_normal[0], face_normal[1], face_normal[2]);
1050 let transformed_fn = mesh.model_matrix.transform_vector(&fn_vec);
1051
1052 if self.is_backface(
1053 face,
1054 geometry.vertices,
1055 mesh.model_matrix,
1056 &transformed_fn,
1057 ) {
1058 continue;
1059 }
1060
1061 if let Some([p1, p2, p3]) = tf_face(face) {
1062 let vertex_colors: [Rgb565; 3] = core::array::from_fn(|k| {
1063 let vn = if !geometry.vertex_normals.is_empty() {
1064 let vn_arr = geometry.vertex_normals[face[k]];
1065 let vn_vec = Vector3::new(vn_arr[0], vn_arr[1], vn_arr[2]);
1066 mesh.model_matrix.transform_vector(&vn_vec)
1067 } else {
1068 transformed_fn
1069 };
1070
1071 let intensity = vn.dot(&adjusted_dir).max(0.0);
1072 let c = color_as_float * intensity + ambient_color;
1073 let mut vc = Rgb565::new(
1074 (c.x.clamp(0.0, 1.0) * 31.0) as u8,
1075 (c.y.clamp(0.0, 1.0) * 63.0) as u8,
1076 (c.z.clamp(0.0, 1.0) * 31.0) as u8,
1077 );
1078 if !self.point_lights.is_empty() {
1079 let vpos = geometry.vertices[face[k]];
1080 let wp = mesh
1081 .model_matrix
1082 .transform_point(&Point3::new(vpos[0], vpos[1], vpos[2]));
1083 vc = Self::add_tint(vc, self.light_tint_at(wp));
1084 }
1085 vc
1086 });
1087
1088 emit(DrawPrimitive::GouraudTriangleWithDepth {
1089 points: [p1.xy(), p2.xy(), p3.xy()],
1090 depths: [p1.z as f32, p2.z as f32, p3.z as f32],
1091 colors: vertex_colors,
1092 });
1093 }
1094 }
1095 }
1096
1097 #[cfg(feature = "lighting")]
1098 RenderMode::Toon(direction, bands) => {
1099 let color_as_float = Vector3::new(
1100 mesh.color.r() as f32 / 32.0,
1101 mesh.color.g() as f32 / 64.0,
1102 mesh.color.b() as f32 / 32.0,
1103 );
1104 let ambient_color = color_as_float * 0.15;
1105 let adjusted_dir = Vector3::new(direction.x, direction.y, -direction.z);
1106 let bands_f = bands.max(1) as f32;
1107
1108 for (face, normal) in geometry.faces.iter().zip(geometry.normals.iter()) {
1109 let normal = Vector3::new(normal[0], normal[1], normal[2]);
1110 let transformed_normal = mesh.model_matrix.transform_vector(&normal);
1111 if self.is_backface(
1112 face,
1113 geometry.vertices,
1114 mesh.model_matrix,
1115 &transformed_normal,
1116 ) {
1117 continue;
1118 }
1119
1120 if let Some([p1, p2, p3]) = tf_face(face) {
1121 let raw_intensity = transformed_normal.dot(&adjusted_dir).max(0.0);
1122 let intensity =
1123 ((raw_intensity * bands_f).round() / bands_f).clamp(0.0, 1.0);
1124
1125 let final_color = color_as_float * intensity + ambient_color;
1126 let final_color = Vector3::new(
1127 final_color.x.clamp(0.0, 1.0),
1128 final_color.y.clamp(0.0, 1.0),
1129 final_color.z.clamp(0.0, 1.0),
1130 );
1131 let mut color = Rgb565::new(
1132 (final_color.x * 31.0) as u8,
1133 (final_color.y * 63.0) as u8,
1134 (final_color.z * 31.0) as u8,
1135 );
1136 if !self.point_lights.is_empty() {
1137 let wc = Self::face_world_center(
1138 face,
1139 geometry.vertices,
1140 mesh.model_matrix,
1141 );
1142 color = Self::add_tint(color, self.light_tint_at(wc));
1143 }
1144 emit(DrawPrimitive::ColoredTriangleWithDepth {
1145 points: [p1.xy(), p2.xy(), p3.xy()],
1146 depths: [p1.z as f32, p2.z as f32, p3.z as f32],
1147 color,
1148 });
1149 }
1150 }
1151 }
1152
1153 #[cfg(feature = "lighting")]
1154 RenderMode::BlinnPhong {
1155 light_dir,
1156 specular_intensity,
1157 shininess,
1158 } => {
1159 let color_as_float = Vector3::new(
1161 mesh.color.r() as f32 / 32.0,
1162 mesh.color.g() as f32 / 64.0,
1163 mesh.color.b() as f32 / 32.0,
1164 );
1165
1166 let ambient_color = color_as_float * 0.1;
1168
1169 let adjusted_light_dir = Vector3::new(light_dir.x, light_dir.y, -light_dir.z);
1172
1173 let light_dir_normalized = adjusted_light_dir.normalize();
1175
1176 for (face, normal) in geometry.faces.iter().zip(geometry.normals.iter()) {
1177 let normal = Vector3::new(normal[0], normal[1], normal[2]);
1179 let transformed_normal = mesh.model_matrix.transform_vector(&normal);
1180 let normalized_normal = transformed_normal.normalize();
1181
1182 if self.is_backface(
1184 face,
1185 geometry.vertices,
1186 mesh.model_matrix,
1187 &normalized_normal,
1188 ) {
1189 continue;
1190 }
1191
1192 if let Some([p1, p2, p3]) = tf_face(face) {
1193 let v0 = geometry.vertices[face[0]];
1195 let v1 = geometry.vertices[face[1]];
1196 let v2 = geometry.vertices[face[2]];
1197 let face_center = Point3::new(
1198 (v0[0] + v1[0] + v2[0]) / 3.0,
1199 (v0[1] + v1[1] + v2[1]) / 3.0,
1200 (v0[2] + v1[2] + v2[2]) / 3.0,
1201 );
1202 let face_center_world = mesh.model_matrix.transform_point(&face_center);
1203
1204 let view_dir = (self.camera.position - face_center_world).normalize();
1206
1207 let half_vector = (light_dir_normalized + view_dir).normalize();
1209
1210 let diffuse_intensity =
1212 normalized_normal.dot(&light_dir_normalized).max(0.0);
1213
1214 let specular_term =
1216 normalized_normal.dot(&half_vector).max(0.0).powf(shininess);
1217
1218 let diffuse_color = color_as_float * diffuse_intensity;
1220 let specular_color =
1221 Vector3::new(1.0, 1.0, 1.0) * specular_term * specular_intensity;
1222 let final_color = ambient_color + diffuse_color + specular_color;
1223
1224 let final_color = Vector3::new(
1225 final_color.x.clamp(0.0, 1.0),
1226 final_color.y.clamp(0.0, 1.0),
1227 final_color.z.clamp(0.0, 1.0),
1228 );
1229
1230 let mut color = Rgb565::new(
1231 (final_color.x * 31.0) as u8,
1232 (final_color.y * 63.0) as u8,
1233 (final_color.z * 31.0) as u8,
1234 );
1235 if !self.point_lights.is_empty() {
1236 color =
1237 Self::add_tint(color, self.light_tint_at(face_center_world));
1238 }
1239 emit(DrawPrimitive::ColoredTriangleWithDepth {
1240 points: [p1.xy(), p2.xy(), p3.xy()],
1241 depths: [p1.z as f32, p2.z as f32, p3.z as f32],
1242 color,
1243 });
1244 }
1245 }
1246 }
1247
1248 RenderMode::Solid => {
1249 if geometry.normals.is_empty() {
1250 for face in geometry.faces.iter() {
1251 #[cfg(feature = "lighting")]
1252 let color = if !self.point_lights.is_empty() {
1253 let wc = Self::face_world_center(
1254 face,
1255 geometry.vertices,
1256 mesh.model_matrix,
1257 );
1258 Self::add_tint(mesh.color, self.light_tint_at(wc))
1259 } else {
1260 mesh.color
1261 };
1262 #[cfg(not(feature = "lighting"))]
1263 let color = mesh.color;
1264 let v = &geometry.vertices;
1265 let clip = [
1266 transform_matrix
1267 * Vector4::new(
1268 v[face[0]][0],
1269 v[face[0]][1],
1270 v[face[0]][2],
1271 1.0,
1272 ),
1273 transform_matrix
1274 * Vector4::new(
1275 v[face[1]][0],
1276 v[face[1]][1],
1277 v[face[1]][2],
1278 1.0,
1279 ),
1280 transform_matrix
1281 * Vector4::new(
1282 v[face[2]][0],
1283 v[face[2]][1],
1284 v[face[2]][2],
1285 1.0,
1286 ),
1287 ];
1288 self.emit_clipped(clip, color, &mut callback);
1289 }
1290 } else {
1291 for (face, normal) in geometry.faces.iter().zip(geometry.normals) {
1292 let normal = Vector3::new(normal[0], normal[1], normal[2]);
1293 let transformed_normal = mesh.model_matrix.transform_vector(&normal);
1294 if self.is_backface(
1295 face,
1296 geometry.vertices,
1297 mesh.model_matrix,
1298 &transformed_normal,
1299 ) {
1300 continue;
1301 }
1302 #[cfg(feature = "lighting")]
1303 let color = if !self.point_lights.is_empty() {
1304 let wc = Self::face_world_center(
1305 face,
1306 geometry.vertices,
1307 mesh.model_matrix,
1308 );
1309 Self::add_tint(mesh.color, self.light_tint_at(wc))
1310 } else {
1311 mesh.color
1312 };
1313 #[cfg(not(feature = "lighting"))]
1314 let color = mesh.color;
1315 let v = &geometry.vertices;
1316 let clip = [
1317 transform_matrix
1318 * Vector4::new(
1319 v[face[0]][0],
1320 v[face[0]][1],
1321 v[face[0]][2],
1322 1.0,
1323 ),
1324 transform_matrix
1325 * Vector4::new(
1326 v[face[1]][0],
1327 v[face[1]][1],
1328 v[face[1]][2],
1329 1.0,
1330 ),
1331 transform_matrix
1332 * Vector4::new(
1333 v[face[2]][0],
1334 v[face[2]][1],
1335 v[face[2]][2],
1336 1.0,
1337 ),
1338 ];
1339 self.emit_clipped(clip, color, &mut callback);
1340 }
1341 }
1342 }
1343
1344 #[cfg(feature = "lighting")]
1345 RenderMode::SectorBright(brightness) => {
1346 if geometry.normals.is_empty() {
1347 for face in geometry.faces.iter() {
1348 let wc =
1349 Self::face_world_center(face, geometry.vertices, mesh.model_matrix);
1350 let mut color = self.sector_shaded_color(mesh.color, brightness, wc);
1351 if !self.point_lights.is_empty() {
1352 color = Self::add_tint(color, self.light_tint_at(wc));
1353 }
1354 let v = &geometry.vertices;
1355 let clip = [
1356 transform_matrix
1357 * Vector4::new(
1358 v[face[0]][0],
1359 v[face[0]][1],
1360 v[face[0]][2],
1361 1.0,
1362 ),
1363 transform_matrix
1364 * Vector4::new(
1365 v[face[1]][0],
1366 v[face[1]][1],
1367 v[face[1]][2],
1368 1.0,
1369 ),
1370 transform_matrix
1371 * Vector4::new(
1372 v[face[2]][0],
1373 v[face[2]][1],
1374 v[face[2]][2],
1375 1.0,
1376 ),
1377 ];
1378 self.emit_clipped(clip, color, &mut callback);
1379 }
1380 } else {
1381 for (face, normal) in geometry.faces.iter().zip(geometry.normals) {
1382 let normal = Vector3::new(normal[0], normal[1], normal[2]);
1383 let transformed_normal = mesh.model_matrix.transform_vector(&normal);
1384 if self.is_backface(
1385 face,
1386 geometry.vertices,
1387 mesh.model_matrix,
1388 &transformed_normal,
1389 ) {
1390 continue;
1391 }
1392 let wc =
1393 Self::face_world_center(face, geometry.vertices, mesh.model_matrix);
1394 let mut color = self.sector_shaded_color(mesh.color, brightness, wc);
1395 if !self.point_lights.is_empty() {
1396 color = Self::add_tint(color, self.light_tint_at(wc));
1397 }
1398 let v = &geometry.vertices;
1399 let clip = [
1400 transform_matrix
1401 * Vector4::new(
1402 v[face[0]][0],
1403 v[face[0]][1],
1404 v[face[0]][2],
1405 1.0,
1406 ),
1407 transform_matrix
1408 * Vector4::new(
1409 v[face[1]][0],
1410 v[face[1]][1],
1411 v[face[1]][2],
1412 1.0,
1413 ),
1414 transform_matrix
1415 * Vector4::new(
1416 v[face[2]][0],
1417 v[face[2]][1],
1418 v[face[2]][2],
1419 1.0,
1420 ),
1421 ];
1422 self.emit_clipped(clip, color, &mut callback);
1423 }
1424 }
1425 }
1426
1427 #[cfg(feature = "textured")]
1428 RenderMode::Textured => {
1429 let Some(texture_id) = geometry.texture_id else {
1434 continue;
1435 };
1436 if geometry.uvs.is_empty() {
1437 continue;
1438 }
1439
1440 if geometry.normals.is_empty() {
1446 for face in geometry.faces.iter() {
1447 if let Some((points, ws)) = tf_face_w(face) {
1448 emit(DrawPrimitive::TexturedTriangleWithDepth {
1449 points: [points[0].xy(), points[1].xy(), points[2].xy()],
1450 depths: [
1451 points[0].z as f32,
1452 points[1].z as f32,
1453 points[2].z as f32,
1454 ],
1455 ws,
1456 uvs: [
1457 geometry.uvs[face[0]],
1458 geometry.uvs[face[1]],
1459 geometry.uvs[face[2]],
1460 ],
1461 texture_id,
1462 });
1463 }
1464 }
1465 } else {
1466 for (face, normal) in geometry.faces.iter().zip(geometry.normals) {
1467 let normal = Vector3::new(normal[0], normal[1], normal[2]);
1468 let transformed_normal = mesh.model_matrix.transform_vector(&normal);
1469 if self.is_backface(
1470 face,
1471 geometry.vertices,
1472 mesh.model_matrix,
1473 &transformed_normal,
1474 ) {
1475 continue;
1476 }
1477 if let Some((points, ws)) = tf_face_w(face) {
1478 emit(DrawPrimitive::TexturedTriangleWithDepth {
1479 points: [points[0].xy(), points[1].xy(), points[2].xy()],
1480 depths: [
1481 points[0].z as f32,
1482 points[1].z as f32,
1483 points[2].z as f32,
1484 ],
1485 ws,
1486 uvs: [
1487 geometry.uvs[face[0]],
1488 geometry.uvs[face[1]],
1489 geometry.uvs[face[2]],
1490 ],
1491 texture_id,
1492 });
1493 }
1494 }
1495 }
1496 }
1497 #[cfg(feature = "textured")]
1498 RenderMode::TexturedGouraud(direction) => {
1499 let Some(texture_id) = geometry.texture_id else {
1500 continue;
1501 };
1502 if geometry.uvs.is_empty() {
1503 continue;
1504 }
1505 let color_as_float = Vector3::new(
1506 mesh.color.r() as f32 / 32.0,
1507 mesh.color.g() as f32 / 64.0,
1508 mesh.color.b() as f32 / 32.0,
1509 );
1510 let ambient_color = color_as_float * 0.1;
1511 let adjusted_dir = Vector3::new(direction.x, direction.y, -direction.z);
1512
1513 if geometry.normals.is_empty() {
1514 for face in geometry.faces.iter() {
1515 if let Some((points, ws)) = tf_face_w(face) {
1516 let vertex_colors = [mesh.color, mesh.color, mesh.color];
1517 emit(DrawPrimitive::TexturedGouraudTriangleWithDepth {
1518 points: [points[0].xy(), points[1].xy(), points[2].xy()],
1519 depths: [
1520 points[0].z as f32,
1521 points[1].z as f32,
1522 points[2].z as f32,
1523 ],
1524 ws,
1525 uvs: [
1526 geometry.uvs[face[0]],
1527 geometry.uvs[face[1]],
1528 geometry.uvs[face[2]],
1529 ],
1530 colors: vertex_colors,
1531 texture_id,
1532 });
1533 }
1534 }
1535 } else {
1536 for (face, face_normal) in
1537 geometry.faces.iter().zip(geometry.normals.iter())
1538 {
1539 let fn_vec =
1540 Vector3::new(face_normal[0], face_normal[1], face_normal[2]);
1541 let transformed_fn = mesh.model_matrix.transform_vector(&fn_vec);
1542
1543 if self.is_backface(
1544 face,
1545 geometry.vertices,
1546 mesh.model_matrix,
1547 &transformed_fn,
1548 ) {
1549 continue;
1550 }
1551
1552 if let Some((points, ws)) = tf_face_w(face) {
1553 let vertex_colors: [Rgb565; 3] = core::array::from_fn(|k| {
1554 let vn = if !geometry.vertex_normals.is_empty() {
1555 let vn_arr = geometry.vertex_normals[face[k]];
1556 let vn_vec = Vector3::new(vn_arr[0], vn_arr[1], vn_arr[2]);
1557 mesh.model_matrix.transform_vector(&vn_vec)
1558 } else {
1559 transformed_fn
1560 };
1561
1562 let intensity = vn.dot(&adjusted_dir).max(0.0);
1563 let c = color_as_float * intensity + ambient_color;
1564 let mut vc = Rgb565::new(
1565 (c.x.clamp(0.0, 1.0) * 31.0) as u8,
1566 (c.y.clamp(0.0, 1.0) * 63.0) as u8,
1567 (c.z.clamp(0.0, 1.0) * 31.0) as u8,
1568 );
1569 if !self.point_lights.is_empty() {
1570 let vpos = geometry.vertices[face[k]];
1571 let wp = mesh.model_matrix.transform_point(&Point3::new(
1572 vpos[0], vpos[1], vpos[2],
1573 ));
1574 vc = Self::add_tint(vc, self.light_tint_at(wp));
1575 }
1576 vc
1577 });
1578
1579 emit(DrawPrimitive::TexturedGouraudTriangleWithDepth {
1580 points: [points[0].xy(), points[1].xy(), points[2].xy()],
1581 depths: [
1582 points[0].z as f32,
1583 points[1].z as f32,
1584 points[2].z as f32,
1585 ],
1586 ws,
1587 uvs: [
1588 geometry.uvs[face[0]],
1589 geometry.uvs[face[1]],
1590 geometry.uvs[face[2]],
1591 ],
1592 colors: vertex_colors,
1593 texture_id,
1594 });
1595 }
1596 }
1597 }
1598 }
1599 #[cfg(feature = "textured")]
1600 RenderMode::MatCap => {
1601 let Some(texture_id) = geometry.texture_id else {
1602 continue;
1603 };
1604 let has_vertex_normals = !geometry.vertex_normals.is_empty();
1605 let has_face_normals = !geometry.normals.is_empty();
1606 if !has_vertex_normals && !has_face_normals {
1607 continue;
1608 }
1609
1610 let mv = self.camera.view_matrix * mesh.model_matrix;
1611 let normal_matrix = nalgebra::Matrix3::new(
1612 mv[(0, 0)],
1613 mv[(0, 1)],
1614 mv[(0, 2)],
1615 mv[(1, 0)],
1616 mv[(1, 1)],
1617 mv[(1, 2)],
1618 mv[(2, 0)],
1619 mv[(2, 1)],
1620 mv[(2, 2)],
1621 );
1622
1623 for (face_idx, face) in geometry.faces.iter().enumerate() {
1624 let face_normal = if has_face_normals {
1625 Vector3::new(
1626 geometry.normals[face_idx][0],
1627 geometry.normals[face_idx][1],
1628 geometry.normals[face_idx][2],
1629 )
1630 } else {
1631 let v0 = Vector3::new(
1632 geometry.vertices[face[0]][0],
1633 geometry.vertices[face[0]][1],
1634 geometry.vertices[face[0]][2],
1635 );
1636 let v1 = Vector3::new(
1637 geometry.vertices[face[1]][0],
1638 geometry.vertices[face[1]][1],
1639 geometry.vertices[face[1]][2],
1640 );
1641 let v2 = Vector3::new(
1642 geometry.vertices[face[2]][0],
1643 geometry.vertices[face[2]][1],
1644 geometry.vertices[face[2]][2],
1645 );
1646 (v1 - v0).cross(&(v2 - v0)).normalize()
1647 };
1648
1649 let transformed_normal = mesh.model_matrix.transform_vector(&face_normal);
1650 if self.is_backface(
1651 face,
1652 geometry.vertices,
1653 mesh.model_matrix,
1654 &transformed_normal,
1655 ) {
1656 continue;
1657 }
1658
1659 if let Some((points, ws)) = tf_face_w(face) {
1660 let mut uvs = [[0.0f32; 2]; 3];
1661 for i in 0..3 {
1662 let vertex_normal = if has_vertex_normals {
1663 Vector3::new(
1664 geometry.vertex_normals[face[i]][0],
1665 geometry.vertex_normals[face[i]][1],
1666 geometry.vertex_normals[face[i]][2],
1667 )
1668 } else {
1669 face_normal
1670 };
1671 let view_normal = (normal_matrix * vertex_normal).normalize();
1672 let u = view_normal.x * 0.5 + 0.5;
1673 let v = -view_normal.y * 0.5 + 0.5;
1674 uvs[i] = [u, v];
1675 }
1676
1677 emit(DrawPrimitive::TexturedTriangleWithDepth {
1678 points: [points[0].xy(), points[1].xy(), points[2].xy()],
1679 depths: [
1680 points[0].z as f32,
1681 points[1].z as f32,
1682 points[2].z as f32,
1683 ],
1684 ws,
1685 uvs,
1686 texture_id,
1687 });
1688 }
1689 }
1690 }
1691 }
1692 }
1693 }
1694
1695 pub fn record<'a, MS, const MAX: usize>(
1696 &self,
1697 meshes: MS,
1698 commands: &mut crate::command_buffer::CommandBuffer<MAX>,
1699 telemetry: Option<&mut crate::telemetry::RecordTelemetry>,
1700 ) -> Result<(), crate::error::RenderError>
1701 where
1702 MS: IntoIterator<Item = &'a K3dMesh<'a>>,
1703 {
1704 self.record_impl(meshes, commands, telemetry)
1705 }
1706
1707 pub fn record_drop_shadow<const MAX: usize>(
1710 &self,
1711 mesh: &K3dMesh,
1712 floor_y: f32,
1713 shadow_radius: f32,
1714 max_fade_distance: f32,
1715 shadow_opacity: u8,
1716 color: Rgb565,
1717 commands: &mut crate::command_buffer::CommandBuffer<MAX>,
1718 ) -> Result<(), crate::error::RenderError> {
1719 let pos = mesh.get_position();
1720 let height = pos.y - floor_y;
1721 if height < 0.0 || height >= max_fade_distance {
1722 return Ok(());
1723 }
1724
1725 let fade = 1.0 - (height / max_fade_distance).clamp(0.0, 1.0);
1726 let radius = shadow_radius * fade;
1727 let opacity = (shadow_opacity as f32 * fade) as u8;
1728 if opacity == 0 {
1729 return Ok(());
1730 }
1731
1732 let y_pos = floor_y + 0.01;
1733 let center_world = [pos.x, y_pos, pos.z];
1734 let center_proj = self.transform_point_with_w(¢er_world, self.camera.vp_matrix);
1735 let Some((c_pt, _c_w)) = center_proj else {
1736 return Ok(());
1737 };
1738
1739 let mut outer_proj: [Option<(Point3<i32>, f32)>; 8] = [None; 8];
1740 for i in 0..8 {
1741 let angle = (i as f32) * (core::f32::consts::PI / 4.0);
1742 let px = pos.x + radius * micromath::F32Ext::cos(angle);
1743 let pz = pos.z + radius * micromath::F32Ext::sin(angle);
1744 outer_proj[i] = self.transform_point_with_w(&[px, y_pos, pz], self.camera.vp_matrix);
1745 }
1746
1747 for i in 0..8 {
1748 let next_idx = (i + 1) % 8;
1749 if let (Some((p1, _w1)), Some((p2, _w2))) = (outer_proj[i], outer_proj[next_idx]) {
1750 commands.push(crate::command_buffer::RenderCommand::Draw(
1751 DrawPrimitive::TranslucentTriangleWithDepth {
1752 points: [c_pt.xy(), p1.xy(), p2.xy()],
1753 depths: [c_pt.z as f32, p1.z as f32, p2.z as f32],
1754 color,
1755 alpha: opacity,
1756 },
1757 ))?;
1758 }
1759 }
1760
1761 Ok(())
1762 }
1763
1764 fn record_impl<'a, MS, const MAX: usize>(
1765 &self,
1766 meshes: MS,
1767 commands: &mut crate::command_buffer::CommandBuffer<MAX>,
1768 telemetry: Option<&mut crate::telemetry::RecordTelemetry>,
1769 ) -> Result<(), crate::error::RenderError>
1770 where
1771 MS: IntoIterator<Item = &'a K3dMesh<'a>>,
1772 {
1773 use crate::command_buffer::RenderCommand;
1774
1775 commands.clear();
1776 commands.push(RenderCommand::ClearDepth(crate::Z_MAX_VALUE))?;
1777 if let Some(caps) = self.caps {
1778 caps.validate_framebuffer(self.width as usize, self.height as usize)?;
1779 }
1780
1781 let mut first_error = None;
1782 let mut visible_meshes = 0usize;
1783 let mut used_texture_ids: heapless::Vec<u32, 64> = heapless::Vec::new();
1784 let mut meshes_total = 0usize;
1785
1786 #[cfg(feature = "record-sort")]
1787 {
1788 let mut sorted: heapless::Vec<(u8, i32, &K3dMesh<'a>), 256> = heapless::Vec::new();
1789 for mesh in meshes {
1790 meshes_total += 1;
1791 if mesh.geometry.vertices.is_empty() {
1792 continue;
1793 }
1794 if self.should_cull_mesh(mesh) {
1795 continue;
1796 }
1797 let distance = (mesh.get_position() - self.camera.position).norm();
1798 let dist_key = (distance * 1000.0) as i32;
1799 if sorted.push((mesh.priority, dist_key, mesh)).is_err() {
1800 break;
1801 }
1802 }
1803 sorted.sort_unstable_by(|a, b| b.0.cmp(&a.0).then_with(|| a.1.cmp(&b.1)));
1804
1805 for &(_, _, mesh) in sorted.iter() {
1806 Self::record_one_mesh(
1807 self,
1808 mesh,
1809 commands,
1810 &mut first_error,
1811 &mut visible_meshes,
1812 &mut used_texture_ids,
1813 )?;
1814 if let Some(err) = first_error.take() {
1815 return Err(err);
1816 }
1817 }
1818 }
1819
1820 #[cfg(not(feature = "record-sort"))]
1821 {
1822 for mesh in meshes {
1823 meshes_total += 1;
1824 if mesh.geometry.vertices.is_empty() {
1825 continue;
1826 }
1827 if self.should_cull_mesh(mesh) {
1828 continue;
1829 }
1830 Self::record_one_mesh(
1831 self,
1832 mesh,
1833 commands,
1834 &mut first_error,
1835 &mut visible_meshes,
1836 &mut used_texture_ids,
1837 )?;
1838 if let Some(err) = first_error.take() {
1839 return Err(err);
1840 }
1841 }
1842 }
1843
1844 if let Some(t) = telemetry {
1845 t.meshes_total = meshes_total;
1846 t.meshes_visible = visible_meshes;
1847 t.unique_textures = used_texture_ids.len();
1848 t.draw_commands = commands
1849 .iter()
1850 .filter(|cmd| matches!(cmd, RenderCommand::Draw(_)))
1851 .count();
1852 t.fallback_used = false;
1853 t.degradation_steps_applied = 0;
1854 t.dropped_meshes = 0;
1855 }
1856
1857 Ok(())
1858 }
1859
1860 fn record_one_mesh<'a, const MAX: usize>(
1861 &self,
1862 mesh: &'a K3dMesh<'a>,
1863 commands: &mut crate::command_buffer::CommandBuffer<MAX>,
1864 first_error: &mut Option<crate::error::RenderError>,
1865 visible_meshes: &mut usize,
1866 used_texture_ids: &mut heapless::Vec<u32, 64>,
1867 ) -> Result<(), crate::error::RenderError> {
1868 use crate::command_buffer::RenderCommand;
1869 use crate::error::{BudgetKind, RenderError};
1870
1871 let distance = (mesh.get_position() - self.camera.position).norm();
1872 let geometry = mesh.select_lod(distance);
1873
1874 if let Some(caps) = self.caps {
1875 *visible_meshes += 1;
1876 if *visible_meshes > caps.max_meshes_per_frame {
1877 return Err(RenderError::OutOfBudget(BudgetKind::MeshesPerFrame {
1878 attempted: *visible_meshes,
1879 max: caps.max_meshes_per_frame,
1880 }));
1881 }
1882
1883 if geometry.vertices.len() > caps.max_vertices_per_mesh {
1884 return Err(RenderError::OutOfBudget(BudgetKind::VerticesPerMesh {
1885 attempted: geometry.vertices.len(),
1886 max: caps.max_vertices_per_mesh,
1887 }));
1888 }
1889
1890 if geometry.faces.len() > caps.max_triangles_per_mesh {
1891 return Err(RenderError::OutOfBudget(BudgetKind::TrianglesPerMesh {
1892 attempted: geometry.faces.len(),
1893 max: caps.max_triangles_per_mesh,
1894 }));
1895 }
1896
1897 if let Some(texture_id) = geometry.texture_id
1898 && !used_texture_ids.contains(&texture_id)
1899 {
1900 let attempted = used_texture_ids.len() + 1;
1901 if attempted > caps.max_textures {
1902 return Err(RenderError::OutOfBudget(BudgetKind::Textures {
1903 attempted,
1904 max: caps.max_textures,
1905 }));
1906 }
1907
1908 if used_texture_ids.push(texture_id).is_err() {
1909 return Err(RenderError::OutOfBudget(BudgetKind::Textures {
1910 attempted,
1911 max: caps.max_textures,
1912 }));
1913 }
1914 }
1915 } else {
1916 *visible_meshes += 1;
1917 }
1918
1919 let mut push_draw = |primitive: DrawPrimitive, first_error: &mut Option<RenderError>| {
1920 if first_error.is_none()
1921 && let Err(e) = commands.push(RenderCommand::Draw(primitive))
1922 {
1923 *first_error = Some(e);
1924 }
1925 };
1926
1927 #[cfg(feature = "lod-crossfade")]
1928 {
1929 match mesh.select_lod_pick(distance) {
1930 mesh::LodPick::Single(_) => {
1931 mesh.lod_force.set(None);
1932 mesh.draw_alpha.set(None);
1933 self.render(core::iter::once(mesh), |primitive| {
1934 push_draw(primitive, first_error);
1935 });
1936 }
1937 mesh::LodPick::Crossfade { near, far, t } => {
1938 let near_lvl = mesh.lod_level_of(near);
1939 let far_lvl = mesh.lod_level_of(far);
1940 if t < 0.5 {
1941 mesh.lod_force.set(Some(near_lvl));
1942 mesh.draw_alpha.set(None);
1943 self.render(core::iter::once(mesh), |primitive| {
1944 push_draw(primitive, first_error);
1945 });
1946 let a = (t * 255.0) as u8;
1947 if a > 16 {
1948 mesh.lod_force.set(Some(far_lvl));
1949 mesh.draw_alpha.set(Some(a));
1950 self.render(core::iter::once(mesh), |primitive| {
1951 push_draw(primitive, first_error);
1952 });
1953 }
1954 } else {
1955 mesh.lod_force.set(Some(far_lvl));
1956 mesh.draw_alpha.set(None);
1957 self.render(core::iter::once(mesh), |primitive| {
1958 push_draw(primitive, first_error);
1959 });
1960 let a = ((1.0 - t) * 255.0) as u8;
1961 if a > 16 {
1962 mesh.lod_force.set(Some(near_lvl));
1963 mesh.draw_alpha.set(Some(a));
1964 self.render(core::iter::once(mesh), |primitive| {
1965 push_draw(primitive, first_error);
1966 });
1967 }
1968 }
1969 mesh.lod_force.set(None);
1970 mesh.draw_alpha.set(None);
1971 }
1972 }
1973 }
1974 #[cfg(not(feature = "lod-crossfade"))]
1975 {
1976 self.render(core::iter::once(mesh), |primitive| {
1977 push_draw(primitive, first_error);
1978 });
1979 }
1980 Ok(())
1981 }
1982
1983 pub fn record_with_fallback<'a, MS, FS, const MAX: usize>(
1984 &self,
1985 primary: MS,
1986 fallback: FS,
1987 commands: &mut crate::command_buffer::CommandBuffer<MAX>,
1988 telemetry: Option<&mut crate::telemetry::RecordTelemetry>,
1989 ) -> Result<BudgetFallbackOutcome, crate::error::RenderError>
1990 where
1991 MS: IntoIterator<Item = &'a K3dMesh<'a>>,
1992 FS: IntoIterator<Item = &'a K3dMesh<'a>>,
1993 {
1994 use crate::error::RenderError;
1995
1996 let mut local_telemetry = crate::telemetry::RecordTelemetry::default();
1997 match self.record_impl(primary, commands, Some(&mut local_telemetry)) {
1998 Ok(()) => {
1999 if let Some(t) = telemetry {
2000 *t = local_telemetry;
2001 t.fallback_used = false;
2002 }
2003 Ok(BudgetFallbackOutcome {
2004 used_fallback: false,
2005 primary_budget_error: None,
2006 })
2007 }
2008 Err(RenderError::OutOfBudget(kind)) => {
2009 let mut fallback_telemetry = crate::telemetry::RecordTelemetry::default();
2010 self.record_impl(fallback, commands, Some(&mut fallback_telemetry))?;
2011 if let Some(t) = telemetry {
2012 *t = fallback_telemetry;
2013 t.fallback_used = true;
2014 }
2015 Ok(BudgetFallbackOutcome {
2016 used_fallback: true,
2017 primary_budget_error: Some(kind),
2018 })
2019 }
2020 Err(e) => Err(e),
2021 }
2022 }
2023
2024 fn downgraded_quality_tier(tier: crate::config::QualityTier) -> crate::config::QualityTier {
2025 use crate::config::QualityTier;
2026 match tier {
2027 QualityTier::Quality => QualityTier::Balanced,
2028 QualityTier::Balanced => QualityTier::Fastest,
2029 QualityTier::Fastest => QualityTier::Fastest,
2030 }
2031 }
2032
2033 pub fn record_with_degradation<'a, const MAX: usize>(
2034 &mut self,
2035 meshes: &[&'a K3dMesh<'a>],
2036 commands: &mut crate::command_buffer::CommandBuffer<MAX>,
2037 policy: crate::config::DegradationPolicy<'_>,
2038 telemetry: Option<&mut crate::telemetry::RecordTelemetry>,
2039 ) -> Result<DegradationOutcome, crate::error::RenderError> {
2040 use crate::config::DegradationStep;
2041 use crate::error::RenderError;
2042
2043 let original_quality = self.quality_tier;
2044 let mut active_quality = self.quality_tier;
2045
2046 let mut outcome = DegradationOutcome {
2047 used_degradation: false,
2048 steps_applied: 0,
2049 dropped_meshes: 0,
2050 final_quality_tier: active_quality,
2051 primary_budget_error: None,
2052 };
2053
2054 let mut local_telemetry = crate::telemetry::RecordTelemetry::default();
2055 match self.record_impl(meshes.iter().copied(), commands, Some(&mut local_telemetry)) {
2056 Ok(()) => {
2057 if let Some(t) = telemetry {
2058 *t = local_telemetry;
2059 }
2060 return Ok(outcome);
2061 }
2062 Err(RenderError::OutOfBudget(kind)) => {
2063 outcome.primary_budget_error = Some(kind);
2064 }
2065 Err(e) => return Err(e),
2066 }
2067
2068 for step in policy.steps {
2069 outcome.used_degradation = true;
2070 outcome.steps_applied += 1;
2071
2072 let mut selected: heapless::Vec<&K3dMesh<'_>, 512> = heapless::Vec::new();
2073 match *step {
2074 DegradationStep::RaisePriorityFloor(min_priority) => {
2075 for mesh in meshes {
2076 if mesh.priority >= min_priority {
2077 let _ = selected.push(*mesh);
2078 } else {
2079 outcome.dropped_meshes += 1;
2080 }
2081 }
2082 }
2083 DegradationStep::MeshDecimationStride(stride) => {
2084 if stride == 0 {
2085 self.quality_tier = original_quality;
2086 return Err(RenderError::InvalidInput(
2087 "mesh decimation stride must be >= 1",
2088 ));
2089 }
2090 for (idx, mesh) in meshes.iter().enumerate() {
2091 if idx % stride == 0 {
2092 let _ = selected.push(*mesh);
2093 } else {
2094 outcome.dropped_meshes += 1;
2095 }
2096 }
2097 }
2098 DegradationStep::DowngradeQuality => {
2099 active_quality = Self::downgraded_quality_tier(active_quality);
2100 self.quality_tier = active_quality;
2101 for mesh in meshes {
2102 let _ = selected.push(*mesh);
2103 }
2104 }
2105 }
2106
2107 if selected.is_empty() {
2108 continue;
2109 }
2110
2111 let mut step_telemetry = crate::telemetry::RecordTelemetry::default();
2112 let attempt = self.record_impl(
2113 selected.iter().copied(),
2114 commands,
2115 Some(&mut step_telemetry),
2116 );
2117
2118 if let Ok(()) = attempt {
2119 outcome.final_quality_tier = self.quality_tier;
2120 if let Some(t) = telemetry {
2121 *t = step_telemetry;
2122 t.fallback_used = true;
2123 t.degradation_steps_applied = outcome.steps_applied;
2124 t.dropped_meshes = outcome.dropped_meshes;
2125 }
2126 self.quality_tier = original_quality;
2127 return Ok(outcome);
2128 }
2129 }
2130
2131 self.quality_tier = original_quality;
2132 Err(crate::error::RenderError::Recoverable {
2133 fault: crate::error::RuntimeFaultKind::Budget(outcome.primary_budget_error.unwrap_or(
2134 crate::error::BudgetKind::DrawPrimitives {
2135 attempted: commands.len(),
2136 max: MAX,
2137 },
2138 )),
2139 action: crate::error::RecoveryAction::SkipFrame,
2140 })
2141 }
2142
2143 pub fn execute<D, const MAX: usize>(
2144 &self,
2145 fb: &mut D,
2146 frame: &mut crate::renderer::FrameCtx<'_>,
2147 commands: &crate::command_buffer::CommandBuffer<MAX>,
2148 telemetry: Option<&mut crate::telemetry::ExecuteTelemetry>,
2149 ) -> Result<Option<crate::renderer::DirtyRegion>, crate::error::RenderError>
2150 where
2151 D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>
2152 + embedded_graphics_core::prelude::OriginDimensions,
2153 <D as embedded_graphics_core::draw_target::DrawTarget>::Error: core::fmt::Debug,
2154 {
2155 if let Some(t) = telemetry {
2156 t.commands_total = commands.len();
2157 t.draw_commands = commands
2158 .iter()
2159 .filter(|cmd| matches!(cmd, crate::command_buffer::RenderCommand::Draw(_)))
2160 .count();
2161 t.clear_color_commands = commands
2162 .iter()
2163 .filter(|cmd| matches!(cmd, crate::command_buffer::RenderCommand::ClearColor(_)))
2164 .count();
2165 t.clear_depth_commands = commands
2166 .iter()
2167 .filter(|cmd| matches!(cmd, crate::command_buffer::RenderCommand::ClearDepth(_)))
2168 .count();
2169 }
2170 let camera_dir = self.camera.get_direction();
2171 crate::renderer::execute_commands_with_dirty_region_effects(
2172 fb,
2173 frame,
2174 commands,
2175 self.fog.as_ref(),
2176 self.dither.as_ref(),
2177 self.screen_tint,
2178 self.stipple_mode,
2179 self.palette_mode,
2180 self.sky,
2181 [camera_dir.x, camera_dir.y, camera_dir.z],
2182 )
2183 }
2184
2185 #[cfg(feature = "textured")]
2196 pub fn execute_with_textures<D, const MAX: usize, const N: usize>(
2197 &self,
2198 fb: &mut D,
2199 frame: &mut crate::renderer::FrameCtx<'_>,
2200 commands: &crate::command_buffer::CommandBuffer<MAX>,
2201 texture_manager: &crate::texture::TextureManager<N>,
2202 telemetry: Option<&mut crate::telemetry::ExecuteTelemetry>,
2203 ) -> Result<Option<crate::renderer::DirtyRegion>, crate::error::RenderError>
2204 where
2205 D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>
2206 + embedded_graphics_core::prelude::OriginDimensions,
2207 <D as embedded_graphics_core::draw_target::DrawTarget>::Error: core::fmt::Debug,
2208 {
2209 if let Some(t) = telemetry {
2210 t.commands_total = commands.len();
2211 t.draw_commands = commands
2212 .iter()
2213 .filter(|cmd| matches!(cmd, crate::command_buffer::RenderCommand::Draw(_)))
2214 .count();
2215 t.clear_color_commands = commands
2216 .iter()
2217 .filter(|cmd| matches!(cmd, crate::command_buffer::RenderCommand::ClearColor(_)))
2218 .count();
2219 t.clear_depth_commands = commands
2220 .iter()
2221 .filter(|cmd| matches!(cmd, crate::command_buffer::RenderCommand::ClearDepth(_)))
2222 .count();
2223 }
2224 let camera_dir = self.camera.get_direction();
2225 crate::renderer::execute_commands_with_dirty_region_effects_textured(
2226 fb,
2227 frame,
2228 commands,
2229 texture_manager,
2230 self.fog.as_ref(),
2231 self.dither.as_ref(),
2232 self.screen_tint,
2233 self.stipple_mode,
2234 self.palette_mode,
2235 self.sky,
2236 [camera_dir.x, camera_dir.y, camera_dir.z],
2237 )
2238 }
2239
2240 pub fn execute_tiled<D, const MAX: usize, const BIN_CAP: usize>(
2241 &self,
2242 fb: &mut D,
2243 frame: &mut crate::renderer::FrameCtx<'_>,
2244 commands: &crate::command_buffer::CommandBuffer<MAX>,
2245 tile: crate::tilebin::TileConfig,
2246 ) -> Result<crate::tilebin::TileBinStats, crate::error::RenderError>
2247 where
2248 D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>
2249 + embedded_graphics_core::prelude::OriginDimensions,
2250 <D as embedded_graphics_core::draw_target::DrawTarget>::Error: core::fmt::Debug,
2251 {
2252 let camera_dir = self.camera.get_direction();
2253 crate::renderer::execute_commands_tiled_effects::<D, MAX, BIN_CAP>(
2254 fb,
2255 frame,
2256 commands,
2257 tile,
2258 self.fog.as_ref(),
2259 self.dither.as_ref(),
2260 self.screen_tint,
2261 self.stipple_mode,
2262 self.palette_mode,
2263 self.sky,
2264 [camera_dir.x, camera_dir.y, camera_dir.z],
2265 )
2266 }
2267
2268 #[cfg(feature = "gizmos")]
2270 pub fn record_aabb_gizmo<const MAX: usize>(
2271 &self,
2272 aabb: &crate::bounds::Aabb,
2273 model_matrix: &Matrix4<f32>,
2274 color: Rgb565,
2275 commands: &mut crate::command_buffer::CommandBuffer<MAX>,
2276 ) -> Result<(), crate::error::RenderError> {
2277 let mut err = None;
2278 crate::gizmos::emit_aabb_wireframe_projected(
2279 aabb,
2280 model_matrix,
2281 |p| self.transform_point(&p, self.camera.vp_matrix),
2282 color,
2283 |prim| {
2284 if err.is_none()
2285 && let Err(e) = commands.push(crate::command_buffer::RenderCommand::Draw(prim))
2286 {
2287 err = Some(e);
2288 }
2289 },
2290 );
2291 match err {
2292 Some(e) => Err(e),
2293 None => Ok(()),
2294 }
2295 }
2296
2297 #[cfg(feature = "gizmos")]
2299 pub fn record_frustum_gizmo<const MAX: usize>(
2300 &self,
2301 color: Rgb565,
2302 commands: &mut crate::command_buffer::CommandBuffer<MAX>,
2303 ) -> Result<(), crate::error::RenderError> {
2304 let mut err = None;
2305 crate::gizmos::emit_frustum_wireframe(
2306 &self.camera,
2307 |p| self.transform_point(&p, self.camera.vp_matrix),
2308 color,
2309 |prim| {
2310 if err.is_none()
2311 && let Err(e) = commands.push(crate::command_buffer::RenderCommand::Draw(prim))
2312 {
2313 err = Some(e);
2314 }
2315 },
2316 );
2317 match err {
2318 Some(e) => Err(e),
2319 None => Ok(()),
2320 }
2321 }
2322}
2323
2324#[cfg(feature = "lod-crossfade")]
2325fn apply_draw_alpha(prim: DrawPrimitive, alpha: u8) -> DrawPrimitive {
2326 match prim {
2327 DrawPrimitive::ColoredTriangleWithDepth {
2328 points,
2329 depths,
2330 color,
2331 } => DrawPrimitive::TranslucentTriangleWithDepth {
2332 points,
2333 depths,
2334 color,
2335 alpha,
2336 },
2337 DrawPrimitive::TranslucentTriangleWithDepth {
2338 points,
2339 depths,
2340 color,
2341 alpha: prev,
2342 } => DrawPrimitive::TranslucentTriangleWithDepth {
2343 points,
2344 depths,
2345 color,
2346 alpha: ((prev as u16 * alpha as u16) / 255) as u8,
2347 },
2348 other => other,
2349 }
2350}