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(crate::Z_MAX_VALUE))?;
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) => crate::clear_zbuffer(frame.zbuffer, *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 crate::clear_zbuffer(frame.zbuffer, crate::Z_MAX_VALUE);
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::TexturedGouraudTriangleWithDepth { points: pts, .. }
781 | DrawPrimitive::LightmappedTriangle { points: pts, .. } => {
782 let min_x = pts.iter().map(|p| p.x).min().unwrap_or(0);
783 let min_y = pts.iter().map(|p| p.y).min().unwrap_or(0);
784 let max_x = pts.iter().map(|p| p.x).max().unwrap_or(0);
785 let max_y = pts.iter().map(|p| p.y).max().unwrap_or(0);
786 (min_x, min_y, max_x, max_y)
787 }
788 }
789}
790
791#[allow(dead_code)]
809pub mod test_level {
810 use super::data::*;
811
812 pub static PLANES: [Plane; 1] = [Plane {
813 normal: [1.0, 0.0, 0.0],
814 dist: 0.0,
815 }];
816
817 pub static NODES: [Node; 1] = [Node {
819 plane: 0,
820 children: [!1i32, !0i32],
821 mins: [-5, -2, -3],
822 maxs: [5, 2, 3],
823 first_face: 0,
824 num_faces: 0,
825 }];
826
827 pub static LEAVES: [Leaf; 2] = [
828 Leaf {
829 cluster: 0,
830 mins: [-5, -2, -3],
831 maxs: [0, 2, 3],
832 first_marksurface: 0,
833 num_marksurfaces: 2,
834 },
835 Leaf {
836 cluster: 1,
837 mins: [0, -2, -3],
838 maxs: [5, 2, 3],
839 first_marksurface: 2,
840 num_marksurfaces: 2,
841 },
842 ];
843
844 pub static FACES: [Face; 4] = [
846 Face {
848 first_vert: 0,
849 num_verts: 4,
850 texture_id: 0,
851 lightmap_id: 0xFFFF,
852 plane: 0,
853 side: 0,
854 sector_light_id: u16::MAX,
855 },
856 Face {
858 first_vert: 4,
859 num_verts: 4,
860 texture_id: 0,
861 lightmap_id: 0xFFFF,
862 plane: 0,
863 side: 0,
864 sector_light_id: u16::MAX,
865 },
866 Face {
868 first_vert: 8,
869 num_verts: 4,
870 texture_id: 0,
871 lightmap_id: 0xFFFF,
872 plane: 0,
873 side: 0,
874 sector_light_id: u16::MAX,
875 },
876 Face {
878 first_vert: 12,
879 num_verts: 4,
880 texture_id: 0,
881 lightmap_id: 0xFFFF,
882 plane: 0,
883 side: 0,
884 sector_light_id: u16::MAX,
885 },
886 ];
887
888 pub static MARKSURFACES: [u16; 4] = [0, 1, 2, 3];
889
890 pub static VERTICES: [[f32; 3]; 16] = [
892 [-5.0, -2.0, -3.0],
894 [0.0, -2.0, -3.0],
895 [0.0, -2.0, 3.0],
896 [-5.0, -2.0, 3.0],
897 [-5.0, 2.0, -3.0],
899 [0.0, 2.0, -3.0],
900 [0.0, 2.0, 3.0],
901 [-5.0, 2.0, 3.0],
902 [0.0, -2.0, -3.0],
904 [5.0, -2.0, -3.0],
905 [5.0, -2.0, 3.0],
906 [0.0, -2.0, 3.0],
907 [0.0, 2.0, -3.0],
909 [5.0, 2.0, -3.0],
910 [5.0, 2.0, 3.0],
911 [0.0, 2.0, 3.0],
912 ];
913
914 pub static UVS: [[f32; 2]; 16] = [
915 [0.0, 0.0],
916 [1.0, 0.0],
917 [1.0, 1.0],
918 [0.0, 1.0],
919 [0.0, 0.0],
920 [1.0, 0.0],
921 [1.0, 1.0],
922 [0.0, 1.0],
923 [0.0, 0.0],
924 [1.0, 0.0],
925 [1.0, 1.0],
926 [0.0, 1.0],
927 [0.0, 0.0],
928 [1.0, 0.0],
929 [1.0, 1.0],
930 [0.0, 1.0],
931 ];
932
933 pub static VIS: [u8; 2] = [0b00000011, 0b00000011];
935 pub static VIS_OFFSETS: [u32; 2] = [0, 1];
936
937 pub fn world() -> BspWorld<'static> {
938 BspWorld::new(
939 &PLANES,
940 &NODES,
941 &LEAVES,
942 &FACES,
943 &MARKSURFACES,
944 &VERTICES,
945 &UVS,
946 &[], &VIS,
948 &VIS_OFFSETS,
949 2,
950 )
951 }
952}
953
954#[cfg(test)]
959mod tests {
960 extern crate std;
961 use super::*;
962 use crate::bsp::scratch::BspScratch;
963 use crate::bsp::test_level;
964 use crate::sector_lights::{LightEffectKind, SectorLight};
965 use nalgebra::Point3;
966
967 #[test]
968 fn test_level_leaf_location() {
969 let world = test_level::world();
970 let leaf_a = world.leaf_for_point([-2.0, 0.0, 0.0]);
971 let leaf_b = world.leaf_for_point([2.0, 0.0, 0.0]);
972 assert_eq!(leaf_a, 0);
973 assert_eq!(leaf_b, 1);
974 }
975
976 #[test]
977 fn test_level_cluster_visibility() {
978 let world = test_level::world();
979 assert!(world.cluster_visible(0, 0));
980 assert!(world.cluster_visible(0, 1));
981 assert!(world.cluster_visible(1, 0));
982 assert!(world.cluster_visible(1, 1));
983 }
984
985 #[test]
986 fn record_bsp_emits_draw_commands() {
987 let world = test_level::world();
988 let mut visframe = [0u32; 4];
989 let mut scratch = BspScratch::new(&mut visframe);
990
991 let mut engine = K3dengine::new(320, 240);
992 engine.camera.set_position(Point3::new(-2.0, 0.0, 0.0));
993 engine.camera.set_target(Point3::new(0.0, 0.0, 0.0));
994
995 let mut commands: CommandBuffer<512> = CommandBuffer::new();
996 let mut tel = BspTelemetry::default();
997
998 engine
999 .record_bsp(&world, &mut scratch, &mut commands, Some(&mut tel))
1000 .unwrap();
1001
1002 let draw_count = commands
1004 .iter()
1005 .filter(|c| matches!(c, RenderCommand::Draw(_)))
1006 .count();
1007 assert!(draw_count > 0, "expected draw commands, got 0");
1008 assert!(tel.faces_visible > 0);
1009 assert!(tel.triangles_emitted > 0);
1010 }
1011
1012 #[test]
1013 fn record_bsp_deduplicates_faces() {
1014 let world = test_level::world();
1015 let mut vf1 = [0u32; 4];
1016 let mut vf2 = [0u32; 4];
1017 let mut s1 = BspScratch::new(&mut vf1);
1018 let mut s2 = BspScratch::new(&mut vf2);
1019
1020 let mut engine = K3dengine::new(320, 240);
1021 engine.camera.set_position(Point3::new(-2.0, 0.0, 0.0));
1022 engine.camera.set_target(Point3::new(0.0, 0.0, 0.0));
1023
1024 let mut c1: CommandBuffer<512> = CommandBuffer::new();
1025 engine.record_bsp(&world, &mut s1, &mut c1, None).unwrap();
1026 let count1 = c1
1027 .iter()
1028 .filter(|c| matches!(c, RenderCommand::Draw(_)))
1029 .count();
1030
1031 let mut c2: CommandBuffer<512> = CommandBuffer::new();
1033 engine.record_bsp(&world, &mut s2, &mut c2, None).unwrap();
1034 let count2 = c2
1035 .iter()
1036 .filter(|c| matches!(c, RenderCommand::Draw(_)))
1037 .count();
1038
1039 assert_eq!(count1, count2);
1040 }
1041
1042 #[test]
1043 fn record_bsp_with_sector_lights_keeps_output_when_faces_unmapped() {
1044 let world = test_level::world();
1045 let mut vf_a = [0u32; 4];
1046 let mut vf_b = [0u32; 4];
1047 let mut scratch_a = BspScratch::new(&mut vf_a);
1048 let mut scratch_b = BspScratch::new(&mut vf_b);
1049
1050 let mut engine = K3dengine::new(320, 240);
1051 engine.camera.set_position(Point3::new(-2.0, 0.0, 0.0));
1052 engine.camera.set_target(Point3::new(0.0, 0.0, 0.0));
1053
1054 let mut commands_base: CommandBuffer<512> = CommandBuffer::new();
1055 engine
1056 .record_bsp(&world, &mut scratch_a, &mut commands_base, None)
1057 .unwrap();
1058 let base_draw_count = commands_base
1059 .iter()
1060 .filter(|c| matches!(c, RenderCommand::Draw(_)))
1061 .count();
1062
1063 let lights = [SectorLight {
1064 base: 255,
1065 alt: 48,
1066 speed: 0.5,
1067 duration: 0.0,
1068 sync: 0.0,
1069 effect: Some(LightEffectKind::Glow),
1070 }];
1071
1072 let mut commands_with_lights: CommandBuffer<512> = CommandBuffer::new();
1073 engine
1074 .record_bsp_with_sector_lights(
1075 &world,
1076 &mut scratch_b,
1077 &mut commands_with_lights,
1078 Some(&lights),
1079 1.0,
1080 None,
1081 )
1082 .unwrap();
1083 let lit_draw_count = commands_with_lights
1084 .iter()
1085 .filter(|c| matches!(c, RenderCommand::Draw(_)))
1086 .count();
1087
1088 assert_eq!(base_draw_count, lit_draw_count);
1090 }
1091
1092 #[test]
1093 fn coverage_buffer_basic() {
1094 let mut data = [0u8; CoverageBuffer::bytes_for(32, 32)];
1095 let mut cov = CoverageBuffer::new(&mut data, 32, 32).unwrap();
1096 cov.clear();
1097 assert!(!cov.is_full());
1098 assert!(!cov.is_covered(5, 5));
1099 cov.mark_covered(5, 5);
1100 assert!(cov.is_covered(5, 5));
1101 }
1102
1103 #[test]
1104 fn clip_and_project_triangle_inside_frustum_emits_one() {
1105 let tri = [
1106 ClipVert {
1107 clip: nalgebra::Vector4::new(-0.5, -0.5, 0.0, 1.0),
1108 uv: [0.0, 0.0],
1109 lm_uv: [0.0, 0.0],
1110 },
1111 ClipVert {
1112 clip: nalgebra::Vector4::new(0.5, -0.5, 0.0, 1.0),
1113 uv: [1.0, 0.0],
1114 lm_uv: [1.0, 0.0],
1115 },
1116 ClipVert {
1117 clip: nalgebra::Vector4::new(0.0, 0.5, 0.0, 1.0),
1118 uv: [0.5, 1.0],
1119 lm_uv: [0.5, 1.0],
1120 },
1121 ];
1122 let out = clip_and_project(tri, 320, 240, 0.1, 40.0);
1123 assert_eq!(out.len(), 1);
1124 }
1125
1126 #[test]
1127 fn clip_and_project_triangle_crossing_side_plane_is_clipped() {
1128 let tri = [
1129 ClipVert {
1130 clip: nalgebra::Vector4::new(0.0, -0.2, 0.0, 1.0),
1131 uv: [0.0, 0.0],
1132 lm_uv: [0.0, 0.0],
1133 },
1134 ClipVert {
1135 clip: nalgebra::Vector4::new(1.6, 0.0, 0.0, 1.0), uv: [1.0, 0.5],
1137 lm_uv: [1.0, 0.5],
1138 },
1139 ClipVert {
1140 clip: nalgebra::Vector4::new(0.0, 0.4, 0.0, 1.0),
1141 uv: [0.0, 1.0],
1142 lm_uv: [0.0, 1.0],
1143 },
1144 ];
1145 let out = clip_and_project(tri, 320, 240, 0.1, 40.0);
1146 assert!(!out.is_empty(), "expected clipped output triangles");
1147 for tri in out.iter() {
1148 for p in tri.points {
1149 assert!(p.x >= 0 && p.x <= 320);
1150 assert!(p.y >= 0 && p.y <= 240);
1151 }
1152 }
1153 }
1154
1155 #[test]
1156 fn clip_and_project_triangle_behind_near_plane_is_discarded() {
1157 let tri = [
1158 ClipVert {
1159 clip: nalgebra::Vector4::new(-0.2, -0.2, -2.0, 1.0),
1160 uv: [0.0, 0.0],
1161 lm_uv: [0.0, 0.0],
1162 },
1163 ClipVert {
1164 clip: nalgebra::Vector4::new(0.2, -0.2, -2.2, 1.0),
1165 uv: [1.0, 0.0],
1166 lm_uv: [1.0, 0.0],
1167 },
1168 ClipVert {
1169 clip: nalgebra::Vector4::new(0.0, 0.2, -2.1, 1.0),
1170 uv: [0.5, 1.0],
1171 lm_uv: [0.5, 1.0],
1172 },
1173 ];
1174 let out = clip_and_project(tri, 320, 240, 0.1, 40.0);
1175 assert_eq!(out.len(), 0);
1176 }
1177}