1#![no_std]
2#![allow(
3 clippy::too_many_arguments,
4 clippy::type_complexity,
5 clippy::needless_range_loop,
6 clippy::result_unit_err,
7 clippy::manual_div_ceil,
8 clippy::manual_is_multiple_of,
9 clippy::explicit_counter_loop,
10 clippy::needless_return,
11 clippy::useless_conversion,
12 clippy::manual_checked_ops,
13 clippy::collapsible_if,
14 clippy::new_without_default,
15 clippy::doc_overindented_list_items,
16 clippy::unnecessary_cast
17)]
18#[cfg(feature = "std")]
19extern crate std;
20
21#[cfg(feature = "depth-u16")]
22pub type ZDepth = u16;
23#[cfg(feature = "depth-u16")]
24pub const Z_MAX_VALUE: ZDepth = u16::MAX;
25#[cfg(feature = "depth-u16")]
26pub const DEPTH_EPSILON: ZDepth = 1;
27
28#[cfg(not(feature = "depth-u16"))]
29pub type ZDepth = u32;
30#[cfg(not(feature = "depth-u16"))]
31pub const Z_MAX_VALUE: ZDepth = u32::MAX;
32#[cfg(not(feature = "depth-u16"))]
33pub const DEPTH_EPSILON: ZDepth = 128;
34
35#[inline(always)]
36pub const fn to_zdepth(z: u32) -> ZDepth {
37 #[cfg(feature = "depth-u16")]
38 {
39 (z >> 16) as u16
40 }
41 #[cfg(not(feature = "depth-u16"))]
42 {
43 z
44 }
45}
46
47#[cfg(feature = "dma2d")]
48unsafe extern "Rust" {
49 #[cfg(feature = "depth-u16")]
50 fn dma2d_clear_zbuffer_u16(ptr: *mut u16, len: usize, value: u16);
51 #[cfg(not(feature = "depth-u16"))]
52 fn dma2d_clear_zbuffer_u32(ptr: *mut u32, len: usize, value: u32);
53}
54
55#[inline(always)]
56pub fn clear_zbuffer(zbuffer: &mut [ZDepth], value: ZDepth) {
57 #[cfg(feature = "dma2d")]
58 {
59 #[cfg(feature = "depth-u16")]
60 unsafe {
61 dma2d_clear_zbuffer_u16(zbuffer.as_mut_ptr(), zbuffer.len(), value);
62 }
63 #[cfg(not(feature = "depth-u16"))]
64 unsafe {
65 dma2d_clear_zbuffer_u32(zbuffer.as_mut_ptr(), zbuffer.len(), value);
66 }
67 }
68 #[cfg(not(feature = "dma2d"))]
69 {
70 zbuffer.fill(value);
71 }
72}
73
74#[allow(unused_imports)]
75use crate::mesh::K3dMesh;
76#[allow(unused_imports)]
77use nalgebra::Vector4;
78use nalgebra::{Matrix4, Point3, Vector3};
79
80#[allow(unused_imports)]
83use nalgebra::ComplexField;
84
85pub mod animation;
86#[cfg(feature = "scene")]
87pub mod billboard;
88pub mod engine;
89pub mod primitive;
90
91#[derive(Debug, Clone, Copy)]
93pub struct MeshRayCastHit {
94 pub distance: f32,
96 pub point: Vector3<f32>,
98 pub normal: Vector3<f32>,
100 pub face_index: usize,
102 pub uv: [f32; 2],
104}
105
106#[cfg(feature = "aabb-cull")]
107pub mod bounds;
108pub mod bridge;
109#[cfg(feature = "raycast")]
110pub mod bsp;
111pub mod camera;
112pub mod camera_controller;
113#[cfg(feature = "scene")]
114pub mod character;
115pub mod color;
116pub mod command_buffer;
117pub mod completion;
118pub mod config;
119pub mod display_backend;
120pub mod dither;
121pub mod draw;
122#[cfg(feature = "embassy")]
123pub mod embassy;
124pub mod error;
125#[cfg(feature = "gizmos")]
126pub mod gizmos;
127pub mod hardware_profile;
128#[cfg(feature = "hud")]
129pub mod hud;
130pub mod input;
131pub mod lens_flare;
132#[cfg(feature = "lighting")]
133pub mod lights;
134pub mod lod;
135pub mod matcap;
136pub mod mesh;
137#[cfg(feature = "painters")]
138pub mod painters;
139#[cfg(feature = "scene")]
140pub mod particles;
141#[cfg(feature = "perfcounter")]
142pub mod perfcounter;
143#[cfg(feature = "physics")]
144pub mod physics;
145pub mod raster;
146pub mod ray_primitive;
147pub mod raycast;
148#[cfg(feature = "render-layers")]
149pub mod render_layers;
150pub mod renderer;
151#[cfg(feature = "gizmos")]
152pub mod simplex_stroke_font;
153pub mod retro;
156pub mod shader;
157pub mod shapes;
158pub mod simd_dsp;
159
160#[cfg(feature = "anim-blend")]
161pub mod absm;
162pub mod color_gradient;
163pub mod curve;
164pub mod decal;
165pub mod navmesh;
166pub mod pool;
167
168pub mod occlusion;
169#[cfg(feature = "scene")]
170pub mod scene_format;
171#[cfg(feature = "scene")]
172pub mod scene_stream;
173#[cfg(feature = "raycast")]
174pub mod sector_lights;
175#[cfg(feature = "scene")]
176pub mod skeleton;
177#[cfg(feature = "physics")]
178pub mod softbody;
179pub mod state_machine;
180pub mod swapchain;
181pub mod telemetry;
182#[cfg(feature = "textured")]
183pub mod texture;
184pub mod tilebin;
185pub mod timer;
186#[cfg(feature = "scene")]
187pub mod transform_anim;
188#[cfg(feature = "scene")]
189pub mod tween;
190pub mod view_frustum;
191
192pub fn mesh_ray_cast(
197 ray_origin: Vector3<f32>,
198 ray_dir: Vector3<f32>,
199 geometry: &mesh::Geometry<'_>,
200 model_matrix: &Matrix4<f32>,
201 max_distance: f32,
202) -> Option<MeshRayCastHit> {
203 #[cfg(feature = "aabb-cull")]
204 {
205 return mesh_ray_cast_bounded(
206 ray_origin,
207 ray_dir,
208 geometry,
209 model_matrix,
210 max_distance,
211 None,
212 );
213 }
214 #[cfg(not(feature = "aabb-cull"))]
215 {
216 mesh_ray_cast_world(ray_origin, ray_dir, geometry, model_matrix, max_distance)
217 }
218}
219
220#[cfg(not(feature = "aabb-cull"))]
221fn mesh_ray_cast_world(
222 ray_origin: Vector3<f32>,
223 ray_dir: Vector3<f32>,
224 geometry: &mesh::Geometry<'_>,
225 model_matrix: &Matrix4<f32>,
226 max_distance: f32,
227) -> Option<MeshRayCastHit> {
228 let mut nearest: Option<MeshRayCastHit> = None;
229 let mut min_dist = max_distance;
230
231 for (face_index, face) in geometry.faces.iter().enumerate() {
232 let raw_v0 = geometry.vertices[face[0]];
233 let raw_v1 = geometry.vertices[face[1]];
234 let raw_v2 = geometry.vertices[face[2]];
235
236 let v0 = model_matrix
237 .transform_point(&Point3::new(raw_v0[0], raw_v0[1], raw_v0[2]))
238 .coords;
239 let v1 = model_matrix
240 .transform_point(&Point3::new(raw_v1[0], raw_v1[1], raw_v1[2]))
241 .coords;
242 let v2 = model_matrix
243 .transform_point(&Point3::new(raw_v2[0], raw_v2[1], raw_v2[2]))
244 .coords;
245
246 let edge1 = v1 - v0;
247 let edge2 = v2 - v0;
248 let h = ray_dir.cross(&edge2);
249 let det = edge1.dot(&h);
250 if det.abs() < 1e-6 {
251 continue;
252 }
253 let inv_det = 1.0 / det;
254 let s = ray_origin - v0;
255 let bary_u = inv_det * s.dot(&h);
256 if !(0.0..=1.0).contains(&bary_u) {
257 continue;
258 }
259 let q = s.cross(&edge1);
260 let bary_v = inv_det * ray_dir.dot(&q);
261 if bary_v < 0.0 || bary_u + bary_v > 1.0 {
262 continue;
263 }
264 let t = inv_det * edge2.dot(&q);
265 if t <= 0.0 || t >= min_dist {
266 continue;
267 }
268 let normal = edge1.cross(&edge2).normalize();
269 let bary_w = 1.0 - bary_u - bary_v;
270 let uv = if geometry.uvs.len() > face[0]
271 && geometry.uvs.len() > face[1]
272 && geometry.uvs.len() > face[2]
273 {
274 let uv0 = geometry.uvs[face[0]];
275 let uv1 = geometry.uvs[face[1]];
276 let uv2 = geometry.uvs[face[2]];
277 [
278 bary_w * uv0[0] + bary_u * uv1[0] + bary_v * uv2[0],
279 bary_w * uv0[1] + bary_u * uv1[1] + bary_v * uv2[1],
280 ]
281 } else {
282 [0.0, 0.0]
283 };
284 let point = ray_origin + ray_dir * t;
285 min_dist = t;
286 nearest = Some(MeshRayCastHit {
287 distance: t,
288 point,
289 normal,
290 face_index,
291 uv,
292 });
293 }
294 nearest
295}
296
297#[cfg(feature = "aabb-cull")]
300pub fn mesh_ray_cast_bounded(
301 ray_origin: Vector3<f32>,
302 ray_dir: Vector3<f32>,
303 geometry: &mesh::Geometry<'_>,
304 model_matrix: &Matrix4<f32>,
305 max_distance: f32,
306 model_aabb: Option<&bounds::Aabb>,
307) -> Option<MeshRayCastHit> {
308 let inv = model_matrix.try_inverse()?;
309 let origin4 = inv * Vector4::new(ray_origin.x, ray_origin.y, ray_origin.z, 1.0);
310 let dir4 = inv * Vector4::new(ray_dir.x, ray_dir.y, ray_dir.z, 0.0);
311 if origin4.w.abs() < 1e-8 {
312 return None;
313 }
314 let local_origin = Vector3::new(origin4.x, origin4.y, origin4.z) / origin4.w;
315 let local_dir = Vector3::new(dir4.x, dir4.y, dir4.z);
316 let dir_len = local_dir.norm();
317 if dir_len < 1e-8 {
318 return None;
319 }
320 let local_dir_n = local_dir / dir_len;
321 let local_max = max_distance * dir_len;
322
323 if let Some(aabb) = model_aabb
324 && aabb
325 .intersect_ray(local_origin, local_dir_n, local_max)
326 .is_none()
327 {
328 return None;
329 }
330
331 let mut nearest: Option<MeshRayCastHit> = None;
332 let mut min_dist = local_max;
333
334 for (face_index, face) in geometry.faces.iter().enumerate() {
335 let v0 = Vector3::new(
336 geometry.vertices[face[0]][0],
337 geometry.vertices[face[0]][1],
338 geometry.vertices[face[0]][2],
339 );
340 let v1 = Vector3::new(
341 geometry.vertices[face[1]][0],
342 geometry.vertices[face[1]][1],
343 geometry.vertices[face[1]][2],
344 );
345 let v2 = Vector3::new(
346 geometry.vertices[face[2]][0],
347 geometry.vertices[face[2]][1],
348 geometry.vertices[face[2]][2],
349 );
350
351 let edge1 = v1 - v0;
352 let edge2 = v2 - v0;
353 let h = local_dir_n.cross(&edge2);
354 let det = edge1.dot(&h);
355 if det.abs() < 1e-6 {
356 continue;
357 }
358 let inv_det = 1.0 / det;
359 let s = local_origin - v0;
360 let bary_u = inv_det * s.dot(&h);
361 if !(0.0..=1.0).contains(&bary_u) {
362 continue;
363 }
364 let q = s.cross(&edge1);
365 let bary_v = inv_det * local_dir_n.dot(&q);
366 if bary_v < 0.0 || bary_u + bary_v > 1.0 {
367 continue;
368 }
369 let t_local = inv_det * edge2.dot(&q);
370 if t_local <= 0.0 || t_local >= min_dist {
371 continue;
372 }
373
374 let normal_local = edge1.cross(&edge2).normalize();
375 let rot = model_matrix.fixed_view::<3, 3>(0, 0);
376 let normal = (rot * normal_local).normalize();
377
378 let bary_w = 1.0 - bary_u - bary_v;
379 let uv = if geometry.uvs.len() > face[0]
380 && geometry.uvs.len() > face[1]
381 && geometry.uvs.len() > face[2]
382 {
383 let uv0 = geometry.uvs[face[0]];
384 let uv1 = geometry.uvs[face[1]];
385 let uv2 = geometry.uvs[face[2]];
386 [
387 bary_w * uv0[0] + bary_u * uv1[0] + bary_v * uv2[0],
388 bary_w * uv0[1] + bary_u * uv1[1] + bary_v * uv2[1],
389 ]
390 } else {
391 [0.0, 0.0]
392 };
393
394 let local_hit = local_origin + local_dir_n * t_local;
395 let world_hit = model_matrix
396 .transform_point(&Point3::from(local_hit))
397 .coords;
398 let world_t = (world_hit - ray_origin).norm();
399 if world_t >= max_distance {
400 continue;
401 }
402
403 min_dist = t_local;
404 nearest = Some(MeshRayCastHit {
405 distance: world_t,
406 point: world_hit,
407 normal,
408 face_index,
409 uv,
410 });
411 }
412
413 nearest
414}
415
416#[cfg(feature = "aabb-cull")]
418pub fn mesh_ray_cast_mesh(
419 ray_origin: Vector3<f32>,
420 ray_dir: Vector3<f32>,
421 mesh: &K3dMesh<'_>,
422 max_distance: f32,
423) -> Option<MeshRayCastHit> {
424 let distance = (mesh.get_position() - Point3::from(ray_origin)).norm();
425 let geometry = mesh.select_lod(distance);
426 let aabb = mesh.model_aabb();
427 mesh_ray_cast_bounded(
428 ray_origin,
429 ray_dir,
430 geometry,
431 &mesh.model_matrix,
432 max_distance,
433 Some(&aabb),
434 )
435}
436
437#[cfg(feature = "dma2d")]
438mod dma2d_stubs {
439 #[cfg(feature = "depth-u16")]
440 #[unsafe(no_mangle)]
441 extern "Rust" fn dma2d_clear_zbuffer_u16(ptr: *mut u16, len: usize, value: u16) {
442 let slice = unsafe { core::slice::from_raw_parts_mut(ptr, len) };
443 slice.fill(value);
444 }
445
446 #[cfg(not(feature = "depth-u16"))]
447 #[unsafe(no_mangle)]
448 extern "Rust" fn dma2d_clear_zbuffer_u32(ptr: *mut u32, len: usize, value: u32) {
449 let slice = unsafe { core::slice::from_raw_parts_mut(ptr, len) };
450 slice.fill(value);
451 }
452}
453
454#[cfg(test)]
455mod tests {
456 extern crate std;
457
458 use super::*;
459 use crate::engine::K3dengine;
460 use crate::primitive::DrawPrimitive;
461
462 #[test]
463 fn test_engine_creation() {
464 let engine = K3dengine::new(640, 480);
465 assert_eq!(engine.width, 640);
466 assert_eq!(engine.height, 480);
467 assert!((engine.camera.get_aspect_ratio() - 640.0 / 480.0).abs() < 0.001);
468 }
469
470 #[test]
471 fn test_transform_point_basic() {
472 let engine = K3dengine::new(640, 480);
473 let transform_matrix = engine.camera.vp_matrix;
475
476 let point = [0.0, 0.0, -5.0];
479 let result = engine.transform_point(&point, transform_matrix);
480
481 if let Some(transformed) = result {
482 assert!(transformed.x >= 0 && transformed.x < 640);
484 assert!(transformed.y >= 0 && transformed.y < 480);
485 }
486 }
488
489 #[test]
490 fn test_transform_point_clamps_out_of_bounds() {
491 let engine = K3dengine::new(640, 480);
492 let model_matrix = nalgebra::Matrix4::identity();
493
494 let point = [100.0, 100.0, -5.0];
496 let result = engine.transform_point(&point, model_matrix);
497 assert!(result.is_none());
499 }
500
501 #[test]
502 fn test_transform_point_behind_camera() {
503 let engine = K3dengine::new(640, 480);
504 let transform_matrix = engine.camera.vp_matrix;
505
506 let point = [0.0, 0.0, 1.0];
508 let _result = engine.transform_point(&point, transform_matrix);
509 }
513
514 #[test]
515 fn test_transform_point_near_plane_clipping() {
516 let engine = K3dengine::new(640, 480);
517 let transform_matrix = engine.camera.vp_matrix;
523
524 let point = [0.0, 0.0, -0.1];
526 let result = engine.transform_point(&point, transform_matrix);
527 assert!(result.is_none());
528 }
529
530 #[test]
531 fn test_transform_point_far_plane_clipping() {
532 let engine = K3dengine::new(640, 480);
533 let transform_matrix = engine.camera.vp_matrix;
534
535 let point = [0.0, 0.0, -1000.0];
537 let result = engine.transform_point(&point, transform_matrix);
538 assert!(result.is_none());
539 }
540
541 #[test]
542 fn test_transform_point_within_near_far_not_culled() {
543 let engine = K3dengine::new(640, 480);
549 let transform_matrix = engine.camera.vp_matrix;
550
551 let point = [0.0, 0.0, -0.8];
552 let result = engine.transform_point(&point, transform_matrix);
553 assert!(
554 result.is_some(),
555 "a point at distance 0.8 (within [near=0.4, far=20]) should not be culled"
556 );
557 }
558
559 #[test]
560 fn test_transform_points_array() {
561 let engine = K3dengine::new(640, 480);
562 let transform_matrix = engine.camera.vp_matrix;
563
564 let vertices = [[0.0, 0.0, -5.0], [0.1, 0.0, -5.0], [0.0, 0.1, -5.0]];
565 let indices = [0, 1, 2];
566
567 let result = engine.transform_points(&indices, &vertices, transform_matrix);
568
569 if let Some(points) = result {
571 assert_eq!(points.len(), 3);
572 }
573 }
575
576 #[test]
577 fn test_render_empty_faces_mesh() {
578 let engine = K3dengine::new(640, 480);
579 let vertices = [[0.0, 0.0, -5.0]]; let geometry = mesh::Geometry {
581 vertices: &vertices,
582 faces: &[],
583 colors: &[],
584 lines: &[],
585 normals: &[],
586 vertex_normals: &[],
587 uvs: &[],
588 texture_id: None,
589 };
590 let mesh = mesh::K3dMesh::new(geometry);
591
592 let mut callback_count = 0;
593 engine.render(std::iter::once(&mesh), |_| {
594 callback_count += 1;
595 });
596
597 assert!(callback_count > 0);
599 }
600
601 #[test]
602 fn test_render_points_mode() {
603 let engine = K3dengine::new(640, 480);
604
605 let vertices = [[0.0, 0.0, -5.0], [0.5, 0.0, -5.0]];
606
607 let geometry = mesh::Geometry {
608 vertices: &vertices,
609 faces: &[],
610 colors: &[],
611 lines: &[],
612 normals: &[],
613 vertex_normals: &[],
614 uvs: &[],
615 texture_id: None,
616 };
617
618 let mut mesh = mesh::K3dMesh::new(geometry);
619 mesh.set_render_mode(mesh::RenderMode::Points);
620
621 let mut primitives = std::vec::Vec::new();
622 engine.render(std::iter::once(&mesh), |prim| {
623 primitives.push(prim);
624 });
625
626 assert!(!primitives.is_empty());
628 for prim in primitives {
629 assert!(matches!(prim, DrawPrimitive::ColoredPoint(_, _)));
630 }
631 }
632
633 #[test]
634 fn test_render_lines_mode_with_faces() {
635 let engine = K3dengine::new(640, 480);
636
637 let vertices = [[0.0, 0.0, -5.0], [0.5, 0.0, -5.0], [0.0, 0.5, -5.0]];
638
639 let faces = [[0, 1, 2]];
640
641 let geometry = mesh::Geometry {
642 vertices: &vertices,
643 faces: &faces,
644 colors: &[],
645 lines: &[],
646 normals: &[],
647 vertex_normals: &[],
648 uvs: &[],
649 texture_id: None,
650 };
651
652 let mut mesh = mesh::K3dMesh::new(geometry);
653 mesh.set_render_mode(mesh::RenderMode::Lines);
654
655 let mut primitives = std::vec::Vec::new();
656 engine.render(std::iter::once(&mesh), |prim| {
657 primitives.push(prim);
658 });
659
660 assert_eq!(primitives.len(), 3);
662 for prim in primitives {
663 assert!(matches!(prim, DrawPrimitive::Line(_, _)));
664 }
665 }
666
667 #[test]
668 #[cfg(feature = "lighting")]
669 fn test_render_gouraud_light_dir() {
670 let mut engine = K3dengine::new(640, 480);
671 engine.camera.set_position(Point3::new(0.0, 0.0, -10.0));
672 engine.camera.set_target(Point3::new(0.0, 0.0, 0.0));
673
674 let vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
675 let faces = [[0, 1, 2]];
676 let normals = [[0.0, 0.0, -1.0]]; let vertex_normals = [[0.0, 0.0, -1.0], [0.0, 0.0, -1.0], [0.0, 0.0, -1.0]];
678
679 let geometry = mesh::Geometry {
680 vertices: &vertices,
681 faces: &faces,
682 colors: &[],
683 lines: &[],
684 normals: &normals,
685 vertex_normals: &vertex_normals,
686 uvs: &[],
687 texture_id: None,
688 };
689
690 let mut mesh = mesh::K3dMesh::new(geometry);
691 mesh.set_render_mode(mesh::RenderMode::GouraudLightDir(Vector3::new(
692 0.0, 0.0, 1.0,
693 )));
694
695 let mut primitives = std::vec::Vec::new();
696 engine.render(std::iter::once(&mesh), |prim| {
697 primitives.push(prim);
698 });
699
700 assert!(!primitives.is_empty());
702 for prim in &primitives {
703 assert!(matches!(
704 prim,
705 DrawPrimitive::GouraudTriangleWithDepth { .. }
706 ));
707 }
708 }
709
710 #[test]
717 fn test_solid_inward_normals_interior_camera() {
718 let mut engine = K3dengine::new(320, 240);
719 engine
721 .camera
722 .set_position(nalgebra::Point3::new(0.0, 0.0, 0.0));
723 engine
724 .camera
725 .set_target(nalgebra::Point3::new(0.0, 0.0, -1.0));
726
727 #[rustfmt::skip]
730 let vertices: &[[f32; 3]] = &[
731 [-1.0, -1.0, -2.0],
732 [ 1.0, -1.0, -2.0],
733 [ 1.0, 1.0, -2.0],
734 [-1.0, 1.0, -2.0],
735 ];
736 let faces: &[[usize; 3]] = &[[0, 1, 2], [0, 2, 3]];
737 let normals: &[[f32; 3]] = &[[0.0, 0.0, 1.0], [0.0, 0.0, 1.0]]; let geometry = mesh::Geometry {
740 vertices,
741 faces,
742 normals,
743 colors: &[],
744 lines: &[],
745 vertex_normals: &[],
746 uvs: &[],
747 texture_id: None,
748 };
749 let mut m = mesh::K3dMesh::new(geometry);
750 m.set_render_mode(mesh::RenderMode::Solid);
751
752 let mut count = 0usize;
753 engine.render(std::iter::once(&m), |_| count += 1);
754
755 assert!(
756 count > 0,
757 "interior Solid-with-inward-normals emitted 0 primitives — culling is wrong"
758 );
759 }
760}