1#![no_std]
2#[cfg(feature = "std")]
3extern crate std;
4
5#[cfg(feature = "depth-u16")]
6pub type ZDepth = u16;
7#[cfg(feature = "depth-u16")]
8pub const Z_MAX_VALUE: ZDepth = u16::MAX;
9#[cfg(feature = "depth-u16")]
10pub const DEPTH_EPSILON: ZDepth = 1;
11
12#[cfg(not(feature = "depth-u16"))]
13pub type ZDepth = u32;
14#[cfg(not(feature = "depth-u16"))]
15pub const Z_MAX_VALUE: ZDepth = u32::MAX;
16#[cfg(not(feature = "depth-u16"))]
17pub const DEPTH_EPSILON: ZDepth = 128;
18
19#[inline(always)]
20pub const fn to_zdepth(z: u32) -> ZDepth {
21 #[cfg(feature = "depth-u16")]
22 {
23 (z >> 16) as u16
24 }
25 #[cfg(not(feature = "depth-u16"))]
26 {
27 z
28 }
29}
30
31#[cfg(feature = "dma2d")]
32unsafe extern "Rust" {
33 #[cfg(feature = "depth-u16")]
34 fn dma2d_clear_zbuffer_u16(ptr: *mut u16, len: usize, value: u16);
35 #[cfg(not(feature = "depth-u16"))]
36 fn dma2d_clear_zbuffer_u32(ptr: *mut u32, len: usize, value: u32);
37}
38
39#[inline(always)]
40pub fn clear_zbuffer(zbuffer: &mut [ZDepth], value: ZDepth) {
41 #[cfg(feature = "dma2d")]
42 {
43 #[cfg(feature = "depth-u16")]
44 unsafe {
45 dma2d_clear_zbuffer_u16(zbuffer.as_mut_ptr(), zbuffer.len(), value);
46 }
47 #[cfg(not(feature = "depth-u16"))]
48 unsafe {
49 dma2d_clear_zbuffer_u32(zbuffer.as_mut_ptr(), zbuffer.len(), value);
50 }
51 }
52 #[cfg(not(feature = "dma2d"))]
53 {
54 zbuffer.fill(value);
55 }
56}
57
58use mesh::K3dMesh;
59use nalgebra::{Matrix4, Point3, Vector3, Vector4};
60
61#[allow(unused_imports)]
64use nalgebra::ComplexField;
65
66pub mod animation;
67#[cfg(feature = "scene")]
68pub mod billboard;
69pub mod engine;
70pub mod primitive;
71
72pub use engine::{BudgetFallbackOutcome, DegradationOutcome, K3dengine};
73pub use primitive::DrawPrimitive;
74
75#[derive(Debug, Clone, Copy)]
77pub struct MeshRayCastHit {
78 pub distance: f32,
80 pub point: Vector3<f32>,
82 pub normal: Vector3<f32>,
84 pub face_index: usize,
86 pub uv: [f32; 2],
88}
89
90#[cfg(feature = "aabb-cull")]
91pub mod bounds;
92pub mod bridge;
93#[cfg(feature = "raycast")]
94pub mod bsp;
95pub mod camera;
96#[cfg(feature = "scene")]
97pub mod character;
98pub mod command_buffer;
99pub mod completion;
100pub mod config;
101pub mod display_backend;
102pub mod draw;
103#[cfg(feature = "embassy")]
104pub mod embassy;
105pub mod error;
106#[cfg(feature = "gizmos")]
107pub mod gizmos;
108pub mod hardware_profile;
109#[cfg(feature = "hud")]
110pub mod hud;
111pub mod input;
112#[cfg(feature = "lighting")]
113pub mod lights;
114pub mod mesh;
115#[cfg(feature = "painters")]
116pub mod painters;
117#[cfg(feature = "scene")]
118pub mod particles;
119#[cfg(feature = "perfcounter")]
120pub mod perfcounter;
121#[cfg(feature = "physics")]
122pub mod physics;
123pub mod raster;
124pub mod raycast;
125#[cfg(feature = "render-layers")]
126pub mod render_layers;
127pub mod renderer;
128pub mod retro;
131pub mod shader;
132
133#[cfg(feature = "scene")]
134pub mod scene_format;
135#[cfg(feature = "scene")]
136pub mod scene_stream;
137#[cfg(feature = "raycast")]
138pub mod sector_lights;
139#[cfg(feature = "scene")]
140pub mod skeleton;
141#[cfg(feature = "physics")]
142pub mod softbody;
143pub mod swapchain;
144pub mod telemetry;
145#[cfg(feature = "textured")]
146pub mod texture;
147pub mod tilebin;
148#[cfg(feature = "scene")]
149pub mod transform_anim;
150#[cfg(feature = "scene")]
151pub mod tween;
152
153pub use embedded_graphics_framebuf::{
155 FrameBuf,
156 backends::{DMACapableFrameBufferBackend, EndianCorrectedBuffer, EndianCorrection},
157};
158
159pub use draw::PixelRead;
160#[cfg(feature = "aa")]
161pub use draw::ReadPixel;
162
163#[cfg(feature = "aabb-cull")]
164pub use bounds::Aabb;
165pub use bridge::{
166 AsEgPoint, AsNalgebraPoint, draw_to, eg_to_nalgebra, nalgebra_to_eg, render_drawable_to_buffer,
167};
168#[cfg(feature = "scene")]
169pub use character::CharacterController;
170pub use completion::{CompletionSlot, WaitTransfer, WaitTransferFuture};
171pub use display_backend::{
172 AsyncDmaTransfer, DisplayBackend, DisplayError, DisplayRegion, DmaTransfer, SimulatorBackend,
173 TransferError,
174};
175#[cfg(feature = "aa")]
176pub use draw::draw_zbuffered_2xssaa;
177pub use draw::{
178 DitherConfig, FogConfig, fast_blend_rgb565, fast_blend_rgba8888, fast_blend_rgba8888_to_rgb565,
179 reverse_color_rgb565, reverse_color_rgba8888,
180};
181#[cfg(feature = "embassy")]
182pub use embassy::{EmbassyWaitTransfer, EmbassyWaitTransferFuture, FrameClock};
183pub use swapchain::{StandardSwapChain, SwapChain};
184#[cfg(feature = "triple-buffering")]
185pub use swapchain::{StandardTripleSwapChain, TripleSwapChain};
186#[cfg(feature = "fixed-transform")]
190pub use embedded_dsp::fixed_point::{
191 FP_ONE, Q16, Q16_MAX, Q16_MIN, ScanlineInterp, abs_q16, angle_to_q16, div_f_q16, div_n_q16,
192 div_q16, from_i16_q16, from_q16, lerp_q16, mul_f_q16, mul_n_q16, mul_q16, q16_to_q31,
193 q31_to_q16, qadd_q16, qsub_q16, recip_q16, to_i16_q16, to_q16,
194};
195pub use input::InputState;
196#[cfg(feature = "lighting")]
197pub use lights::{PointLight, PointLightSet};
198#[cfg(feature = "scene")]
199pub use particles::{ParticleSpawn, ParticleSystem};
200#[cfg(feature = "render-layers")]
201pub use render_layers::RenderLayers;
202pub use renderer::{DirtyRegion, FrameCtx};
203pub use retro::{
204 LightLevels, PaletteMode, RetroStyle, ScreenTint, SkyConfig, StippleMode, TextureMapping,
205};
206#[cfg(feature = "raycast")]
207pub use sector_lights::{LightEffectKind, SectorLight, light_level_at, light_level_u8_at};
208pub use tilebin::{TileBinStats, TileConfig};
209#[cfg(feature = "scene")]
210pub use transform_anim::{AnimationPlayer, SampledTransform, TransformKeyframe, TransformTrack};
211#[cfg(feature = "scene")]
212pub use tween::{Easing, Tween, Tween3, apply_easing, lerp, lerp3, scale_rgb565};
213
214pub fn mesh_ray_cast(
219 ray_origin: Vector3<f32>,
220 ray_dir: Vector3<f32>,
221 geometry: &mesh::Geometry<'_>,
222 model_matrix: &Matrix4<f32>,
223 max_distance: f32,
224) -> Option<MeshRayCastHit> {
225 #[cfg(feature = "aabb-cull")]
226 {
227 return mesh_ray_cast_bounded(
228 ray_origin,
229 ray_dir,
230 geometry,
231 model_matrix,
232 max_distance,
233 None,
234 );
235 }
236 #[cfg(not(feature = "aabb-cull"))]
237 {
238 mesh_ray_cast_world(ray_origin, ray_dir, geometry, model_matrix, max_distance)
239 }
240}
241
242#[cfg(not(feature = "aabb-cull"))]
243fn mesh_ray_cast_world(
244 ray_origin: Vector3<f32>,
245 ray_dir: Vector3<f32>,
246 geometry: &mesh::Geometry<'_>,
247 model_matrix: &Matrix4<f32>,
248 max_distance: f32,
249) -> Option<MeshRayCastHit> {
250 let mut nearest: Option<MeshRayCastHit> = None;
251 let mut min_dist = max_distance;
252
253 for (face_index, face) in geometry.faces.iter().enumerate() {
254 let raw_v0 = geometry.vertices[face[0]];
255 let raw_v1 = geometry.vertices[face[1]];
256 let raw_v2 = geometry.vertices[face[2]];
257
258 let v0 = model_matrix
259 .transform_point(&Point3::new(raw_v0[0], raw_v0[1], raw_v0[2]))
260 .coords;
261 let v1 = model_matrix
262 .transform_point(&Point3::new(raw_v1[0], raw_v1[1], raw_v1[2]))
263 .coords;
264 let v2 = model_matrix
265 .transform_point(&Point3::new(raw_v2[0], raw_v2[1], raw_v2[2]))
266 .coords;
267
268 let edge1 = v1 - v0;
269 let edge2 = v2 - v0;
270 let h = ray_dir.cross(&edge2);
271 let det = edge1.dot(&h);
272 if det.abs() < 1e-6 {
273 continue;
274 }
275 let inv_det = 1.0 / det;
276 let s = ray_origin - v0;
277 let bary_u = inv_det * s.dot(&h);
278 if !(0.0..=1.0).contains(&bary_u) {
279 continue;
280 }
281 let q = s.cross(&edge1);
282 let bary_v = inv_det * ray_dir.dot(&q);
283 if bary_v < 0.0 || bary_u + bary_v > 1.0 {
284 continue;
285 }
286 let t = inv_det * edge2.dot(&q);
287 if t <= 0.0 || t >= min_dist {
288 continue;
289 }
290 let normal = edge1.cross(&edge2).normalize();
291 let bary_w = 1.0 - bary_u - bary_v;
292 let uv = if geometry.uvs.len() > face[0]
293 && geometry.uvs.len() > face[1]
294 && geometry.uvs.len() > face[2]
295 {
296 let uv0 = geometry.uvs[face[0]];
297 let uv1 = geometry.uvs[face[1]];
298 let uv2 = geometry.uvs[face[2]];
299 [
300 bary_w * uv0[0] + bary_u * uv1[0] + bary_v * uv2[0],
301 bary_w * uv0[1] + bary_u * uv1[1] + bary_v * uv2[1],
302 ]
303 } else {
304 [0.0, 0.0]
305 };
306 let point = ray_origin + ray_dir * t;
307 min_dist = t;
308 nearest = Some(MeshRayCastHit {
309 distance: t,
310 point,
311 normal,
312 face_index,
313 uv,
314 });
315 }
316 nearest
317}
318
319#[cfg(feature = "aabb-cull")]
322pub fn mesh_ray_cast_bounded(
323 ray_origin: Vector3<f32>,
324 ray_dir: Vector3<f32>,
325 geometry: &mesh::Geometry<'_>,
326 model_matrix: &Matrix4<f32>,
327 max_distance: f32,
328 model_aabb: Option<&Aabb>,
329) -> Option<MeshRayCastHit> {
330 let inv = model_matrix.try_inverse()?;
331 let origin4 = inv * Vector4::new(ray_origin.x, ray_origin.y, ray_origin.z, 1.0);
332 let dir4 = inv * Vector4::new(ray_dir.x, ray_dir.y, ray_dir.z, 0.0);
333 if origin4.w.abs() < 1e-8 {
334 return None;
335 }
336 let local_origin = Vector3::new(origin4.x, origin4.y, origin4.z) / origin4.w;
337 let local_dir = Vector3::new(dir4.x, dir4.y, dir4.z);
338 let dir_len = local_dir.norm();
339 if dir_len < 1e-8 {
340 return None;
341 }
342 let local_dir_n = local_dir / dir_len;
343 let local_max = max_distance * dir_len;
344
345 if let Some(aabb) = model_aabb
346 && aabb
347 .intersect_ray(local_origin, local_dir_n, local_max)
348 .is_none()
349 {
350 return None;
351 }
352
353 let mut nearest: Option<MeshRayCastHit> = None;
354 let mut min_dist = local_max;
355
356 for (face_index, face) in geometry.faces.iter().enumerate() {
357 let v0 = Vector3::new(
358 geometry.vertices[face[0]][0],
359 geometry.vertices[face[0]][1],
360 geometry.vertices[face[0]][2],
361 );
362 let v1 = Vector3::new(
363 geometry.vertices[face[1]][0],
364 geometry.vertices[face[1]][1],
365 geometry.vertices[face[1]][2],
366 );
367 let v2 = Vector3::new(
368 geometry.vertices[face[2]][0],
369 geometry.vertices[face[2]][1],
370 geometry.vertices[face[2]][2],
371 );
372
373 let edge1 = v1 - v0;
374 let edge2 = v2 - v0;
375 let h = local_dir_n.cross(&edge2);
376 let det = edge1.dot(&h);
377 if det.abs() < 1e-6 {
378 continue;
379 }
380 let inv_det = 1.0 / det;
381 let s = local_origin - v0;
382 let bary_u = inv_det * s.dot(&h);
383 if !(0.0..=1.0).contains(&bary_u) {
384 continue;
385 }
386 let q = s.cross(&edge1);
387 let bary_v = inv_det * local_dir_n.dot(&q);
388 if bary_v < 0.0 || bary_u + bary_v > 1.0 {
389 continue;
390 }
391 let t_local = inv_det * edge2.dot(&q);
392 if t_local <= 0.0 || t_local >= min_dist {
393 continue;
394 }
395
396 let normal_local = edge1.cross(&edge2).normalize();
397 let rot = model_matrix.fixed_view::<3, 3>(0, 0);
398 let normal = (rot * normal_local).normalize();
399
400 let bary_w = 1.0 - bary_u - bary_v;
401 let uv = if geometry.uvs.len() > face[0]
402 && geometry.uvs.len() > face[1]
403 && geometry.uvs.len() > face[2]
404 {
405 let uv0 = geometry.uvs[face[0]];
406 let uv1 = geometry.uvs[face[1]];
407 let uv2 = geometry.uvs[face[2]];
408 [
409 bary_w * uv0[0] + bary_u * uv1[0] + bary_v * uv2[0],
410 bary_w * uv0[1] + bary_u * uv1[1] + bary_v * uv2[1],
411 ]
412 } else {
413 [0.0, 0.0]
414 };
415
416 let local_hit = local_origin + local_dir_n * t_local;
417 let world_hit = model_matrix
418 .transform_point(&Point3::from(local_hit))
419 .coords;
420 let world_t = (world_hit - ray_origin).norm();
421 if world_t >= max_distance {
422 continue;
423 }
424
425 min_dist = t_local;
426 nearest = Some(MeshRayCastHit {
427 distance: world_t,
428 point: world_hit,
429 normal,
430 face_index,
431 uv,
432 });
433 }
434
435 nearest
436}
437
438#[cfg(feature = "aabb-cull")]
440pub fn mesh_ray_cast_mesh(
441 ray_origin: Vector3<f32>,
442 ray_dir: Vector3<f32>,
443 mesh: &K3dMesh<'_>,
444 max_distance: f32,
445) -> Option<MeshRayCastHit> {
446 let distance = (mesh.get_position() - Point3::from(ray_origin)).norm();
447 let geometry = mesh.select_lod(distance);
448 let aabb = mesh.model_aabb();
449 mesh_ray_cast_bounded(
450 ray_origin,
451 ray_dir,
452 geometry,
453 &mesh.model_matrix,
454 max_distance,
455 Some(&aabb),
456 )
457}
458
459#[cfg(feature = "dma2d")]
460mod dma2d_stubs {
461 #[cfg(feature = "depth-u16")]
462 #[unsafe(no_mangle)]
463 extern "Rust" fn dma2d_clear_zbuffer_u16(ptr: *mut u16, len: usize, value: u16) {
464 let slice = unsafe { core::slice::from_raw_parts_mut(ptr, len) };
465 slice.fill(value);
466 }
467
468 #[cfg(not(feature = "depth-u16"))]
469 #[unsafe(no_mangle)]
470 extern "Rust" fn dma2d_clear_zbuffer_u32(ptr: *mut u32, len: usize, value: u32) {
471 let slice = unsafe { core::slice::from_raw_parts_mut(ptr, len) };
472 slice.fill(value);
473 }
474}
475
476#[cfg(test)]
477mod tests {
478 extern crate std;
479
480 #[cfg(feature = "depth-u16")]
481 pub type ZDepth = u16;
482 #[cfg(feature = "depth-u16")]
483 pub const Z_MAX_VALUE: ZDepth = u16::MAX;
484 #[cfg(feature = "depth-u16")]
485 pub const DEPTH_EPSILON: ZDepth = 1;
486
487 #[cfg(not(feature = "depth-u16"))]
488 pub type ZDepth = u32;
489 #[cfg(not(feature = "depth-u16"))]
490 pub const Z_MAX_VALUE: ZDepth = u32::MAX;
491 #[cfg(not(feature = "depth-u16"))]
492 pub const DEPTH_EPSILON: ZDepth = 128;
493
494 #[inline(always)]
495 pub const fn to_zdepth(z: u32) -> ZDepth {
496 #[cfg(feature = "depth-u16")]
497 {
498 (z >> 16) as u16
499 }
500 #[cfg(not(feature = "depth-u16"))]
501 {
502 z
503 }
504 }
505
506 use super::*;
507
508 #[test]
509 fn test_engine_creation() {
510 let engine = K3dengine::new(640, 480);
511 assert_eq!(engine.width, 640);
512 assert_eq!(engine.height, 480);
513 assert!((engine.camera.get_aspect_ratio() - 640.0 / 480.0).abs() < 0.001);
514 }
515
516 #[test]
517 fn test_transform_point_basic() {
518 let engine = K3dengine::new(640, 480);
519 let transform_matrix = engine.camera.vp_matrix;
521
522 let point = [0.0, 0.0, -5.0];
525 let result = engine.transform_point(&point, transform_matrix);
526
527 if let Some(transformed) = result {
528 assert!(transformed.x >= 0 && transformed.x < 640);
530 assert!(transformed.y >= 0 && transformed.y < 480);
531 }
532 }
534
535 #[test]
536 fn test_transform_point_clamps_out_of_bounds() {
537 let engine = K3dengine::new(640, 480);
538 let model_matrix = nalgebra::Matrix4::identity();
539
540 let point = [100.0, 100.0, -5.0];
542 let result = engine.transform_point(&point, model_matrix);
543 assert!(result.is_none());
545 }
546
547 #[test]
548 fn test_transform_point_behind_camera() {
549 let engine = K3dengine::new(640, 480);
550 let transform_matrix = engine.camera.vp_matrix;
551
552 let point = [0.0, 0.0, 1.0];
554 let _result = engine.transform_point(&point, transform_matrix);
555 }
559
560 #[test]
561 fn test_transform_point_near_plane_clipping() {
562 let engine = K3dengine::new(640, 480);
563 let transform_matrix = engine.camera.vp_matrix;
569
570 let point = [0.0, 0.0, -0.1];
572 let result = engine.transform_point(&point, transform_matrix);
573 assert!(result.is_none());
574 }
575
576 #[test]
577 fn test_transform_point_far_plane_clipping() {
578 let engine = K3dengine::new(640, 480);
579 let transform_matrix = engine.camera.vp_matrix;
580
581 let point = [0.0, 0.0, -1000.0];
583 let result = engine.transform_point(&point, transform_matrix);
584 assert!(result.is_none());
585 }
586
587 #[test]
588 fn test_transform_point_within_near_far_not_culled() {
589 let engine = K3dengine::new(640, 480);
595 let transform_matrix = engine.camera.vp_matrix;
596
597 let point = [0.0, 0.0, -0.8];
598 let result = engine.transform_point(&point, transform_matrix);
599 assert!(
600 result.is_some(),
601 "a point at distance 0.8 (within [near=0.4, far=20]) should not be culled"
602 );
603 }
604
605 #[test]
606 fn test_transform_points_array() {
607 let engine = K3dengine::new(640, 480);
608 let transform_matrix = engine.camera.vp_matrix;
609
610 let vertices = [[0.0, 0.0, -5.0], [0.1, 0.0, -5.0], [0.0, 0.1, -5.0]];
611 let indices = [0, 1, 2];
612
613 let result = engine.transform_points(&indices, &vertices, transform_matrix);
614
615 if let Some(points) = result {
617 assert_eq!(points.len(), 3);
618 }
619 }
621
622 #[test]
623 fn test_render_empty_faces_mesh() {
624 let engine = K3dengine::new(640, 480);
625 let vertices = [[0.0, 0.0, -5.0]]; let geometry = mesh::Geometry {
627 vertices: &vertices,
628 faces: &[],
629 colors: &[],
630 lines: &[],
631 normals: &[],
632 vertex_normals: &[],
633 uvs: &[],
634 texture_id: None,
635 };
636 let mesh = mesh::K3dMesh::new(geometry);
637
638 let mut callback_count = 0;
639 engine.render(std::iter::once(&mesh), |_| {
640 callback_count += 1;
641 });
642
643 assert!(callback_count > 0);
645 }
646
647 #[test]
648 fn test_render_points_mode() {
649 let engine = K3dengine::new(640, 480);
650
651 let vertices = [[0.0, 0.0, -5.0], [0.5, 0.0, -5.0]];
652
653 let geometry = mesh::Geometry {
654 vertices: &vertices,
655 faces: &[],
656 colors: &[],
657 lines: &[],
658 normals: &[],
659 vertex_normals: &[],
660 uvs: &[],
661 texture_id: None,
662 };
663
664 let mut mesh = mesh::K3dMesh::new(geometry);
665 mesh.set_render_mode(mesh::RenderMode::Points);
666
667 let mut primitives = std::vec::Vec::new();
668 engine.render(std::iter::once(&mesh), |prim| {
669 primitives.push(prim);
670 });
671
672 assert!(primitives.len() > 0);
674 for prim in primitives {
675 assert!(matches!(prim, DrawPrimitive::ColoredPoint(_, _)));
676 }
677 }
678
679 #[test]
680 fn test_render_lines_mode_with_faces() {
681 let engine = K3dengine::new(640, 480);
682
683 let vertices = [[0.0, 0.0, -5.0], [0.5, 0.0, -5.0], [0.0, 0.5, -5.0]];
684
685 let faces = [[0, 1, 2]];
686
687 let geometry = mesh::Geometry {
688 vertices: &vertices,
689 faces: &faces,
690 colors: &[],
691 lines: &[],
692 normals: &[],
693 vertex_normals: &[],
694 uvs: &[],
695 texture_id: None,
696 };
697
698 let mut mesh = mesh::K3dMesh::new(geometry);
699 mesh.set_render_mode(mesh::RenderMode::Lines);
700
701 let mut primitives = std::vec::Vec::new();
702 engine.render(std::iter::once(&mesh), |prim| {
703 primitives.push(prim);
704 });
705
706 assert_eq!(primitives.len(), 3);
708 for prim in primitives {
709 assert!(matches!(prim, DrawPrimitive::Line(_, _)));
710 }
711 }
712
713 #[test]
714 #[cfg(feature = "lighting")]
715 fn test_render_gouraud_light_dir() {
716 let mut engine = K3dengine::new(640, 480);
717 engine.camera.set_position(Point3::new(0.0, 0.0, -10.0));
718 engine.camera.set_target(Point3::new(0.0, 0.0, 0.0));
719
720 let vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
721 let faces = [[0, 1, 2]];
722 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]];
724
725 let geometry = mesh::Geometry {
726 vertices: &vertices,
727 faces: &faces,
728 colors: &[],
729 lines: &[],
730 normals: &normals,
731 vertex_normals: &vertex_normals,
732 uvs: &[],
733 texture_id: None,
734 };
735
736 let mut mesh = mesh::K3dMesh::new(geometry);
737 mesh.set_render_mode(mesh::RenderMode::GouraudLightDir(Vector3::new(
738 0.0, 0.0, 1.0,
739 )));
740
741 let mut primitives = std::vec::Vec::new();
742 engine.render(std::iter::once(&mesh), |prim| {
743 primitives.push(prim);
744 });
745
746 assert!(!primitives.is_empty());
748 for prim in &primitives {
749 assert!(matches!(
750 prim,
751 DrawPrimitive::GouraudTriangleWithDepth { .. }
752 ));
753 }
754 }
755
756 #[test]
763 fn test_solid_inward_normals_interior_camera() {
764 let mut engine = K3dengine::new(320, 240);
765 engine
767 .camera
768 .set_position(nalgebra::Point3::new(0.0, 0.0, 0.0));
769 engine
770 .camera
771 .set_target(nalgebra::Point3::new(0.0, 0.0, -1.0));
772
773 #[rustfmt::skip]
776 let vertices: &[[f32; 3]] = &[
777 [-1.0, -1.0, -2.0],
778 [ 1.0, -1.0, -2.0],
779 [ 1.0, 1.0, -2.0],
780 [-1.0, 1.0, -2.0],
781 ];
782 let faces: &[[usize; 3]] = &[[0, 1, 2], [0, 2, 3]];
783 let normals: &[[f32; 3]] = &[[0.0, 0.0, 1.0], [0.0, 0.0, 1.0]]; let geometry = mesh::Geometry {
786 vertices,
787 faces,
788 normals,
789 colors: &[],
790 lines: &[],
791 vertex_normals: &[],
792 uvs: &[],
793 texture_id: None,
794 };
795 let mut m = mesh::K3dMesh::new(geometry);
796 m.set_render_mode(mesh::RenderMode::Solid);
797
798 let mut count = 0usize;
799 engine.render(std::iter::once(&m), |_| count += 1);
800
801 assert!(
802 count > 0,
803 "interior Solid-with-inward-normals emitted 0 primitives — culling is wrong"
804 );
805 }
806}