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 pub(crate) fog: Option<crate::draw::FogConfig>,
26 pub(crate) dither: Option<crate::draw::DitherConfig>,
28 pub(crate) vertex_snap_bits: u8,
30 pub(crate) texture_mapping: crate::retro::TextureMapping,
32 pub(crate) light_levels: crate::retro::LightLevels,
34 pub(crate) stipple_mode: crate::retro::StippleMode,
36 pub(crate) screen_tint: Option<crate::retro::ScreenTint>,
38 pub(crate) palette_mode: crate::retro::PaletteMode,
40 pub(crate) sky: Option<crate::retro::SkyConfig>,
42 #[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 pub fn set_fog(&mut self, fog: crate::draw::FogConfig) {
88 self.fog = Some(fog);
89 }
90
91 pub fn clear_fog(&mut self) {
93 self.fog = None;
94 }
95
96 pub fn set_dither(&mut self, dither: crate::draw::DitherConfig) {
98 self.dither = Some(dither);
99 }
100
101 pub fn clear_dither(&mut self) {
103 self.dither = None;
104 }
105
106 pub fn set_vertex_snap_bits(&mut self, bits: u8) {
108 self.vertex_snap_bits = bits.min(16);
109 }
110
111 pub fn set_texture_mapping(&mut self, mapping: crate::retro::TextureMapping) {
113 self.texture_mapping = mapping;
114 }
115
116 pub fn set_light_levels(&mut self, levels: crate::retro::LightLevels) {
118 self.light_levels = levels;
119 }
120
121 pub fn set_stipple_mode(&mut self, mode: crate::retro::StippleMode) {
123 self.stipple_mode = mode;
124 }
125
126 pub fn set_screen_tint(&mut self, tint: crate::retro::ScreenTint) {
128 self.screen_tint = Some(tint);
129 }
130
131 pub fn clear_screen_tint(&mut self) {
133 self.screen_tint = None;
134 }
135
136 pub fn set_palette_mode(&mut self, mode: crate::retro::PaletteMode) {
138 self.palette_mode = mode;
139 }
140
141 pub fn set_sky(&mut self, sky: crate::retro::SkyConfig) {
143 self.sky = Some(sky);
144 }
145
146 pub fn clear_sky(&mut self) {
148 self.sky = None;
149 }
150
151 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 #[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 #[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 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}