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