Skip to main content

embedded_3dgfx/engine/
mod.rs

1use core::fmt::Debug;
2use embedded_graphics_core::draw_target::DrawTarget;
3use embedded_graphics_core::geometry::OriginDimensions;
4use embedded_graphics_core::pixelcolor::Rgb565;
5use nalgebra::{Matrix4, Point3, Vector3, Vector4};
6
7use crate::camera::Camera;
8use crate::command_buffer::CommandBuffer;
9use crate::error::RenderError;
10use crate::mesh::{K3dMesh, RenderMode};
11use crate::primitive::DrawPrimitive;
12
13mod pipeline;
14mod recording;
15mod transform;
16
17pub struct K3dengine {
18    pub camera: Camera,
19    pub(crate) width: u16,
20    pub(crate) height: u16,
21    pub(crate) caps: Option<crate::config::ProfileCaps>,
22    pub(crate) quality_tier: crate::config::QualityTier,
23    pub(crate) material_profile: crate::config::MaterialProfile,
24    /// Depth-based fog applied during `execute` / `execute_tiled`.
25    pub(crate) fog: Option<crate::draw::FogConfig>,
26    /// Ordered dithering applied during `execute` / `execute_tiled`.
27    pub(crate) dither: Option<crate::draw::DitherConfig>,
28    /// Optional NDC snap precision for retro-style vertex jitter.
29    pub(crate) vertex_snap_bits: u8,
30    /// Texture interpolation mode for textured raster paths.
31    pub(crate) texture_mapping: crate::retro::TextureMapping,
32    /// Sector brightness behavior.
33    pub(crate) light_levels: crate::retro::LightLevels,
34    /// Optional stipple mode for textured/lightmapped passes.
35    pub(crate) stipple_mode: crate::retro::StippleMode,
36    /// Optional full-screen tint blended during rasterization.
37    pub(crate) screen_tint: Option<crate::retro::ScreenTint>,
38    /// Optional palette quantization.
39    pub(crate) palette_mode: crate::retro::PaletteMode,
40    /// Optional sky background rendered before scene geometry.
41    pub(crate) sky: Option<crate::retro::SkyConfig>,
42    /// Runtime point lights (max 16).  Applied at face-centre granularity
43    /// during `record` for mesh geometry and at face level for BSP.
44    #[cfg(feature = "lighting")]
45    pub(crate) point_lights: heapless::Vec<crate::lights::PointLight, 16>,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub struct BudgetFallbackOutcome {
50    pub used_fallback: bool,
51    pub primary_budget_error: Option<crate::error::BudgetKind>,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct DegradationOutcome {
56    pub used_degradation: bool,
57    pub steps_applied: usize,
58    pub dropped_meshes: usize,
59    pub final_quality_tier: crate::config::QualityTier,
60    pub primary_budget_error: Option<crate::error::BudgetKind>,
61}
62
63impl K3dengine {
64    pub fn new(width: u16, height: u16) -> K3dengine {
65        K3dengine {
66            camera: Camera::new(width as f32 / height as f32),
67            width,
68            height,
69            caps: None,
70            quality_tier: crate::config::QualityTier::Balanced,
71            material_profile: crate::config::MaterialProfile::Lambert,
72            fog: None,
73            dither: None,
74            vertex_snap_bits: 0,
75            texture_mapping: crate::retro::TextureMapping::PerspectiveCorrect,
76            light_levels: crate::retro::LightLevels::Linear,
77            stipple_mode: crate::retro::StippleMode::Off,
78            screen_tint: None,
79            palette_mode: crate::retro::PaletteMode::Off,
80            sky: None,
81            #[cfg(feature = "lighting")]
82            point_lights: heapless::Vec::new(),
83        }
84    }
85
86    /// Enable depth-based fog for subsequent [`execute`][Self::execute] calls.
87    pub fn set_fog(&mut self, fog: crate::draw::FogConfig) {
88        self.fog = Some(fog);
89    }
90
91    /// Disable fog (default state).
92    pub fn clear_fog(&mut self) {
93        self.fog = None;
94    }
95
96    /// Enable ordered dithering for subsequent execute passes.
97    pub fn set_dither(&mut self, dither: crate::draw::DitherConfig) {
98        self.dither = Some(dither);
99    }
100
101    /// Disable ordered dithering.
102    pub fn clear_dither(&mut self) {
103        self.dither = None;
104    }
105
106    /// Set NDC vertex snap precision. `0` disables snapping.
107    pub fn set_vertex_snap_bits(&mut self, bits: u8) {
108        self.vertex_snap_bits = bits.min(16);
109    }
110
111    /// Select texture interpolation mode.
112    pub fn set_texture_mapping(&mut self, mapping: crate::retro::TextureMapping) {
113        self.texture_mapping = mapping;
114    }
115
116    /// Select sector light quantization model.
117    pub fn set_light_levels(&mut self, levels: crate::retro::LightLevels) {
118        self.light_levels = levels;
119    }
120
121    /// Set stipple mode used by textured/lightmapped raster paths.
122    pub fn set_stipple_mode(&mut self, mode: crate::retro::StippleMode) {
123        self.stipple_mode = mode;
124    }
125
126    /// Set an optional full-screen tint.
127    pub fn set_screen_tint(&mut self, tint: crate::retro::ScreenTint) {
128        self.screen_tint = Some(tint);
129    }
130
131    /// Disable full-screen tint.
132    pub fn clear_screen_tint(&mut self) {
133        self.screen_tint = None;
134    }
135
136    /// Set output palette quantization mode.
137    pub fn set_palette_mode(&mut self, mode: crate::retro::PaletteMode) {
138        self.palette_mode = mode;
139    }
140
141    /// Set procedural sky rendering parameters.
142    pub fn set_sky(&mut self, sky: crate::retro::SkyConfig) {
143        self.sky = Some(sky);
144    }
145
146    /// Disable procedural sky rendering.
147    pub fn clear_sky(&mut self) {
148        self.sky = None;
149    }
150
151    /// Apply a coarse retro visual preset.
152    pub fn apply_retro_style(&mut self, style: crate::retro::RetroStyle) {
153        self.fog = style.fog;
154        self.dither = style.dither;
155        self.set_vertex_snap_bits(style.vertex_snap_bits);
156        self.texture_mapping = style.texture_mapping;
157        self.light_levels = style.light_levels;
158        self.stipple_mode = style.stipple_mode;
159        self.screen_tint = style.screen_tint;
160        self.palette_mode = style.palette_mode;
161        self.sky = style.sky;
162    }
163
164    /// Add a dynamic point light. Returns `false` when the 16-light limit is reached.
165    #[cfg(feature = "lighting")]
166    pub fn add_point_light(&mut self, light: crate::lights::PointLight) -> bool {
167        self.point_lights.push(light).is_ok()
168    }
169
170    /// Remove all dynamic point lights.
171    #[cfg(feature = "lighting")]
172    pub fn clear_point_lights(&mut self) {
173        self.point_lights.clear();
174    }
175
176    #[cfg(feature = "lighting")]
177    #[allow(dead_code)]
178    #[inline]
179    pub(crate) fn light_tint_at(&self, world_pos: Point3<f32>) -> Rgb565 {
180        pipeline::light_tint_at(self, world_pos)
181    }
182
183    #[cfg(feature = "lighting")]
184    #[allow(dead_code)]
185    #[inline]
186    pub(crate) fn add_tint(base: Rgb565, tint: Rgb565) -> Rgb565 {
187        pipeline::add_tint(base, tint)
188    }
189
190    #[cfg(feature = "lighting")]
191    #[allow(dead_code)]
192    #[inline]
193    pub(crate) fn sector_shaded_color(
194        &self,
195        base: Rgb565,
196        brightness: u8,
197        face_center: Point3<f32>,
198    ) -> Rgb565 {
199        pipeline::sector_shaded_color(self, base, brightness, face_center)
200    }
201
202    #[cfg(feature = "lighting")]
203    #[allow(dead_code)]
204    #[inline]
205    pub(crate) fn face_world_center(
206        face: &[usize; 3],
207        vertices: &[[f32; 3]],
208        model_matrix: Matrix4<f32>,
209    ) -> Point3<f32> {
210        pipeline::face_world_center(face, vertices, model_matrix)
211    }
212
213    pub fn set_caps(&mut self, caps: crate::config::ProfileCaps) {
214        self.caps = Some(caps);
215        self.apply_render_defaults(crate::config::render_defaults_for_profile(caps));
216    }
217
218    pub fn clear_caps(&mut self) {
219        self.caps = None;
220    }
221
222    pub fn set_quality_tier(&mut self, tier: crate::config::QualityTier) {
223        self.quality_tier = tier;
224    }
225
226    pub fn set_material_profile(&mut self, profile: crate::config::MaterialProfile) {
227        self.material_profile = profile;
228    }
229
230    pub fn apply_render_defaults(&mut self, defaults: crate::config::RenderDefaults) {
231        self.quality_tier = defaults.quality_tier;
232        self.material_profile = defaults.material_profile;
233    }
234
235    pub(crate) fn resolve_render_mode(&self, mode: &RenderMode) -> RenderMode {
236        #[cfg(not(feature = "lighting"))]
237        {
238            let _ = self;
239            mode.clone()
240        }
241        #[cfg(feature = "lighting")]
242        {
243            use crate::config::{MaterialProfile, QualityTier};
244            match self.quality_tier {
245                QualityTier::Fastest => match mode {
246                    #[cfg(feature = "lighting")]
247                    RenderMode::BlinnPhong { .. }
248                    | RenderMode::GouraudLightDir(_)
249                    | RenderMode::Toon(_, _)
250                    | RenderMode::SolidLightDir(_) => RenderMode::Solid,
251                    _ => mode.clone(),
252                },
253                QualityTier::Balanced => match (self.material_profile, mode) {
254                    (MaterialProfile::Unlit, RenderMode::BlinnPhong { .. })
255                    | (MaterialProfile::Unlit, RenderMode::GouraudLightDir(_))
256                    | (MaterialProfile::Unlit, RenderMode::Toon(_, _))
257                    | (MaterialProfile::Unlit, RenderMode::SolidLightDir(_)) => RenderMode::Solid,
258                    (MaterialProfile::Lambert, RenderMode::BlinnPhong { light_dir, .. }) => {
259                        RenderMode::SolidLightDir(*light_dir)
260                    }
261                    _ => mode.clone(),
262                },
263                QualityTier::Quality => match (self.material_profile, mode) {
264                    (MaterialProfile::Unlit, RenderMode::BlinnPhong { .. })
265                    | (MaterialProfile::Unlit, RenderMode::GouraudLightDir(_))
266                    | (MaterialProfile::Unlit, RenderMode::Toon(_, _))
267                    | (MaterialProfile::Unlit, RenderMode::SolidLightDir(_)) => RenderMode::Solid,
268                    (MaterialProfile::Lambert, RenderMode::BlinnPhong { light_dir, .. }) => {
269                        RenderMode::SolidLightDir(*light_dir)
270                    }
271                    _ => mode.clone(),
272                },
273            }
274        }
275    }
276
277    #[inline]
278    pub(crate) fn should_cull_mesh(&self, mesh: &K3dMesh) -> bool {
279        transform::should_cull_mesh(&self.camera, mesh)
280    }
281
282    #[inline(always)]
283    pub fn transform_point(
284        &self,
285        point: &[f32; 3],
286        model_matrix: Matrix4<f32>,
287    ) -> Option<Point3<i32>> {
288        transform::transform_point(&self.camera, self.width, self.height, point, model_matrix)
289    }
290
291    /// Project a 3D world position into 2D screen coordinates using the camera's view-projection matrix.
292    pub fn project_point(
293        &self,
294        point: Point3<f32>,
295    ) -> Option<embedded_graphics_core::geometry::Point> {
296        let arr = [point.x, point.y, point.z];
297        let pt3 = self.transform_point(&arr, self.camera.vp_matrix)?;
298        Some(embedded_graphics_core::geometry::Point::new(pt3.x, pt3.y))
299    }
300
301    #[inline(always)]
302    pub fn transform_points<const N: usize>(
303        &self,
304        indices: &[usize; N],
305        vertices: &[[f32; 3]],
306        model_matrix: Matrix4<f32>,
307    ) -> Option<[Point3<i32>; N]> {
308        transform::transform_points(
309            &self.camera,
310            self.width,
311            self.height,
312            indices,
313            vertices,
314            model_matrix,
315        )
316    }
317
318    #[inline(always)]
319    pub fn transform_points_with_w<const N: usize>(
320        &self,
321        indices: &[usize; N],
322        vertices: &[[f32; 3]],
323        model_matrix: Matrix4<f32>,
324    ) -> Option<([Point3<i32>; N], [f32; N])> {
325        transform::transform_points_with_w(
326            &self.camera,
327            self.width,
328            self.height,
329            indices,
330            vertices,
331            model_matrix,
332        )
333    }
334
335    #[inline]
336    pub(crate) fn is_backface(
337        &self,
338        face: &[usize; 3],
339        vertices: &[[f32; 3]],
340        model_matrix: Matrix4<f32>,
341        world_normal: &Vector3<f32>,
342    ) -> bool {
343        transform::is_backface(&self.camera, face, vertices, model_matrix, world_normal)
344    }
345
346    pub(crate) fn emit_clipped<F>(&self, clip: [Vector4<f32>; 3], color: Rgb565, callback: &mut F)
347    where
348        F: FnMut(DrawPrimitive),
349    {
350        transform::emit_clipped(
351            &self.camera,
352            self.width,
353            self.height,
354            self.vertex_snap_bits,
355            clip,
356            color,
357            callback,
358        );
359    }
360
361    #[allow(dead_code)]
362    pub(crate) fn render<'a, MS, F>(&self, meshes: MS, callback: F)
363    where
364        MS: IntoIterator<Item = &'a K3dMesh<'a>>,
365        F: FnMut(DrawPrimitive),
366    {
367        pipeline::render(self, meshes, callback);
368    }
369
370    pub fn record<'a, MS, const MAX: usize>(
371        &self,
372        meshes: MS,
373        commands: &mut CommandBuffer<MAX>,
374        telemetry: Option<&mut crate::telemetry::RecordTelemetry>,
375    ) -> Result<(), RenderError>
376    where
377        MS: IntoIterator<Item = &'a K3dMesh<'a>>,
378    {
379        recording::record(self, meshes, commands, telemetry)
380    }
381
382    pub fn record_drop_shadow<const MAX: usize>(
383        &self,
384        mesh: &K3dMesh,
385        floor_y: f32,
386        shadow_radius: f32,
387        max_fade_distance: f32,
388        shadow_opacity: u8,
389        color: Rgb565,
390        commands: &mut CommandBuffer<MAX>,
391    ) -> Result<(), RenderError> {
392        recording::record_drop_shadow(
393            self,
394            mesh,
395            floor_y,
396            shadow_radius,
397            max_fade_distance,
398            shadow_opacity,
399            color,
400            commands,
401        )
402    }
403
404    #[allow(dead_code)]
405    pub(crate) fn record_one_mesh<'a, const MAX: usize>(
406        &self,
407        mesh: &'a K3dMesh<'a>,
408        commands: &mut CommandBuffer<MAX>,
409        first_error: &mut Option<RenderError>,
410        visible_meshes: &mut usize,
411        used_texture_ids: &mut heapless::Vec<u32, 64>,
412    ) -> Result<(), RenderError> {
413        recording::record_one_mesh(
414            self,
415            mesh,
416            commands,
417            first_error,
418            visible_meshes,
419            used_texture_ids,
420        )
421    }
422
423    pub fn record_with_fallback<'a, MS, FS, const MAX: usize>(
424        &self,
425        primary: MS,
426        fallback: FS,
427        commands: &mut CommandBuffer<MAX>,
428        telemetry: Option<&mut crate::telemetry::RecordTelemetry>,
429    ) -> Result<BudgetFallbackOutcome, RenderError>
430    where
431        MS: IntoIterator<Item = &'a K3dMesh<'a>>,
432        FS: IntoIterator<Item = &'a K3dMesh<'a>>,
433    {
434        recording::record_with_fallback(self, primary, fallback, commands, telemetry)
435    }
436
437    pub fn record_with_degradation<'a, const MAX: usize>(
438        &mut self,
439        meshes: &[&'a K3dMesh<'a>],
440        commands: &mut CommandBuffer<MAX>,
441        policy: crate::config::DegradationPolicy<'_>,
442        telemetry: Option<&mut crate::telemetry::RecordTelemetry>,
443    ) -> Result<DegradationOutcome, RenderError> {
444        recording::record_with_degradation(self, meshes, commands, policy, telemetry)
445    }
446
447    pub fn execute<D, const MAX: usize>(
448        &self,
449        fb: &mut D,
450        frame: &mut crate::renderer::FrameCtx<'_>,
451        commands: &CommandBuffer<MAX>,
452        telemetry: Option<&mut crate::telemetry::ExecuteTelemetry>,
453    ) -> Result<Option<crate::renderer::DirtyRegion>, RenderError>
454    where
455        D: DrawTarget<Color = Rgb565> + OriginDimensions,
456        D::Error: Debug,
457    {
458        recording::execute(self, fb, frame, commands, telemetry)
459    }
460
461    #[cfg(feature = "textured")]
462    pub fn execute_with_textures<D, const MAX: usize, const N: usize>(
463        &self,
464        fb: &mut D,
465        frame: &mut crate::renderer::FrameCtx<'_>,
466        commands: &CommandBuffer<MAX>,
467        texture_manager: &crate::texture::TextureManager<N>,
468        telemetry: Option<&mut crate::telemetry::ExecuteTelemetry>,
469    ) -> Result<Option<crate::renderer::DirtyRegion>, RenderError>
470    where
471        D: DrawTarget<Color = Rgb565> + OriginDimensions,
472        D::Error: Debug,
473    {
474        recording::execute_with_textures(self, fb, frame, commands, texture_manager, telemetry)
475    }
476
477    pub fn execute_tiled<D, const MAX: usize, const BIN_CAP: usize>(
478        &self,
479        fb: &mut D,
480        frame: &mut crate::renderer::FrameCtx<'_>,
481        commands: &CommandBuffer<MAX>,
482        tile: crate::tilebin::TileConfig,
483    ) -> Result<crate::tilebin::TileBinStats, RenderError>
484    where
485        D: DrawTarget<Color = Rgb565> + OriginDimensions,
486        D::Error: Debug,
487    {
488        recording::execute_tiled::<D, MAX, BIN_CAP>(self, fb, frame, commands, tile)
489    }
490
491    #[cfg(feature = "gizmos")]
492    pub fn record_aabb_gizmo<const MAX: usize>(
493        &self,
494        aabb: &crate::bounds::Aabb,
495        model_matrix: &Matrix4<f32>,
496        color: Rgb565,
497        commands: &mut CommandBuffer<MAX>,
498    ) -> Result<(), RenderError> {
499        recording::record_aabb_gizmo(self, aabb, model_matrix, color, commands)
500    }
501
502    #[cfg(feature = "gizmos")]
503    pub fn record_frustum_gizmo<const MAX: usize>(
504        &self,
505        color: Rgb565,
506        commands: &mut CommandBuffer<MAX>,
507    ) -> Result<(), RenderError> {
508        recording::record_frustum_gizmo(self, color, commands)
509    }
510}