1#[cfg(feature = "std")]
18pub mod builder;
19pub mod coverage;
20pub mod data;
21pub mod pvs;
22pub mod scratch;
23pub mod traverse;
24
25use core::fmt::Debug;
26
27use embedded_graphics_core::{
28 draw_target::DrawTarget, pixelcolor::Rgb565, prelude::OriginDimensions,
29};
30use heapless::Vec as HVec;
31use nalgebra::{Point2, Vector4};
32
33use crate::{
34 DrawPrimitive, K3dengine,
35 command_buffer::{CommandBuffer, RenderCommand},
36 error::RenderError,
37 lights::PointLight,
38 renderer::FrameCtx,
39 sector_lights::{SectorLight, light_level_u8_at},
40 texture::TextureManager,
41};
42
43use coverage::CoverageBuffer;
44use data::{BspWorld, Face};
45use scratch::BspScratch;
46use traverse::{ClipVert, frustum_from_vp, project_to_screen, walk_front_to_back};
47
48#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
54pub struct BspTelemetry {
55 pub leaves_visited: usize,
57 pub leaves_culled_pvs: usize,
59 pub leaves_culled_frustum: usize,
61 pub faces_visible: usize,
63 pub faces_emitted: usize,
65 pub triangles_emitted: usize,
67}
68
69struct ProjTri {
75 points: [Point2<i32>; 3],
76 depths: [f32; 3],
77 ws: [f32; 3],
78 uvs: [[f32; 2]; 3],
79 lm_uvs: [[f32; 2]; 3],
80}
81
82#[inline]
83fn lerp_clip_vert(a: ClipVert, b: ClipVert, t: f32) -> ClipVert {
84 ClipVert {
85 clip: a.clip + (b.clip - a.clip) * t,
86 uv: [
87 a.uv[0] + (b.uv[0] - a.uv[0]) * t,
88 a.uv[1] + (b.uv[1] - a.uv[1]) * t,
89 ],
90 lm_uv: [
91 a.lm_uv[0] + (b.lm_uv[0] - a.lm_uv[0]) * t,
92 a.lm_uv[1] + (b.lm_uv[1] - a.lm_uv[1]) * t,
93 ],
94 }
95}
96
97fn clip_polygon_plane(
98 input: &HVec<ClipVert, 10>,
99 output: &mut HVec<ClipVert, 10>,
100 dist: impl Fn(ClipVert) -> f32,
101) {
102 output.clear();
103 let n = input.len();
104 if n == 0 {
105 return;
106 }
107 for i in 0..n {
108 let prev = input[(n + i - 1) % n];
109 let curr = input[i];
110 let d_prev = dist(prev);
111 let d_curr = dist(curr);
112 if d_curr >= 0.0 {
113 if d_prev < 0.0 {
114 let denom = d_prev - d_curr;
115 if denom.abs() > 1e-6 {
116 let t = d_prev / denom;
117 let _ = output.push(lerp_clip_vert(prev, curr, t));
118 }
119 }
120 let _ = output.push(curr);
121 } else if d_prev >= 0.0 {
122 let denom = d_prev - d_curr;
123 if denom.abs() > 1e-6 {
124 let t = d_prev / denom;
125 let _ = output.push(lerp_clip_vert(prev, curr, t));
126 }
127 }
128 }
129}
130
131fn clip_and_project(
137 v: [ClipVert; 3],
138 width: u16,
139 height: u16,
140 near: f32,
141 far: f32,
142) -> HVec<ProjTri, 8> {
143 let mut result: HVec<ProjTri, 8> = HVec::new();
144
145 let mut a: HVec<ClipVert, 10> = HVec::new();
147 let mut b: HVec<ClipVert, 10> = HVec::new();
148 for vert in &v {
149 let _ = a.push(*vert);
150 }
151
152 clip_polygon_plane(&a, &mut b, |p| p.clip.w);
154 if b.len() < 3 {
155 return result;
156 }
157 core::mem::swap(&mut a, &mut b);
158
159 clip_polygon_plane(&a, &mut b, |p| p.clip.z + p.clip.w);
161 if b.len() < 3 {
162 return result;
163 }
164 core::mem::swap(&mut a, &mut b);
165
166 clip_polygon_plane(&a, &mut b, |p| p.clip.w - p.clip.z);
168 if b.len() < 3 {
169 return result;
170 }
171 core::mem::swap(&mut a, &mut b);
172
173 clip_polygon_plane(&a, &mut b, |p| p.clip.x + p.clip.w);
175 if b.len() < 3 {
176 return result;
177 }
178 core::mem::swap(&mut a, &mut b);
179
180 clip_polygon_plane(&a, &mut b, |p| p.clip.w - p.clip.x);
182 if b.len() < 3 {
183 return result;
184 }
185 core::mem::swap(&mut a, &mut b);
186
187 clip_polygon_plane(&a, &mut b, |p| p.clip.y + p.clip.w);
189 if b.len() < 3 {
190 return result;
191 }
192 core::mem::swap(&mut a, &mut b);
193
194 clip_polygon_plane(&a, &mut b, |p| p.clip.w - p.clip.y);
196 if b.len() < 3 {
197 return result;
198 }
199 core::mem::swap(&mut a, &mut b);
200
201 let n = a.len();
203 for i in 1..(n - 1) {
204 let pts = [&a[0], &a[i], &a[i + 1]];
205
206 let mut screen = [Point2::new(0i32, 0i32); 3];
207 let mut depths = [0.0f32; 3];
208 let mut ws = [0.0f32; 3];
209 let mut uvs = [[0.0f32; 2]; 3];
210 let mut lm_uvs = [[0.0f32; 2]; 3];
211 let mut ok = true;
212
213 for (k, &pt) in pts.iter().enumerate() {
214 if let Some((sp, depth, w)) = project_to_screen(pt.clip, width, height, near, far) {
215 screen[k] = sp;
216 depths[k] = depth;
217 ws[k] = w;
218 uvs[k] = pt.uv;
219 lm_uvs[k] = pt.lm_uv;
220 } else {
221 ok = false;
222 break;
223 }
224 }
225
226 if ok {
227 let _ = result.push(ProjTri {
228 points: screen,
229 depths,
230 ws,
231 uvs,
232 lm_uvs,
233 });
234 }
235 }
236
237 result
238}
239
240fn face_fan_triangle(
245 world: &BspWorld<'_>,
246 face: &Face,
247 k: usize,
248 vp: &nalgebra::Matrix4<f32>,
249) -> Option<[ClipVert; 3]> {
250 let base = face.first_vert as usize;
251 let i0 = base;
252 let i1 = base + k + 1;
253 let i2 = base + k + 2;
254
255 if i2 >= world.vertices.len() {
256 return None;
257 }
258
259 let to_clip = |idx: usize| -> Vector4<f32> {
260 let v = &world.vertices[idx];
261 vp * Vector4::new(v[0], v[1], v[2], 1.0)
262 };
263
264 let uv = |idx: usize| -> [f32; 2] { world.uvs.get(idx).copied().unwrap_or([0.0, 0.0]) };
265
266 let lm_uv = |idx: usize| -> [f32; 2] { world.lm_uvs.get(idx).copied().unwrap_or([0.0, 0.0]) };
267
268 Some([
269 ClipVert {
270 clip: to_clip(i0),
271 uv: uv(i0),
272 lm_uv: lm_uv(i0),
273 },
274 ClipVert {
275 clip: to_clip(i1),
276 uv: uv(i1),
277 lm_uv: lm_uv(i1),
278 },
279 ClipVert {
280 clip: to_clip(i2),
281 uv: uv(i2),
282 lm_uv: lm_uv(i2),
283 },
284 ])
285}
286
287fn face_dynamic_tint(world: &BspWorld<'_>, face: &Face, lights: &[PointLight]) -> Rgb565 {
294 use embedded_graphics_core::pixelcolor::RgbColor;
295 if lights.is_empty() {
296 return Rgb565::new(0, 0, 0);
297 }
298 let base = face.first_vert as usize;
299 let n = face.num_verts as usize;
300 let mut cx = 0.0f32;
301 let mut cy = 0.0f32;
302 let mut cz = 0.0f32;
303 for i in 0..n {
304 if let Some(v) = world.vertices.get(base + i) {
305 cx += v[0];
306 cy += v[1];
307 cz += v[2];
308 }
309 }
310 if n == 0 {
311 return Rgb565::new(0, 0, 0);
312 }
313 let inv = 1.0 / n as f32;
314 let world_pos = nalgebra::Point3::new(cx * inv, cy * inv, cz * inv);
315 let mut r = 0u32;
316 let mut g = 0u32;
317 let mut b = 0u32;
318 for light in lights {
319 let c = light.contribution_at(world_pos);
320 r += c.r() as u32;
321 g += c.g() as u32;
322 b += c.b() as u32;
323 }
324 Rgb565::new(r.min(31) as u8, g.min(63) as u8, b.min(31) as u8)
325}
326
327impl K3dengine {
332 pub fn record_bsp<const MAX: usize>(
340 &self,
341 world: &BspWorld<'_>,
342 scratch: &mut BspScratch<'_>,
343 commands: &mut CommandBuffer<MAX>,
344 telemetry: Option<&mut BspTelemetry>,
345 ) -> Result<(), RenderError> {
346 self.record_bsp_with_sector_lights(world, scratch, commands, None, 0.0, telemetry)
347 }
348
349 pub fn record_bsp_with_sector_lights<const MAX: usize>(
351 &self,
352 world: &BspWorld<'_>,
353 scratch: &mut BspScratch<'_>,
354 commands: &mut CommandBuffer<MAX>,
355 sector_lights: Option<&[SectorLight]>,
356 time_seconds: f32,
357 telemetry: Option<&mut BspTelemetry>,
358 ) -> Result<(), RenderError> {
359 commands.clear();
360 commands.push(RenderCommand::ClearDepth(u32::MAX))?;
361 scratch.mark_new_frame();
362
363 let cam = [
364 self.camera.position.x,
365 self.camera.position.y,
366 self.camera.position.z,
367 ];
368 let cam_leaf = world.leaf_for_point(cam);
369 let cam_cluster = world.leaves.get(cam_leaf).map(|l| l.cluster).unwrap_or(-1);
370
371 let frustum = frustum_from_vp(&self.camera.vp_matrix);
372 let vp = &self.camera.vp_matrix;
373
374 let mut tel = BspTelemetry::default();
375 let mut first_err: Option<RenderError> = None;
376
377 walk_front_to_back(world, scratch, cam, cam_cluster, &frustum, |_fi, face| {
378 if first_err.is_some() {
379 return;
380 }
381 tel.faces_visible += 1;
382
383 let dynamic_tint = face_dynamic_tint(world, face, &self.point_lights);
384
385 let num_tris = face.num_verts.saturating_sub(2) as usize;
386 let mut emitted_any = false;
387
388 for k in 0..num_tris {
389 let tri = match face_fan_triangle(world, face, k, vp) {
390 Some(t) => t,
391 None => continue,
392 };
393 let projected = clip_and_project(
394 tri,
395 self.width,
396 self.height,
397 self.camera.near,
398 self.camera.far,
399 );
400
401 for pt in projected.iter() {
402 let brightness = if let Some(lights) = sector_lights {
403 if face.sector_light_id == u16::MAX {
404 255
405 } else {
406 lights
407 .get(face.sector_light_id as usize)
408 .map_or(255, |light| light_level_u8_at(light, time_seconds))
409 }
410 } else {
411 255
412 }
413 .max(32);
415 let prim = DrawPrimitive::LightmappedTriangle {
416 points: [pt.points[0], pt.points[1], pt.points[2]],
417 depths: pt.depths,
418 ws: pt.ws,
419 surface_uvs: pt.uvs,
420 lm_uvs: pt.lm_uvs,
421 texture_id: face.texture_id,
422 lightmap_id: if face.lightmap_id == 0xFFFF {
423 u32::MAX
424 } else {
425 face.lightmap_id as u32
426 },
427 brightness,
428 dynamic_tint,
429 };
430
431 if let Err(e) = commands.push(RenderCommand::Draw(prim)) {
432 first_err = Some(e);
433 return;
434 }
435 tel.triangles_emitted += 1;
436 emitted_any = true;
437 }
438 }
439
440 if emitted_any {
441 tel.faces_emitted += 1;
442 }
443 });
444
445 if let Some(err) = first_err {
446 return Err(err);
447 }
448
449 if let Some(t) = telemetry {
450 *t = tel;
451 }
452 Ok(())
453 }
454
455 pub fn execute_bsp_textured<D, const MAX: usize, const N: usize>(
460 &self,
461 fb: &mut D,
462 frame: &mut FrameCtx<'_>,
463 commands: &CommandBuffer<MAX>,
464 texture_manager: &TextureManager<N>,
465 telemetry: Option<&mut crate::telemetry::ExecuteTelemetry>,
466 ) -> Result<Option<crate::renderer::DirtyRegion>, RenderError>
467 where
468 D: DrawTarget<Color = Rgb565> + OriginDimensions,
469 D::Error: Debug,
470 {
471 use crate::draw::{draw_zbuffered_lightmapped_mapped, draw_zbuffered_with_textures_mapped};
472 use crate::renderer::DirtyRegion;
473
474 frame.validate()?;
475 let mut dirty: Option<(i32, i32, i32, i32)> = None;
476
477 if let Some(t) = telemetry {
478 t.commands_total = commands.len();
479 t.draw_commands = commands
480 .iter()
481 .filter(|c| matches!(c, RenderCommand::Draw(_)))
482 .count();
483 }
484
485 for cmd in commands.iter() {
486 match cmd {
487 RenderCommand::ClearDepth(v) => frame.zbuffer.fill(*v),
488 RenderCommand::ClearColor(color) => {
489 let w = frame.width as i32;
490 let h = frame.height as i32;
491 for y in 0..h {
492 for x in 0..w {
493 fb.draw_iter([embedded_graphics_core::Pixel(
494 embedded_graphics_core::prelude::Point::new(x, y),
495 *color,
496 )])
497 .map_err(|_| RenderError::InvalidInput("clear write failed"))?;
498 }
499 }
500 }
501 RenderCommand::Draw(prim) => {
502 let (x0, y0, x1, y1) = prim_bounds(prim);
503 match prim.clone() {
504 DrawPrimitive::LightmappedTriangle {
505 points,
506 depths,
507 ws,
508 surface_uvs,
509 lm_uvs,
510 texture_id,
511 lightmap_id,
512 brightness,
513 dynamic_tint,
514 } => {
515 draw_zbuffered_lightmapped_mapped(
516 points,
517 depths,
518 ws,
519 surface_uvs,
520 lm_uvs,
521 texture_id,
522 lightmap_id,
523 brightness,
524 dynamic_tint,
525 self.fog.as_ref(),
526 texture_manager,
527 fb,
528 frame.zbuffer,
529 frame.width,
530 self.texture_mapping,
531 self.stipple_mode,
532 self.screen_tint,
533 self.palette_mode,
534 );
535 }
536 other => {
537 draw_zbuffered_with_textures_mapped(
538 other,
539 fb,
540 frame.zbuffer,
541 frame.width,
542 texture_manager,
543 self.fog.as_ref(),
544 self.dither.as_ref(),
545 self.texture_mapping,
546 self.stipple_mode,
547 self.screen_tint,
548 self.palette_mode,
549 );
550 }
551 }
552 if let Some((cx0, cy0, cx1, cy1)) = dirty {
553 dirty = Some((cx0.min(x0), cy0.min(y0), cx1.max(x1), cy1.max(y1)));
554 } else {
555 dirty = Some((x0, y0, x1, y1));
556 }
557 }
558 }
559 }
560
561 let region = dirty.and_then(|(x0, y0, x1, y1)| {
562 if x1 < x0 || y1 < y0 {
563 return None;
564 }
565 Some(DirtyRegion {
566 x: x0 as usize,
567 y: y0 as usize,
568 width: (x1 - x0 + 1) as usize,
569 height: (y1 - y0 + 1) as usize,
570 })
571 });
572 Ok(region)
573 }
574
575 pub fn render_bsp_direct<D, const N: usize>(
580 &self,
581 world: &BspWorld<'_>,
582 scratch: &mut BspScratch<'_>,
583 texture_manager: &TextureManager<N>,
584 fb: &mut D,
585 frame: &mut FrameCtx<'_>,
586 telemetry: Option<&mut BspTelemetry>,
587 ) -> Result<(), RenderError>
588 where
589 D: DrawTarget<Color = Rgb565> + OriginDimensions,
590 D::Error: Debug,
591 {
592 use crate::draw::draw_zbuffered_lightmapped_mapped;
593
594 frame.validate()?;
595 frame.zbuffer.fill(u32::MAX);
596 scratch.mark_new_frame();
597
598 let cam = [
599 self.camera.position.x,
600 self.camera.position.y,
601 self.camera.position.z,
602 ];
603 let cam_leaf = world.leaf_for_point(cam);
604 let cam_cluster = world.leaves.get(cam_leaf).map(|l| l.cluster).unwrap_or(-1);
605 let frustum = frustum_from_vp(&self.camera.vp_matrix);
606 let vp = &self.camera.vp_matrix;
607
608 let mut tel = BspTelemetry::default();
609
610 walk_front_to_back(world, scratch, cam, cam_cluster, &frustum, |_fi, face| {
611 tel.faces_visible += 1;
612
613 let dynamic_tint = face_dynamic_tint(world, face, &self.point_lights);
614
615 let num_tris = face.num_verts.saturating_sub(2) as usize;
616 let mut emitted_any = false;
617
618 for k in 0..num_tris {
619 let tri = match face_fan_triangle(world, face, k, vp) {
620 Some(t) => t,
621 None => continue,
622 };
623 let projected = clip_and_project(
624 tri,
625 self.width,
626 self.height,
627 self.camera.near,
628 self.camera.far,
629 );
630
631 for pt in projected.iter() {
632 draw_zbuffered_lightmapped_mapped(
633 [pt.points[0], pt.points[1], pt.points[2]],
634 pt.depths,
635 pt.ws,
636 pt.uvs,
637 pt.lm_uvs,
638 face.texture_id,
639 if face.lightmap_id == 0xFFFF {
640 u32::MAX
641 } else {
642 face.lightmap_id as u32
643 },
644 255,
645 dynamic_tint,
646 self.fog.as_ref(),
647 texture_manager,
648 fb,
649 frame.zbuffer,
650 frame.width,
651 self.texture_mapping,
652 self.stipple_mode,
653 self.screen_tint,
654 self.palette_mode,
655 );
656 tel.triangles_emitted += 1;
657 emitted_any = true;
658 }
659 }
660
661 if emitted_any {
662 tel.faces_emitted += 1;
663 }
664 });
665
666 if let Some(t) = telemetry {
667 *t = tel;
668 }
669 Ok(())
670 }
671
672 pub fn render_bsp_coverage<D, const N: usize>(
682 &self,
683 world: &BspWorld<'_>,
684 scratch: &mut BspScratch<'_>,
685 texture_manager: &TextureManager<N>,
686 fb: &mut D,
687 coverage: &mut CoverageBuffer<'_>,
688 telemetry: Option<&mut BspTelemetry>,
689 ) -> Result<(), RenderError>
690 where
691 D: DrawTarget<Color = Rgb565> + OriginDimensions,
692 D::Error: Debug,
693 {
694 use crate::draw::draw_bsp_coverage;
695
696 coverage.clear();
697 scratch.mark_new_frame();
698
699 let cam = [
700 self.camera.position.x,
701 self.camera.position.y,
702 self.camera.position.z,
703 ];
704 let cam_leaf = world.leaf_for_point(cam);
705 let cam_cluster = world.leaves.get(cam_leaf).map(|l| l.cluster).unwrap_or(-1);
706 let frustum = frustum_from_vp(&self.camera.vp_matrix);
707 let vp = &self.camera.vp_matrix;
708
709 let mut tel = BspTelemetry::default();
710
711 walk_front_to_back(world, scratch, cam, cam_cluster, &frustum, |_fi, face| {
712 if coverage.is_full() {
714 return;
715 }
716
717 tel.faces_visible += 1;
718 let num_tris = face.num_verts.saturating_sub(2) as usize;
719 let mut emitted_any = false;
720
721 for k in 0..num_tris {
722 let tri = match face_fan_triangle(world, face, k, vp) {
723 Some(t) => t,
724 None => continue,
725 };
726 let projected = clip_and_project(
727 tri,
728 self.width,
729 self.height,
730 self.camera.near,
731 self.camera.far,
732 );
733
734 for pt in projected.iter() {
735 draw_bsp_coverage(
736 [pt.points[0], pt.points[1], pt.points[2]],
737 pt.ws,
738 pt.uvs,
739 face.texture_id,
740 texture_manager,
741 fb,
742 coverage,
743 self.texture_mapping,
744 self.stipple_mode,
745 self.screen_tint,
746 self.palette_mode,
747 );
748 tel.triangles_emitted += 1;
749 emitted_any = true;
750 }
751 }
752
753 if emitted_any {
754 tel.faces_emitted += 1;
755 }
756 });
757
758 if let Some(t) = telemetry {
759 *t = tel;
760 }
761 Ok(())
762 }
763}
764
765fn prim_bounds(p: &DrawPrimitive) -> (i32, i32, i32, i32) {
770 match p {
771 DrawPrimitive::ColoredPoint(pt, _) => (pt.x, pt.y, pt.x, pt.y),
772 DrawPrimitive::Line([a, b], _) => (a.x.min(b.x), a.y.min(b.y), a.x.max(b.x), a.y.max(b.y)),
773 DrawPrimitive::ColoredTriangle(pts, _)
774 | DrawPrimitive::ColoredTriangleWithDepth { points: pts, .. }
775 | DrawPrimitive::TranslucentTriangleWithDepth { points: pts, .. }
776 | DrawPrimitive::GouraudTriangle { points: pts, .. }
777 | DrawPrimitive::GouraudTriangleWithDepth { points: pts, .. }
778 | DrawPrimitive::TexturedTriangle { points: pts, .. }
779 | DrawPrimitive::TexturedTriangleWithDepth { points: pts, .. }
780 | DrawPrimitive::LightmappedTriangle { points: pts, .. } => {
781 let min_x = pts.iter().map(|p| p.x).min().unwrap_or(0);
782 let min_y = pts.iter().map(|p| p.y).min().unwrap_or(0);
783 let max_x = pts.iter().map(|p| p.x).max().unwrap_or(0);
784 let max_y = pts.iter().map(|p| p.y).max().unwrap_or(0);
785 (min_x, min_y, max_x, max_y)
786 }
787 }
788}
789
790#[allow(dead_code)]
808pub mod test_level {
809 use super::data::*;
810
811 pub static PLANES: [Plane; 1] = [Plane {
812 normal: [1.0, 0.0, 0.0],
813 dist: 0.0,
814 }];
815
816 pub static NODES: [Node; 1] = [Node {
818 plane: 0,
819 children: [!1i32, !0i32],
820 mins: [-5, -2, -3],
821 maxs: [5, 2, 3],
822 first_face: 0,
823 num_faces: 0,
824 }];
825
826 pub static LEAVES: [Leaf; 2] = [
827 Leaf {
828 cluster: 0,
829 mins: [-5, -2, -3],
830 maxs: [0, 2, 3],
831 first_marksurface: 0,
832 num_marksurfaces: 2,
833 },
834 Leaf {
835 cluster: 1,
836 mins: [0, -2, -3],
837 maxs: [5, 2, 3],
838 first_marksurface: 2,
839 num_marksurfaces: 2,
840 },
841 ];
842
843 pub static FACES: [Face; 4] = [
845 Face {
847 first_vert: 0,
848 num_verts: 4,
849 texture_id: 0,
850 lightmap_id: 0xFFFF,
851 plane: 0,
852 side: 0,
853 sector_light_id: u16::MAX,
854 },
855 Face {
857 first_vert: 4,
858 num_verts: 4,
859 texture_id: 0,
860 lightmap_id: 0xFFFF,
861 plane: 0,
862 side: 0,
863 sector_light_id: u16::MAX,
864 },
865 Face {
867 first_vert: 8,
868 num_verts: 4,
869 texture_id: 0,
870 lightmap_id: 0xFFFF,
871 plane: 0,
872 side: 0,
873 sector_light_id: u16::MAX,
874 },
875 Face {
877 first_vert: 12,
878 num_verts: 4,
879 texture_id: 0,
880 lightmap_id: 0xFFFF,
881 plane: 0,
882 side: 0,
883 sector_light_id: u16::MAX,
884 },
885 ];
886
887 pub static MARKSURFACES: [u16; 4] = [0, 1, 2, 3];
888
889 pub static VERTICES: [[f32; 3]; 16] = [
891 [-5.0, -2.0, -3.0],
893 [0.0, -2.0, -3.0],
894 [0.0, -2.0, 3.0],
895 [-5.0, -2.0, 3.0],
896 [-5.0, 2.0, -3.0],
898 [0.0, 2.0, -3.0],
899 [0.0, 2.0, 3.0],
900 [-5.0, 2.0, 3.0],
901 [0.0, -2.0, -3.0],
903 [5.0, -2.0, -3.0],
904 [5.0, -2.0, 3.0],
905 [0.0, -2.0, 3.0],
906 [0.0, 2.0, -3.0],
908 [5.0, 2.0, -3.0],
909 [5.0, 2.0, 3.0],
910 [0.0, 2.0, 3.0],
911 ];
912
913 pub static UVS: [[f32; 2]; 16] = [
914 [0.0, 0.0],
915 [1.0, 0.0],
916 [1.0, 1.0],
917 [0.0, 1.0],
918 [0.0, 0.0],
919 [1.0, 0.0],
920 [1.0, 1.0],
921 [0.0, 1.0],
922 [0.0, 0.0],
923 [1.0, 0.0],
924 [1.0, 1.0],
925 [0.0, 1.0],
926 [0.0, 0.0],
927 [1.0, 0.0],
928 [1.0, 1.0],
929 [0.0, 1.0],
930 ];
931
932 pub static VIS: [u8; 2] = [0b00000011, 0b00000011];
934 pub static VIS_OFFSETS: [u32; 2] = [0, 1];
935
936 pub fn world() -> BspWorld<'static> {
937 BspWorld::new(
938 &PLANES,
939 &NODES,
940 &LEAVES,
941 &FACES,
942 &MARKSURFACES,
943 &VERTICES,
944 &UVS,
945 &[], &VIS,
947 &VIS_OFFSETS,
948 2,
949 )
950 }
951}
952
953#[cfg(test)]
958mod tests {
959 extern crate std;
960 use super::*;
961 use crate::bsp::scratch::BspScratch;
962 use crate::bsp::test_level;
963 use crate::sector_lights::{LightEffectKind, SectorLight};
964 use nalgebra::Point3;
965
966 #[test]
967 fn test_level_leaf_location() {
968 let world = test_level::world();
969 let leaf_a = world.leaf_for_point([-2.0, 0.0, 0.0]);
970 let leaf_b = world.leaf_for_point([2.0, 0.0, 0.0]);
971 assert_eq!(leaf_a, 0);
972 assert_eq!(leaf_b, 1);
973 }
974
975 #[test]
976 fn test_level_cluster_visibility() {
977 let world = test_level::world();
978 assert!(world.cluster_visible(0, 0));
979 assert!(world.cluster_visible(0, 1));
980 assert!(world.cluster_visible(1, 0));
981 assert!(world.cluster_visible(1, 1));
982 }
983
984 #[test]
985 fn record_bsp_emits_draw_commands() {
986 let world = test_level::world();
987 let mut visframe = [0u32; 4];
988 let mut scratch = BspScratch::new(&mut visframe);
989
990 let mut engine = K3dengine::new(320, 240);
991 engine.camera.set_position(Point3::new(-2.0, 0.0, 0.0));
992 engine.camera.set_target(Point3::new(0.0, 0.0, 0.0));
993
994 let mut commands: CommandBuffer<512> = CommandBuffer::new();
995 let mut tel = BspTelemetry::default();
996
997 engine
998 .record_bsp(&world, &mut scratch, &mut commands, Some(&mut tel))
999 .unwrap();
1000
1001 let draw_count = commands
1003 .iter()
1004 .filter(|c| matches!(c, RenderCommand::Draw(_)))
1005 .count();
1006 assert!(draw_count > 0, "expected draw commands, got 0");
1007 assert!(tel.faces_visible > 0);
1008 assert!(tel.triangles_emitted > 0);
1009 }
1010
1011 #[test]
1012 fn record_bsp_deduplicates_faces() {
1013 let world = test_level::world();
1014 let mut vf1 = [0u32; 4];
1015 let mut vf2 = [0u32; 4];
1016 let mut s1 = BspScratch::new(&mut vf1);
1017 let mut s2 = BspScratch::new(&mut vf2);
1018
1019 let mut engine = K3dengine::new(320, 240);
1020 engine.camera.set_position(Point3::new(-2.0, 0.0, 0.0));
1021 engine.camera.set_target(Point3::new(0.0, 0.0, 0.0));
1022
1023 let mut c1: CommandBuffer<512> = CommandBuffer::new();
1024 engine.record_bsp(&world, &mut s1, &mut c1, None).unwrap();
1025 let count1 = c1
1026 .iter()
1027 .filter(|c| matches!(c, RenderCommand::Draw(_)))
1028 .count();
1029
1030 let mut c2: CommandBuffer<512> = CommandBuffer::new();
1032 engine.record_bsp(&world, &mut s2, &mut c2, None).unwrap();
1033 let count2 = c2
1034 .iter()
1035 .filter(|c| matches!(c, RenderCommand::Draw(_)))
1036 .count();
1037
1038 assert_eq!(count1, count2);
1039 }
1040
1041 #[test]
1042 fn record_bsp_with_sector_lights_keeps_output_when_faces_unmapped() {
1043 let world = test_level::world();
1044 let mut vf_a = [0u32; 4];
1045 let mut vf_b = [0u32; 4];
1046 let mut scratch_a = BspScratch::new(&mut vf_a);
1047 let mut scratch_b = BspScratch::new(&mut vf_b);
1048
1049 let mut engine = K3dengine::new(320, 240);
1050 engine.camera.set_position(Point3::new(-2.0, 0.0, 0.0));
1051 engine.camera.set_target(Point3::new(0.0, 0.0, 0.0));
1052
1053 let mut commands_base: CommandBuffer<512> = CommandBuffer::new();
1054 engine
1055 .record_bsp(&world, &mut scratch_a, &mut commands_base, None)
1056 .unwrap();
1057 let base_draw_count = commands_base
1058 .iter()
1059 .filter(|c| matches!(c, RenderCommand::Draw(_)))
1060 .count();
1061
1062 let lights = [SectorLight {
1063 base: 255,
1064 alt: 48,
1065 speed: 0.5,
1066 duration: 0.0,
1067 sync: 0.0,
1068 effect: Some(LightEffectKind::Glow),
1069 }];
1070
1071 let mut commands_with_lights: CommandBuffer<512> = CommandBuffer::new();
1072 engine
1073 .record_bsp_with_sector_lights(
1074 &world,
1075 &mut scratch_b,
1076 &mut commands_with_lights,
1077 Some(&lights),
1078 1.0,
1079 None,
1080 )
1081 .unwrap();
1082 let lit_draw_count = commands_with_lights
1083 .iter()
1084 .filter(|c| matches!(c, RenderCommand::Draw(_)))
1085 .count();
1086
1087 assert_eq!(base_draw_count, lit_draw_count);
1089 }
1090
1091 #[test]
1092 fn coverage_buffer_basic() {
1093 let mut data = [0u8; CoverageBuffer::bytes_for(32, 32)];
1094 let mut cov = CoverageBuffer::new(&mut data, 32, 32).unwrap();
1095 cov.clear();
1096 assert!(!cov.is_full());
1097 assert!(!cov.is_covered(5, 5));
1098 cov.mark_covered(5, 5);
1099 assert!(cov.is_covered(5, 5));
1100 }
1101
1102 #[test]
1103 fn clip_and_project_triangle_inside_frustum_emits_one() {
1104 let tri = [
1105 ClipVert {
1106 clip: nalgebra::Vector4::new(-0.5, -0.5, 0.0, 1.0),
1107 uv: [0.0, 0.0],
1108 lm_uv: [0.0, 0.0],
1109 },
1110 ClipVert {
1111 clip: nalgebra::Vector4::new(0.5, -0.5, 0.0, 1.0),
1112 uv: [1.0, 0.0],
1113 lm_uv: [1.0, 0.0],
1114 },
1115 ClipVert {
1116 clip: nalgebra::Vector4::new(0.0, 0.5, 0.0, 1.0),
1117 uv: [0.5, 1.0],
1118 lm_uv: [0.5, 1.0],
1119 },
1120 ];
1121 let out = clip_and_project(tri, 320, 240, 0.1, 40.0);
1122 assert_eq!(out.len(), 1);
1123 }
1124
1125 #[test]
1126 fn clip_and_project_triangle_crossing_side_plane_is_clipped() {
1127 let tri = [
1128 ClipVert {
1129 clip: nalgebra::Vector4::new(0.0, -0.2, 0.0, 1.0),
1130 uv: [0.0, 0.0],
1131 lm_uv: [0.0, 0.0],
1132 },
1133 ClipVert {
1134 clip: nalgebra::Vector4::new(1.6, 0.0, 0.0, 1.0), uv: [1.0, 0.5],
1136 lm_uv: [1.0, 0.5],
1137 },
1138 ClipVert {
1139 clip: nalgebra::Vector4::new(0.0, 0.4, 0.0, 1.0),
1140 uv: [0.0, 1.0],
1141 lm_uv: [0.0, 1.0],
1142 },
1143 ];
1144 let out = clip_and_project(tri, 320, 240, 0.1, 40.0);
1145 assert!(!out.is_empty(), "expected clipped output triangles");
1146 for tri in out.iter() {
1147 for p in tri.points {
1148 assert!(p.x >= 0 && p.x <= 320);
1149 assert!(p.y >= 0 && p.y <= 240);
1150 }
1151 }
1152 }
1153
1154 #[test]
1155 fn clip_and_project_triangle_behind_near_plane_is_discarded() {
1156 let tri = [
1157 ClipVert {
1158 clip: nalgebra::Vector4::new(-0.2, -0.2, -2.0, 1.0),
1159 uv: [0.0, 0.0],
1160 lm_uv: [0.0, 0.0],
1161 },
1162 ClipVert {
1163 clip: nalgebra::Vector4::new(0.2, -0.2, -2.2, 1.0),
1164 uv: [1.0, 0.0],
1165 lm_uv: [1.0, 0.0],
1166 },
1167 ClipVert {
1168 clip: nalgebra::Vector4::new(0.0, 0.2, -2.1, 1.0),
1169 uv: [0.5, 1.0],
1170 lm_uv: [0.5, 1.0],
1171 },
1172 ];
1173 let out = clip_and_project(tri, 320, 240, 0.1, 40.0);
1174 assert_eq!(out.len(), 0);
1175 }
1176}