1use roxlap_formats::kv6::Kv6;
23use roxlap_formats::sprite::{Sprite, SPRITE_FLAG_INVISIBLE, SPRITE_FLAG_NO_Z};
24use roxlap_formats::voxel_clip::{DecodedClip, VoxelFrame};
25
26use crate::camera_math::CameraState;
27use crate::dda::{dda_setup, intersect_aabb, min_axis, pixel_ray, shade};
28use crate::opticast::OpticastSettings;
29use crate::raster_target::RasterTarget;
30
31const NEAR_Z: f32 = 1.0;
34
35#[inline]
41fn full_bright(col: u32) -> u32 {
42 (col & 0x00ff_ffff) | 0x8000_0000
43}
44
45pub struct SpriteDense {
54 dims: [i32; 3],
55 occ: Vec<bool>,
56 col: Vec<u32>,
57 pivot: [f32; 3],
58}
59
60impl SpriteDense {
61 #[must_use]
63 #[allow(clippy::cast_possible_wrap)]
64 pub fn from_kv6(kv6: &Kv6) -> Self {
65 let dims = [kv6.xsiz as i32, kv6.ysiz as i32, kv6.zsiz as i32];
66 let n = (dims[0].max(0) * dims[1].max(0) * dims[2].max(0)) as usize;
67 let mut occ = vec![false; n];
68 let mut col = vec![0u32; n];
69 let mut vi = 0usize;
70 for x in 0..kv6.xsiz as usize {
71 for y in 0..kv6.ysiz as usize {
72 let cnt = usize::from(kv6.ylen[x][y]);
73 for _ in 0..cnt {
74 let v = kv6.voxels[vi];
75 vi += 1;
76 let z = i32::from(v.z);
77 if z >= 0 && z < dims[2] {
78 let idx = ((x as i32 * dims[1] + y as i32) * dims[2] + z) as usize;
79 occ[idx] = true;
80 col[idx] = full_bright(v.col);
81 }
82 }
83 }
84 }
85 Self {
86 dims,
87 occ,
88 col,
89 pivot: [kv6.xpiv, kv6.ypiv, kv6.zpiv],
90 }
91 }
92
93 #[must_use]
99 #[allow(clippy::cast_possible_wrap)]
100 pub fn from_voxel_frame(frame: &VoxelFrame, dims: [u32; 3], pivot: [f32; 3]) -> Self {
101 let (mx, my, mz) = (dims[0], dims[1], dims[2]);
102 let owpc = mz.div_ceil(32).max(1) as usize;
103 let n = (mx * my * mz) as usize;
104 let mut occ = vec![false; n];
105 let mut col = vec![0u32; n];
106 for col_idx in 0..(mx * my) as usize {
107 let x = col_idx as u32 % mx;
108 let y = col_idx as u32 / mx;
109 let run_start = frame.color_offsets[col_idx] as usize;
110 let mut k = 0usize;
111 for z in 0..mz {
112 let word = frame.occupancy[col_idx * owpc + (z >> 5) as usize];
113 if (word >> (z & 31)) & 1 != 0 {
114 let idx = (((x * my + y) * mz) + z) as usize;
115 occ[idx] = true;
116 col[idx] = full_bright(frame.colors[run_start + k]);
117 k += 1;
118 }
119 }
120 }
121 Self {
122 dims: [mx as i32, my as i32, mz as i32],
123 occ,
124 col,
125 pivot,
126 }
127 }
128
129 #[inline]
130 #[allow(clippy::cast_sign_loss)]
131 fn at(&self, c: [i32; 3]) -> Option<u32> {
132 let idx = ((c[0] * self.dims[1] + c[1]) * self.dims[2] + c[2]) as usize;
133 self.occ[idx].then(|| self.col[idx])
134 }
135}
136
137fn invert_basis(s: [f32; 3], h: [f32; 3], f: [f32; 3]) -> Option<[[f32; 3]; 3]> {
140 let det = s[0] * (h[1] * f[2] - f[1] * h[2]) - h[0] * (s[1] * f[2] - f[1] * s[2])
141 + f[0] * (s[1] * h[2] - h[1] * s[2]);
142 if det.abs() < 1e-12 {
143 return None;
144 }
145 let inv = 1.0 / det;
146 Some([
147 [
148 (h[1] * f[2] - f[1] * h[2]) * inv,
149 -(h[0] * f[2] - f[0] * h[2]) * inv,
150 (h[0] * f[1] - f[0] * h[1]) * inv,
151 ],
152 [
153 -(s[1] * f[2] - f[1] * s[2]) * inv,
154 (s[0] * f[2] - f[0] * s[2]) * inv,
155 -(s[0] * f[1] - f[0] * s[1]) * inv,
156 ],
157 [
158 (s[1] * h[2] - h[1] * s[2]) * inv,
159 -(s[0] * h[2] - h[0] * s[2]) * inv,
160 (s[0] * h[1] - h[0] * s[1]) * inv,
161 ],
162 ])
163}
164
165#[inline]
166fn mat_apply(m: &[[f32; 3]; 3], v: [f32; 3]) -> [f32; 3] {
167 [
168 m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2],
169 m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2],
170 m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2],
171 ]
172}
173
174#[allow(clippy::cast_possible_truncation)]
178fn cast_local(dense: &SpriteDense, origin: [f32; 3], dir: [f32; 3]) -> Option<(u32, f32)> {
179 #[allow(clippy::cast_precision_loss)]
180 let hi = [
181 dense.dims[0] as f32,
182 dense.dims[1] as f32,
183 dense.dims[2] as f32,
184 ];
185 let (t0, t1) = intersect_aabb(origin, dir, [0.0; 3], hi)?;
186 let start = t0 + 1e-4;
187 let p = [
188 origin[0] + dir[0] * start,
189 origin[1] + dir[1] * start,
190 origin[2] + dir[2] * start,
191 ];
192 let mut cell = [
193 (p[0].floor() as i32).clamp(0, dense.dims[0] - 1),
194 (p[1].floor() as i32).clamp(0, dense.dims[1] - 1),
195 (p[2].floor() as i32).clamp(0, dense.dims[2] - 1),
196 ];
197 let (step, mut t_max, t_delta) = dda_setup(origin, dir, cell, 1.0);
198 let mut t_curr = t0;
199 let max_steps = (dense.dims[0] + dense.dims[1] + dense.dims[2]) as usize + 8;
200 for _ in 0..max_steps {
201 if cell[0] < 0
202 || cell[0] >= dense.dims[0]
203 || cell[1] < 0
204 || cell[1] >= dense.dims[1]
205 || cell[2] < 0
206 || cell[2] >= dense.dims[2]
207 || t_curr > t1
208 {
209 return None;
210 }
211 if let Some(color) = dense.at(cell) {
212 return Some((color, t_curr));
213 }
214 let axis = min_axis(t_max);
215 t_curr = t_max[axis];
216 cell[axis] += step[axis];
217 t_max[axis] += t_delta[axis];
218 }
219 None
220}
221
222#[allow(
232 clippy::too_many_arguments,
233 clippy::cast_possible_truncation,
234 clippy::cast_sign_loss
235)]
236#[must_use]
237pub fn draw_sprite_dda(
238 fb: &mut [u32],
239 zb: &mut [f32],
240 pitch_pixels: usize,
241 width: u32,
242 height: u32,
243 cam: &CameraState,
244 settings: &OpticastSettings,
245 sprite: &Sprite,
246) -> u32 {
247 if sprite.flags & SPRITE_FLAG_INVISIBLE != 0 {
248 return 0;
249 }
250 let dense = SpriteDense::from_kv6(&sprite.kv6);
253 draw_sprite_dense(
254 fb,
255 zb,
256 pitch_pixels,
257 width,
258 height,
259 cam,
260 settings,
261 &dense,
262 sprite.p,
263 sprite.s,
264 sprite.h,
265 sprite.f,
266 sprite.flags,
267 )
268}
269
270#[allow(
276 clippy::too_many_arguments,
277 clippy::cast_possible_truncation,
278 clippy::cast_sign_loss
279)]
280#[must_use]
281pub fn draw_sprite_dense(
282 fb: &mut [u32],
283 zb: &mut [f32],
284 pitch_pixels: usize,
285 width: u32,
286 height: u32,
287 cam: &CameraState,
288 settings: &OpticastSettings,
289 dense: &SpriteDense,
290 pos: [f32; 3],
291 s: [f32; 3],
292 h: [f32; 3],
293 f: [f32; 3],
294 flags: u32,
295) -> u32 {
296 if flags & SPRITE_FLAG_INVISIBLE != 0 || dense.occ.is_empty() {
297 return 0;
298 }
299 let Some(minv) = invert_basis(s, h, f) else {
300 return 0;
301 };
302 let pivot = dense.pivot;
303 let no_z = flags & SPRITE_FLAG_NO_Z != 0;
304
305 let Some(rect) = project_screen_rect(dense, pos, s, h, f, cam, settings, width, height) else {
307 return 0;
308 };
309
310 debug_assert_eq!(fb.len(), zb.len());
311 let target = RasterTarget::new(fb, zb);
312 let mut written = 0u32;
313 for py in rect.1..rect.3 {
314 let row = py as usize * pitch_pixels;
315 for px in rect.0..rect.2 {
316 let (origin, dir) = pixel_ray(cam, settings, px, py);
317 let rel = [origin[0] - pos[0], origin[1] - pos[1], origin[2] - pos[2]];
319 let ol = mat_apply(&minv, rel);
320 let origin_local = [ol[0] + pivot[0], ol[1] + pivot[1], ol[2] + pivot[2]];
321 let dir_local = mat_apply(&minv, dir);
322 let Some((color, t)) = cast_local(dense, origin_local, dir_local) else {
323 continue;
324 };
325 let fwd_dot =
326 dir[0] * cam.forward[0] + dir[1] * cam.forward[1] + dir[2] * cam.forward[2];
327 let depth = t * fwd_dot;
328 if depth < NEAR_Z {
329 continue;
330 }
331 let lit = shade(color, 0);
332 let idx = row + px as usize;
333 let wrote = unsafe {
336 if no_z {
337 target.write_color(idx, lit);
338 target.write_depth(idx, depth);
339 true
340 } else {
341 target.z_test_write(idx, lit, depth)
342 }
343 };
344 written += u32::from(wrote);
345 }
346 }
347 written
348}
349
350#[allow(
354 clippy::cast_possible_truncation,
355 clippy::cast_sign_loss,
356 clippy::cast_precision_loss
357)]
358fn project_screen_rect(
359 dense: &SpriteDense,
360 pos: [f32; 3],
361 s: [f32; 3],
362 h: [f32; 3],
363 f: [f32; 3],
364 cam: &CameraState,
365 settings: &OpticastSettings,
366 width: u32,
367 height: u32,
368) -> Option<(u32, u32, u32, u32)> {
369 let (xs, ys, zs) = (
370 dense.dims[0] as f32,
371 dense.dims[1] as f32,
372 dense.dims[2] as f32,
373 );
374 let (xp, yp, zp) = (dense.pivot[0], dense.pivot[1], dense.pivot[2]);
375 let (mut x0, mut y0, mut x1, mut y1) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
376 let mut all_front = true;
377 for &cx in &[0.0, xs] {
378 for &cy in &[0.0, ys] {
379 for &cz in &[0.0, zs] {
380 let lx = cx - xp;
382 let ly = cy - yp;
383 let lz = cz - zp;
384 let world = [
385 pos[0] + lx * s[0] + ly * h[0] + lz * f[0],
386 pos[1] + lx * s[1] + ly * h[1] + lz * f[1],
387 pos[2] + lx * s[2] + ly * h[2] + lz * f[2],
388 ];
389 let rel = [
390 world[0] - cam.pos[0],
391 world[1] - cam.pos[1],
392 world[2] - cam.pos[2],
393 ];
394 let cz_cam =
395 rel[0] * cam.forward[0] + rel[1] * cam.forward[1] + rel[2] * cam.forward[2];
396 if cz_cam < NEAR_Z {
397 all_front = false;
398 continue;
399 }
400 let cx_cam = rel[0] * cam.right[0] + rel[1] * cam.right[1] + rel[2] * cam.right[2];
401 let cy_cam = rel[0] * cam.down[0] + rel[1] * cam.down[1] + rel[2] * cam.down[2];
402 let sx = settings.hx + cx_cam / cz_cam * settings.hz;
403 let sy = settings.hy + cy_cam / cz_cam * settings.hz;
404 x0 = x0.min(sx);
405 y0 = y0.min(sy);
406 x1 = x1.max(sx);
407 y1 = y1.max(sy);
408 }
409 }
410 }
411 let (w, h) = (width as f32, height as f32);
412 let (rx0, ry0, rx1, ry1) = if all_front {
413 (
414 (x0 - 1.0).max(0.0),
415 (y0 - 1.0).max(0.0),
416 (x1 + 1.0).min(w),
417 (y1 + 1.0).min(h),
418 )
419 } else {
420 (0.0, 0.0, w, h)
422 };
423 if rx0 >= rx1 || ry0 >= ry1 {
424 return None;
425 }
426 Some((rx0 as u32, ry0 as u32, rx1.ceil() as u32, ry1.ceil() as u32))
427}
428
429pub struct ClipFlipbook {
436 frames: Vec<SpriteDense>,
437}
438
439impl ClipFlipbook {
440 #[must_use]
443 pub fn empty() -> Self {
444 Self { frames: Vec::new() }
445 }
446
447 #[must_use]
449 pub fn from_decoded(clip: &DecodedClip) -> Self {
450 let frames = clip
451 .frames
452 .iter()
453 .map(|frame| SpriteDense::from_voxel_frame(frame, clip.dims, clip.pivot))
454 .collect();
455 Self { frames }
456 }
457
458 #[must_use]
459 pub fn frame_count(&self) -> usize {
460 self.frames.len()
461 }
462
463 #[must_use]
465 pub fn frame(&self, frame: usize) -> Option<&SpriteDense> {
466 self.frames.get(frame)
467 }
468
469 pub fn set_frame(&mut self, frame: usize, dense: SpriteDense) -> bool {
473 match self.frames.get_mut(frame) {
474 Some(slot) => {
475 *slot = dense;
476 true
477 }
478 None => false,
479 }
480 }
481
482 #[allow(clippy::too_many_arguments)]
486 #[must_use]
487 pub fn draw_frame(
488 &self,
489 fb: &mut [u32],
490 zb: &mut [f32],
491 pitch_pixels: usize,
492 width: u32,
493 height: u32,
494 cam: &CameraState,
495 settings: &OpticastSettings,
496 frame: usize,
497 pos: [f32; 3],
498 s: [f32; 3],
499 h: [f32; 3],
500 f: [f32; 3],
501 flags: u32,
502 ) -> u32 {
503 let Some(dense) = self.frames.get(frame) else {
504 return 0;
505 };
506 draw_sprite_dense(
507 fb,
508 zb,
509 pitch_pixels,
510 width,
511 height,
512 cam,
513 settings,
514 dense,
515 pos,
516 s,
517 h,
518 f,
519 flags,
520 )
521 }
522}
523
524#[cfg(test)]
525mod tests {
526 use super::*;
527 use crate::camera_math;
528 use crate::Camera;
529 use roxlap_formats::kv6::Kv6;
530 use roxlap_formats::sprite::Sprite;
531 use roxlap_formats::voxel_clip::{LoopMode, VoxelClip, VoxelFrame};
532
533 fn settings(w: u32, h: u32) -> OpticastSettings {
534 OpticastSettings::for_oracle_framebuffer(w, h)
535 }
536
537 fn cam_looking_y() -> Camera {
539 Camera {
540 pos: [0.0, 0.0, 0.0],
541 right: [1.0, 0.0, 0.0],
542 down: [0.0, 0.0, 1.0],
543 forward: [0.0, 1.0, 0.0],
544 }
545 }
546
547 fn clip_frame(dims: [u32; 3], fill: impl Fn(u32, u32, u32) -> Option<u32>) -> VoxelFrame {
549 let owpc = dims[2].div_ceil(32).max(1) as usize;
550 let cols = (dims[0] * dims[1]) as usize;
551 let mut occupancy = vec![0u32; cols * owpc];
552 let mut color_offsets = vec![0u32; cols + 1];
553 let mut colors = Vec::new();
554 for y in 0..dims[1] {
555 for x in 0..dims[0] {
556 let col = (x + y * dims[0]) as usize;
557 color_offsets[col] = colors.len() as u32;
558 for z in 0..dims[2] {
559 if let Some(c) = fill(x, y, z) {
560 occupancy[col * owpc + (z >> 5) as usize] |= 1u32 << (z & 31);
561 colors.push(c);
562 }
563 }
564 }
565 }
566 color_offsets[cols] = colors.len() as u32;
567 VoxelFrame {
568 occupancy,
569 colors,
570 color_offsets,
571 }
572 }
573
574 #[test]
579 fn clip_flipbook_frames_render_differently() {
580 let dims = [8u32, 8, 8];
581 let f0 = clip_frame(dims, |_x, _y, z| (z < 4).then_some(0x00FF_0000)); let f1 = clip_frame(dims, |_x, _y, z| (z >= 4).then_some(0x0000_FF00)); let clip = VoxelClip::from_frames(
584 dims,
585 [4.0, 4.0, 4.0],
586 1.0,
587 LoopMode::Loop,
588 &[f0, f1],
589 &[],
590 33,
591 0,
592 );
593 let decoded = clip.decode().expect("decode");
594 let book = ClipFlipbook::from_decoded(&decoded);
595 assert_eq!(book.frame_count(), 2);
596 assert!(book.frame(0).is_some() && book.frame(2).is_none());
597
598 let (w, h) = (64u32, 64u32);
599 let n = (w * h) as usize;
600 let cam = cam_looking_y();
601 let cs = camera_math::derive(&cam, w, h, 32.0, 32.0, 32.0);
602 let cfg = settings(w, h);
603 let pose = [0.0, 40.0, 0.0];
604 let (s, hh, f) = ([1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]);
605
606 let render = |frame: usize| -> Vec<u32> {
607 let mut fb = vec![0u32; n];
608 let mut zb = vec![f32::INFINITY; n];
609 let wrote = book.draw_frame(
610 &mut fb, &mut zb, w as usize, w, h, &cs, &cfg, frame, pose, s, hh, f, 0,
611 );
612 assert!(wrote > 0, "frame {frame} should draw some pixels");
613 fb
614 };
615 let fb0 = render(0);
616 let fb1 = render(1);
617 assert_ne!(fb0, fb1, "distinct frames must render distinct pixels");
618 assert!(fb0.iter().any(|&p| (p & 0x00FF_0000) != 0));
621 assert!(fb1.iter().any(|&p| (p & 0x0000_FF00) != 0));
622 let mut fb = vec![0u32; n];
624 let mut zb = vec![f32::INFINITY; n];
625 assert_eq!(
626 book.draw_frame(&mut fb, &mut zb, w as usize, w, h, &cs, &cfg, 9, pose, s, hh, f, 0),
627 0
628 );
629 }
630
631 #[test]
632 fn clip_flipbook_set_frame_replaces_one_frame() {
633 let dims = [8u32, 8, 8];
636 let f0 = clip_frame(dims, |_, _, z| (z < 4).then_some(0x00FF_0000)); let f1 = clip_frame(dims, |_, _, z| (z >= 4).then_some(0x0000_FF00)); let clip =
639 VoxelClip::from_frames(dims, [4.0; 3], 1.0, LoopMode::Loop, &[f0, f1], &[], 33, 0);
640 let decoded = clip.decode().unwrap();
641 let mut book = ClipFlipbook::from_decoded(&decoded);
642
643 let (w, h) = (64u32, 64u32);
644 let n = (w * h) as usize;
645 let cs = camera_math::derive(&cam_looking_y(), w, h, 32.0, 32.0, 32.0);
646 let cfg = settings(w, h);
647 let render0 = |b: &ClipFlipbook| -> Vec<u32> {
648 let mut fb = vec![0u32; n];
649 let mut zb = vec![f32::INFINITY; n];
650 let _ = b.draw_frame(
651 &mut fb,
652 &mut zb,
653 w as usize,
654 w,
655 h,
656 &cs,
657 &cfg,
658 0,
659 [0.0, 40.0, 0.0],
660 [1.0, 0.0, 0.0],
661 [0.0, 1.0, 0.0],
662 [0.0, 0.0, 1.0],
663 0,
664 );
665 fb
666 };
667
668 let before = render0(&book);
669 assert!(
670 before.iter().any(|&p| (p & 0x00FF_0000) != 0),
671 "frame 0 is red"
672 );
673
674 let replacement = SpriteDense::from_voxel_frame(&decoded.frames[1], dims, decoded.pivot);
676 assert!(book.set_frame(0, replacement));
677 let extra = SpriteDense::from_voxel_frame(&decoded.frames[1], dims, decoded.pivot);
678 assert!(!book.set_frame(9, extra), "out-of-range set_frame is false");
679
680 let after = render0(&book);
681 assert!(
682 after.iter().any(|&p| (p & 0x0000_FF00) != 0),
683 "frame 0 now green"
684 );
685 assert_ne!(before, after);
686 }
687
688 #[test]
691 fn cube_sprite_renders() {
692 let kv6 = Kv6::solid_cube(8, 0x80_C0_40_20);
693 let sprite = Sprite::axis_aligned(kv6, [0.0, 40.0, 0.0]);
694 let (w, h) = (64u32, 64u32);
695 let n = (w * h) as usize;
696 let mut fb = vec![0u32; n];
697 let mut zb = vec![f32::INFINITY; n];
698 let cam = cam_looking_y();
699 let cs = camera_math::derive(&cam, w, h, 32.0, 32.0, 32.0);
700 let wrote = draw_sprite_dda(
701 &mut fb,
702 &mut zb,
703 w as usize,
704 w,
705 h,
706 &cs,
707 &settings(w, h),
708 &sprite,
709 );
710
711 assert!(wrote > 20, "cube should cover many pixels (got {wrote})");
712 let centre = (h / 2 * w + w / 2) as usize;
713 assert_eq!(
714 fb[centre] & 0x00ff_ffff,
715 0x00_C0_40_20,
716 "got {:08x}",
717 fb[centre]
718 );
719 assert!(
721 (zb[centre] - 36.0).abs() < 3.0,
722 "centre depth {} not ≈ 36",
723 zb[centre]
724 );
725 }
726
727 #[test]
732 fn zero_high_byte_sprite_not_black() {
733 let kv6 = Kv6::solid_cube(8, 0x00_C0_40_20);
734 let sprite = Sprite::axis_aligned(kv6, [0.0, 40.0, 0.0]);
735 let (w, h) = (64u32, 64u32);
736 let n = (w * h) as usize;
737 let mut fb = vec![0u32; n];
738 let mut zb = vec![f32::INFINITY; n];
739 let cam = cam_looking_y();
740 let cs = camera_math::derive(&cam, w, h, 32.0, 32.0, 32.0);
741 let wrote = draw_sprite_dda(
742 &mut fb,
743 &mut zb,
744 w as usize,
745 w,
746 h,
747 &cs,
748 &settings(w, h),
749 &sprite,
750 );
751 assert!(wrote > 20, "cube should cover many pixels (got {wrote})");
752 let centre = (h / 2 * w + w / 2) as usize;
753 assert_eq!(
754 fb[centre] & 0x00ff_ffff,
755 0x00_C0_40_20,
756 "zero-high-byte sprite rendered as {:08x} (black bug)",
757 fb[centre]
758 );
759 }
760
761 #[test]
764 fn sprite_respects_zbuffer() {
765 let kv6 = Kv6::solid_cube(8, 0x80_FF_FF_FF);
766 let sprite = Sprite::axis_aligned(kv6, [0.0, 40.0, 0.0]);
767 let (w, h) = (32u32, 32u32);
768 let n = (w * h) as usize;
769 let cam = cam_looking_y();
770 let cs = camera_math::derive(&cam, w, h, 16.0, 16.0, 16.0);
771 let centre = (h / 2 * w + w / 2) as usize;
772
773 let mut fb = vec![0u32; n];
775 let mut zb = vec![f32::INFINITY; n];
776 fb[centre] = 0x80_11_22_33;
777 zb[centre] = 10.0;
778 let _ = draw_sprite_dda(
779 &mut fb,
780 &mut zb,
781 w as usize,
782 w,
783 h,
784 &cs,
785 &settings(w, h),
786 &sprite,
787 );
788 assert_eq!(
789 fb[centre], 0x80_11_22_33,
790 "near terrain must occlude sprite"
791 );
792
793 let mut fb2 = vec![0u32; n];
795 let mut zb2 = vec![f32::INFINITY; n];
796 fb2[centre] = 0x80_11_22_33;
797 zb2[centre] = 100.0;
798 let _ = draw_sprite_dda(
799 &mut fb2,
800 &mut zb2,
801 w as usize,
802 w,
803 h,
804 &cs,
805 &settings(w, h),
806 &sprite,
807 );
808 assert_ne!(fb2[centre], 0x80_11_22_33, "sprite must beat far terrain");
809 assert!(zb2[centre] < 100.0, "sprite depth must replace terrain's");
810 }
811
812 fn covered_rect(fb: &[u32], w: u32, h: u32) -> (u32, u32, u32, u32) {
815 let (mut x0, mut y0, mut x1, mut y1) = (w, h, 0u32, 0u32);
816 for py in 0..h {
817 for px in 0..w {
818 if fb[(py * w + px) as usize] & 0x00ff_ffff != 0 {
819 x0 = x0.min(px);
820 y0 = y0.min(py);
821 x1 = x1.max(px);
822 y1 = y1.max(py);
823 }
824 }
825 }
826 (x0, y0, x1, y1)
827 }
828
829 #[test]
834 fn posed_basis_reorients_silhouette() {
835 let kv6 = Kv6::solid_box(16, 4, 4, 0x80_C0_40_20);
838 let (w, h) = (64u32, 64u32);
839 let n = (w * h) as usize;
840 let cam = cam_looking_y();
841 let cs = camera_math::derive(&cam, w, h, 32.0, 32.0, 32.0);
842
843 let aa = Sprite::axis_aligned(kv6.clone(), [0.0, 40.0, 0.0]);
845 let mut fb = vec![0u32; n];
846 let mut zb = vec![f32::INFINITY; n];
847 let _ = draw_sprite_dda(
848 &mut fb,
849 &mut zb,
850 w as usize,
851 w,
852 h,
853 &cs,
854 &settings(w, h),
855 &aa,
856 );
857 let (ax0, ay0, ax1, ay1) = covered_rect(&fb, w, h);
858 let aa_wide = (ax1 - ax0) as i32 - (ay1 - ay0) as i32;
859 assert!(
860 aa_wide > 4,
861 "axis-aligned box should be wider than tall (got w-h={aa_wide})"
862 );
863
864 let mut posed = aa.clone();
867 posed.s = [0.0, 0.0, 1.0]; posed.h = [0.0, 1.0, 0.0]; posed.f = [1.0, 0.0, 0.0]; let mut fb2 = vec![0u32; n];
871 let mut zb2 = vec![f32::INFINITY; n];
872 let _ = draw_sprite_dda(
873 &mut fb2,
874 &mut zb2,
875 w as usize,
876 w,
877 h,
878 &cs,
879 &settings(w, h),
880 &posed,
881 );
882 let (bx0, by0, bx1, by1) = covered_rect(&fb2, w, h);
883 let posed_tall = (by1 - by0) as i32 - (bx1 - bx0) as i32;
884 assert!(
885 posed_tall > 4,
886 "posed box should be taller than wide (got h-w={posed_tall})"
887 );
888 }
889
890 #[test]
893 fn degenerate_basis_draws_nothing() {
894 let kv6 = Kv6::solid_cube(8, 0x80_FF_FF_FF);
895 let mut sprite = Sprite::axis_aligned(kv6, [0.0, 40.0, 0.0]);
896 sprite.f = sprite.s; let (w, h) = (32u32, 32u32);
898 let n = (w * h) as usize;
899 let mut fb = vec![0u32; n];
900 let mut zb = vec![f32::INFINITY; n];
901 let cam = cam_looking_y();
902 let cs = camera_math::derive(&cam, w, h, 16.0, 16.0, 16.0);
903 let wrote = draw_sprite_dda(
904 &mut fb,
905 &mut zb,
906 w as usize,
907 w,
908 h,
909 &cs,
910 &settings(w, h),
911 &sprite,
912 );
913 assert_eq!(wrote, 0, "singular basis must skip, not panic");
914 }
915
916 #[test]
918 fn invisible_sprite_skipped() {
919 let kv6 = Kv6::solid_cube(8, 0x80_FF_FF_FF);
920 let mut sprite = Sprite::axis_aligned(kv6, [0.0, 40.0, 0.0]);
921 sprite.flags |= roxlap_formats::sprite::SPRITE_FLAG_INVISIBLE;
922 let (w, h) = (32u32, 32u32);
923 let n = (w * h) as usize;
924 let mut fb = vec![0u32; n];
925 let mut zb = vec![f32::INFINITY; n];
926 let cam = cam_looking_y();
927 let cs = camera_math::derive(&cam, w, h, 16.0, 16.0, 16.0);
928 let wrote = draw_sprite_dda(
929 &mut fb,
930 &mut zb,
931 w as usize,
932 w,
933 h,
934 &cs,
935 &settings(w, h),
936 &sprite,
937 );
938 assert_eq!(wrote, 0);
939 }
940}