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 #[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 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}