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    #[inline]
178    pub(crate) fn light_tint_at(&self, world_pos: Point3<f32>) -> Rgb565 {
179        pipeline::light_tint_at(self, world_pos)
180    }
181
182    #[cfg(feature = "lighting")]
183    #[inline]
184    pub(crate) fn add_tint(base: Rgb565, tint: Rgb565) -> Rgb565 {
185        pipeline::add_tint(base, tint)
186    }
187
188    #[cfg(feature = "lighting")]
189    #[inline]
190    pub(crate) fn sector_shaded_color(
191        &self,
192        base: Rgb565,
193        brightness: u8,
194        face_center: Point3<f32>,
195    ) -> Rgb565 {
196        pipeline::sector_shaded_color(self, base, brightness, face_center)
197    }
198
199    #[cfg(feature = "lighting")]
200    #[inline]
201    pub(crate) fn face_world_center(
202        face: &[usize; 3],
203        vertices: &[[f32; 3]],
204        model_matrix: Matrix4<f32>,
205    ) -> Point3<f32> {
206        pipeline::face_world_center(face, vertices, model_matrix)
207    }
208
209    pub fn set_caps(&mut self, caps: crate::config::ProfileCaps) {
210        self.caps = Some(caps);
211        self.apply_render_defaults(crate::config::render_defaults_for_profile(caps));
212    }
213
214    pub fn clear_caps(&mut self) {
215        self.caps = None;
216    }
217
218    pub fn set_quality_tier(&mut self, tier: crate::config::QualityTier) {
219        self.quality_tier = tier;
220    }
221
222    pub fn set_material_profile(&mut self, profile: crate::config::MaterialProfile) {
223        self.material_profile = profile;
224    }
225
226    pub fn apply_render_defaults(&mut self, defaults: crate::config::RenderDefaults) {
227        self.quality_tier = defaults.quality_tier;
228        self.material_profile = defaults.material_profile;
229    }
230
231    pub(crate) fn resolve_render_mode(&self, mode: &RenderMode) -> RenderMode {
232        #[cfg(not(feature = "lighting"))]
233        {
234            let _ = self;
235            mode.clone()
236        }
237        #[cfg(feature = "lighting")]
238        {
239            use crate::config::{MaterialProfile, QualityTier};
240            match self.quality_tier {
241                QualityTier::Fastest => match mode {
242                    #[cfg(feature = "lighting")]
243                    RenderMode::BlinnPhong { .. }
244                    | RenderMode::GouraudLightDir(_)
245                    | RenderMode::Toon(_, _)
246                    | RenderMode::SolidLightDir(_) => RenderMode::Solid,
247                    _ => mode.clone(),
248                },
249                QualityTier::Balanced => match (self.material_profile, mode) {
250                    (MaterialProfile::Unlit, RenderMode::BlinnPhong { .. })
251                    | (MaterialProfile::Unlit, RenderMode::GouraudLightDir(_))
252                    | (MaterialProfile::Unlit, RenderMode::Toon(_, _))
253                    | (MaterialProfile::Unlit, RenderMode::SolidLightDir(_)) => RenderMode::Solid,
254                    (MaterialProfile::Lambert, RenderMode::BlinnPhong { light_dir, .. }) => {
255                        RenderMode::SolidLightDir(*light_dir)
256                    }
257                    _ => mode.clone(),
258                },
259                QualityTier::Quality => match (self.material_profile, mode) {
260                    (MaterialProfile::Unlit, RenderMode::BlinnPhong { .. })
261                    | (MaterialProfile::Unlit, RenderMode::GouraudLightDir(_))
262                    | (MaterialProfile::Unlit, RenderMode::Toon(_, _))
263                    | (MaterialProfile::Unlit, RenderMode::SolidLightDir(_)) => RenderMode::Solid,
264                    (MaterialProfile::Lambert, RenderMode::BlinnPhong { light_dir, .. }) => {
265                        RenderMode::SolidLightDir(*light_dir)
266                    }
267                    _ => mode.clone(),
268                },
269            }
270        }
271    }
272
273    #[inline]
274    pub(crate) fn should_cull_mesh(&self, mesh: &K3dMesh) -> bool {
275        transform::should_cull_mesh(&self.camera, mesh)
276    }
277
278    #[inline(always)]
279    pub fn transform_point(
280        &self,
281        point: &[f32; 3],
282        model_matrix: Matrix4<f32>,
283    ) -> Option<Point3<i32>> {
284        transform::transform_point(&self.camera, self.width, self.height, point, model_matrix)
285    }
286
287    /// Project a 3D world position into 2D screen coordinates using the camera's view-projection matrix.
288    pub fn project_point(
289        &self,
290        point: Point3<f32>,
291    ) -> Option<embedded_graphics_core::geometry::Point> {
292        let arr = [point.x, point.y, point.z];
293        let pt3 = self.transform_point(&arr, self.camera.vp_matrix)?;
294        Some(embedded_graphics_core::geometry::Point::new(pt3.x, pt3.y))
295    }
296
297    #[inline(always)]
298    pub fn transform_points<const N: usize>(
299        &self,
300        indices: &[usize; N],
301        vertices: &[[f32; 3]],
302        model_matrix: Matrix4<f32>,
303    ) -> Option<[Point3<i32>; N]> {
304        transform::transform_points(
305            &self.camera,
306            self.width,
307            self.height,
308            indices,
309            vertices,
310            model_matrix,
311        )
312    }
313
314    #[inline(always)]
315    pub fn transform_points_with_w<const N: usize>(
316        &self,
317        indices: &[usize; N],
318        vertices: &[[f32; 3]],
319        model_matrix: Matrix4<f32>,
320    ) -> Option<([Point3<i32>; N], [f32; N])> {
321        transform::transform_points_with_w(
322            &self.camera,
323            self.width,
324            self.height,
325            indices,
326            vertices,
327            model_matrix,
328        )
329    }
330
331    #[inline]
332    pub(crate) fn is_backface(
333        &self,
334        face: &[usize; 3],
335        vertices: &[[f32; 3]],
336        model_matrix: Matrix4<f32>,
337        world_normal: &Vector3<f32>,
338    ) -> bool {
339        transform::is_backface(&self.camera, face, vertices, model_matrix, world_normal)
340    }
341
342    pub(crate) fn emit_clipped<F>(&self, clip: [Vector4<f32>; 3], color: Rgb565, callback: &mut F)
343    where
344        F: FnMut(DrawPrimitive),
345    {
346        transform::emit_clipped(
347            &self.camera,
348            self.width,
349            self.height,
350            self.vertex_snap_bits,
351            clip,
352            color,
353            callback,
354        );
355    }
356
357    #[allow(dead_code)]
358    pub(crate) fn render<'a, MS, F>(&self, meshes: MS, callback: F)
359    where
360        MS: IntoIterator<Item = &'a K3dMesh<'a>>,
361        F: FnMut(DrawPrimitive),
362    {
363        pipeline::render(self, meshes, callback);
364    }
365
366    pub fn record<'a, MS, const MAX: usize>(
367        &self,
368        meshes: MS,
369        commands: &mut CommandBuffer<MAX>,
370        telemetry: Option<&mut crate::telemetry::RecordTelemetry>,
371    ) -> Result<(), RenderError>
372    where
373        MS: IntoIterator<Item = &'a K3dMesh<'a>>,
374    {
375        recording::record(self, meshes, commands, telemetry)
376    }
377
378    pub fn record_drop_shadow<const MAX: usize>(
379        &self,
380        mesh: &K3dMesh,
381        floor_y: f32,
382        shadow_radius: f32,
383        max_fade_distance: f32,
384        shadow_opacity: u8,
385        color: Rgb565,
386        commands: &mut CommandBuffer<MAX>,
387    ) -> Result<(), RenderError> {
388        recording::record_drop_shadow(
389            self,
390            mesh,
391            floor_y,
392            shadow_radius,
393            max_fade_distance,
394            shadow_opacity,
395            color,
396            commands,
397        )
398    }
399
400    #[allow(dead_code)]
401    pub(crate) fn record_one_mesh<'a, const MAX: usize>(
402        &self,
403        mesh: &'a K3dMesh<'a>,
404        commands: &mut CommandBuffer<MAX>,
405        first_error: &mut Option<RenderError>,
406        visible_meshes: &mut usize,
407        used_texture_ids: &mut heapless::Vec<u32, 64>,
408    ) -> Result<(), RenderError> {
409        recording::record_one_mesh(
410            self,
411            mesh,
412            commands,
413            first_error,
414            visible_meshes,
415            used_texture_ids,
416        )
417    }
418
419    pub fn record_with_fallback<'a, MS, FS, const MAX: usize>(
420        &self,
421        primary: MS,
422        fallback: FS,
423        commands: &mut CommandBuffer<MAX>,
424        telemetry: Option<&mut crate::telemetry::RecordTelemetry>,
425    ) -> Result<BudgetFallbackOutcome, RenderError>
426    where
427        MS: IntoIterator<Item = &'a K3dMesh<'a>>,
428        FS: IntoIterator<Item = &'a K3dMesh<'a>>,
429    {
430        recording::record_with_fallback(self, primary, fallback, commands, telemetry)
431    }
432
433    pub fn record_with_degradation<'a, const MAX: usize>(
434        &mut self,
435        meshes: &[&'a K3dMesh<'a>],
436        commands: &mut CommandBuffer<MAX>,
437        policy: crate::config::DegradationPolicy<'_>,
438        telemetry: Option<&mut crate::telemetry::RecordTelemetry>,
439    ) -> Result<DegradationOutcome, RenderError> {
440        recording::record_with_degradation(self, meshes, commands, policy, telemetry)
441    }
442
443    pub fn execute<D, const MAX: usize>(
444        &self,
445        fb: &mut D,
446        frame: &mut crate::renderer::FrameCtx<'_>,
447        commands: &CommandBuffer<MAX>,
448        telemetry: Option<&mut crate::telemetry::ExecuteTelemetry>,
449    ) -> Result<Option<crate::renderer::DirtyRegion>, RenderError>
450    where
451        D: DrawTarget<Color = Rgb565> + OriginDimensions,
452        D::Error: Debug,
453    {
454        recording::execute(self, fb, frame, commands, telemetry)
455    }
456
457    #[cfg(feature = "textured")]
458    pub fn execute_with_textures<D, const MAX: usize, const N: usize>(
459        &self,
460        fb: &mut D,
461        frame: &mut crate::renderer::FrameCtx<'_>,
462        commands: &CommandBuffer<MAX>,
463        texture_manager: &crate::texture::TextureManager<N>,
464        telemetry: Option<&mut crate::telemetry::ExecuteTelemetry>,
465    ) -> Result<Option<crate::renderer::DirtyRegion>, RenderError>
466    where
467        D: DrawTarget<Color = Rgb565> + OriginDimensions,
468        D::Error: Debug,
469    {
470        recording::execute_with_textures(self, fb, frame, commands, texture_manager, telemetry)
471    }
472
473    pub fn execute_tiled<D, const MAX: usize, const BIN_CAP: usize>(
474        &self,
475        fb: &mut D,
476        frame: &mut crate::renderer::FrameCtx<'_>,
477        commands: &CommandBuffer<MAX>,
478        tile: crate::tilebin::TileConfig,
479    ) -> Result<crate::tilebin::TileBinStats, RenderError>
480    where
481        D: DrawTarget<Color = Rgb565> + OriginDimensions,
482        D::Error: Debug,
483    {
484        recording::execute_tiled::<D, MAX, BIN_CAP>(self, fb, frame, commands, tile)
485    }
486
487    #[cfg(feature = "gizmos")]
488    pub fn record_aabb_gizmo<const MAX: usize>(
489        &self,
490        aabb: &crate::bounds::Aabb,
491        model_matrix: &Matrix4<f32>,
492        color: Rgb565,
493        commands: &mut CommandBuffer<MAX>,
494    ) -> Result<(), RenderError> {
495        recording::record_aabb_gizmo(self, aabb, model_matrix, color, commands)
496    }
497
498    #[cfg(feature = "gizmos")]
499    pub fn record_frustum_gizmo<const MAX: usize>(
500        &self,
501        color: Rgb565,
502        commands: &mut CommandBuffer<MAX>,
503    ) -> Result<(), RenderError> {
504        recording::record_frustum_gizmo(self, color, commands)
505    }
506}