1use roxlap_formats::kv6::Kv6;
23use roxlap_formats::sprite::{Sprite, SPRITE_FLAG_INVISIBLE, SPRITE_FLAG_NO_Z};
24
25use crate::camera_math::CameraState;
26use crate::dda::{dda_setup, intersect_aabb, min_axis, pixel_ray, shade};
27use crate::opticast::OpticastSettings;
28use crate::raster_target::RasterTarget;
29
30const NEAR_Z: f32 = 1.0;
33
34struct Kv6Dense {
39 dims: [i32; 3],
40 occ: Vec<bool>,
41 col: Vec<u32>,
42}
43
44impl Kv6Dense {
45 #[allow(clippy::cast_possible_wrap)]
46 fn build(kv6: &Kv6) -> Self {
47 let dims = [kv6.xsiz as i32, kv6.ysiz as i32, kv6.zsiz as i32];
48 let n = (dims[0].max(0) * dims[1].max(0) * dims[2].max(0)) as usize;
49 let mut occ = vec![false; n];
50 let mut col = vec![0u32; n];
51 let mut vi = 0usize;
52 for x in 0..kv6.xsiz as usize {
53 for y in 0..kv6.ysiz as usize {
54 let cnt = usize::from(kv6.ylen[x][y]);
55 for _ in 0..cnt {
56 let v = kv6.voxels[vi];
57 vi += 1;
58 let z = i32::from(v.z);
59 if z >= 0 && z < dims[2] {
60 let idx = ((x as i32 * dims[1] + y as i32) * dims[2] + z) as usize;
61 occ[idx] = true;
62 col[idx] = (v.col & 0x00ff_ffff) | 0x8000_0000;
70 }
71 }
72 }
73 }
74 Self { dims, occ, col }
75 }
76
77 #[inline]
78 #[allow(clippy::cast_sign_loss)]
79 fn at(&self, c: [i32; 3]) -> Option<u32> {
80 let idx = ((c[0] * self.dims[1] + c[1]) * self.dims[2] + c[2]) as usize;
81 self.occ[idx].then(|| self.col[idx])
82 }
83}
84
85fn invert_basis(s: [f32; 3], h: [f32; 3], f: [f32; 3]) -> Option<[[f32; 3]; 3]> {
88 let det = s[0] * (h[1] * f[2] - f[1] * h[2]) - h[0] * (s[1] * f[2] - f[1] * s[2])
89 + f[0] * (s[1] * h[2] - h[1] * s[2]);
90 if det.abs() < 1e-12 {
91 return None;
92 }
93 let inv = 1.0 / det;
94 Some([
95 [
96 (h[1] * f[2] - f[1] * h[2]) * inv,
97 -(h[0] * f[2] - f[0] * h[2]) * inv,
98 (h[0] * f[1] - f[0] * h[1]) * inv,
99 ],
100 [
101 -(s[1] * f[2] - f[1] * s[2]) * inv,
102 (s[0] * f[2] - f[0] * s[2]) * inv,
103 -(s[0] * f[1] - f[0] * s[1]) * inv,
104 ],
105 [
106 (s[1] * h[2] - h[1] * s[2]) * inv,
107 -(s[0] * h[2] - h[0] * s[2]) * inv,
108 (s[0] * h[1] - h[0] * s[1]) * inv,
109 ],
110 ])
111}
112
113#[inline]
114fn mat_apply(m: &[[f32; 3]; 3], v: [f32; 3]) -> [f32; 3] {
115 [
116 m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2],
117 m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2],
118 m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2],
119 ]
120}
121
122#[allow(clippy::cast_possible_truncation)]
126fn cast_local(dense: &Kv6Dense, origin: [f32; 3], dir: [f32; 3]) -> Option<(u32, f32)> {
127 #[allow(clippy::cast_precision_loss)]
128 let hi = [
129 dense.dims[0] as f32,
130 dense.dims[1] as f32,
131 dense.dims[2] as f32,
132 ];
133 let (t0, t1) = intersect_aabb(origin, dir, [0.0; 3], hi)?;
134 let start = t0 + 1e-4;
135 let p = [
136 origin[0] + dir[0] * start,
137 origin[1] + dir[1] * start,
138 origin[2] + dir[2] * start,
139 ];
140 let mut cell = [
141 (p[0].floor() as i32).clamp(0, dense.dims[0] - 1),
142 (p[1].floor() as i32).clamp(0, dense.dims[1] - 1),
143 (p[2].floor() as i32).clamp(0, dense.dims[2] - 1),
144 ];
145 let (step, mut t_max, t_delta) = dda_setup(origin, dir, cell, 1.0);
146 let mut t_curr = t0;
147 let max_steps = (dense.dims[0] + dense.dims[1] + dense.dims[2]) as usize + 8;
148 for _ in 0..max_steps {
149 if cell[0] < 0
150 || cell[0] >= dense.dims[0]
151 || cell[1] < 0
152 || cell[1] >= dense.dims[1]
153 || cell[2] < 0
154 || cell[2] >= dense.dims[2]
155 || t_curr > t1
156 {
157 return None;
158 }
159 if let Some(color) = dense.at(cell) {
160 return Some((color, t_curr));
161 }
162 let axis = min_axis(t_max);
163 t_curr = t_max[axis];
164 cell[axis] += step[axis];
165 t_max[axis] += t_delta[axis];
166 }
167 None
168}
169
170#[allow(
180 clippy::too_many_arguments,
181 clippy::cast_possible_truncation,
182 clippy::cast_sign_loss
183)]
184#[must_use]
185pub fn draw_sprite_dda(
186 fb: &mut [u32],
187 zb: &mut [f32],
188 pitch_pixels: usize,
189 width: u32,
190 height: u32,
191 cam: &CameraState,
192 settings: &OpticastSettings,
193 sprite: &Sprite,
194) -> u32 {
195 if sprite.flags & SPRITE_FLAG_INVISIBLE != 0 {
196 return 0;
197 }
198 let dense = Kv6Dense::build(&sprite.kv6);
199 if dense.occ.is_empty() {
200 return 0;
201 }
202 let Some(minv) = invert_basis(sprite.s, sprite.h, sprite.f) else {
203 return 0;
204 };
205 let pivot = [sprite.kv6.xpiv, sprite.kv6.ypiv, sprite.kv6.zpiv];
206 let no_z = sprite.flags & SPRITE_FLAG_NO_Z != 0;
207
208 let Some(rect) = project_screen_rect(sprite, cam, settings, width, height) else {
210 return 0;
211 };
212
213 debug_assert_eq!(fb.len(), zb.len());
214 let target = RasterTarget::new(fb, zb);
215 let mut written = 0u32;
216 for py in rect.1..rect.3 {
217 let row = py as usize * pitch_pixels;
218 for px in rect.0..rect.2 {
219 let (origin, dir) = pixel_ray(cam, settings, px, py);
220 let rel = [
222 origin[0] - sprite.p[0],
223 origin[1] - sprite.p[1],
224 origin[2] - sprite.p[2],
225 ];
226 let ol = mat_apply(&minv, rel);
227 let origin_local = [ol[0] + pivot[0], ol[1] + pivot[1], ol[2] + pivot[2]];
228 let dir_local = mat_apply(&minv, dir);
229 let Some((color, t)) = cast_local(&dense, origin_local, dir_local) else {
230 continue;
231 };
232 let fwd_dot =
233 dir[0] * cam.forward[0] + dir[1] * cam.forward[1] + dir[2] * cam.forward[2];
234 let depth = t * fwd_dot;
235 if depth < NEAR_Z {
236 continue;
237 }
238 let lit = shade(color, 0);
239 let idx = row + px as usize;
240 let wrote = unsafe {
243 if no_z {
244 target.write_color(idx, lit);
245 target.write_depth(idx, depth);
246 true
247 } else {
248 target.z_test_write(idx, lit, depth)
249 }
250 };
251 written += u32::from(wrote);
252 }
253 }
254 written
255}
256
257#[allow(
261 clippy::cast_possible_truncation,
262 clippy::cast_sign_loss,
263 clippy::cast_precision_loss
264)]
265fn project_screen_rect(
266 sprite: &Sprite,
267 cam: &CameraState,
268 settings: &OpticastSettings,
269 width: u32,
270 height: u32,
271) -> Option<(u32, u32, u32, u32)> {
272 let kv6 = &sprite.kv6;
273 let (xs, ys, zs) = (kv6.xsiz as f32, kv6.ysiz as f32, kv6.zsiz as f32);
274 let (xp, yp, zp) = (kv6.xpiv, kv6.ypiv, kv6.zpiv);
275 let (mut x0, mut y0, mut x1, mut y1) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
276 let mut all_front = true;
277 for &cx in &[0.0, xs] {
278 for &cy in &[0.0, ys] {
279 for &cz in &[0.0, zs] {
280 let lx = cx - xp;
282 let ly = cy - yp;
283 let lz = cz - zp;
284 let world = [
285 sprite.p[0] + lx * sprite.s[0] + ly * sprite.h[0] + lz * sprite.f[0],
286 sprite.p[1] + lx * sprite.s[1] + ly * sprite.h[1] + lz * sprite.f[1],
287 sprite.p[2] + lx * sprite.s[2] + ly * sprite.h[2] + lz * sprite.f[2],
288 ];
289 let rel = [
290 world[0] - cam.pos[0],
291 world[1] - cam.pos[1],
292 world[2] - cam.pos[2],
293 ];
294 let cz_cam =
295 rel[0] * cam.forward[0] + rel[1] * cam.forward[1] + rel[2] * cam.forward[2];
296 if cz_cam < NEAR_Z {
297 all_front = false;
298 continue;
299 }
300 let cx_cam = rel[0] * cam.right[0] + rel[1] * cam.right[1] + rel[2] * cam.right[2];
301 let cy_cam = rel[0] * cam.down[0] + rel[1] * cam.down[1] + rel[2] * cam.down[2];
302 let sx = settings.hx + cx_cam / cz_cam * settings.hz;
303 let sy = settings.hy + cy_cam / cz_cam * settings.hz;
304 x0 = x0.min(sx);
305 y0 = y0.min(sy);
306 x1 = x1.max(sx);
307 y1 = y1.max(sy);
308 }
309 }
310 }
311 let (w, h) = (width as f32, height as f32);
312 let (rx0, ry0, rx1, ry1) = if all_front {
313 (
314 (x0 - 1.0).max(0.0),
315 (y0 - 1.0).max(0.0),
316 (x1 + 1.0).min(w),
317 (y1 + 1.0).min(h),
318 )
319 } else {
320 (0.0, 0.0, w, h)
322 };
323 if rx0 >= rx1 || ry0 >= ry1 {
324 return None;
325 }
326 Some((rx0 as u32, ry0 as u32, rx1.ceil() as u32, ry1.ceil() as u32))
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332 use crate::camera_math;
333 use crate::Camera;
334 use roxlap_formats::kv6::Kv6;
335 use roxlap_formats::sprite::Sprite;
336
337 fn settings(w: u32, h: u32) -> OpticastSettings {
338 OpticastSettings::for_oracle_framebuffer(w, h)
339 }
340
341 fn cam_looking_y() -> Camera {
343 Camera {
344 pos: [0.0, 0.0, 0.0],
345 right: [1.0, 0.0, 0.0],
346 down: [0.0, 0.0, 1.0],
347 forward: [0.0, 1.0, 0.0],
348 }
349 }
350
351 #[test]
354 fn cube_sprite_renders() {
355 let kv6 = Kv6::solid_cube(8, 0x80_C0_40_20);
356 let sprite = Sprite::axis_aligned(kv6, [0.0, 40.0, 0.0]);
357 let (w, h) = (64u32, 64u32);
358 let n = (w * h) as usize;
359 let mut fb = vec![0u32; n];
360 let mut zb = vec![f32::INFINITY; n];
361 let cam = cam_looking_y();
362 let cs = camera_math::derive(&cam, w, h, 32.0, 32.0, 32.0);
363 let wrote = draw_sprite_dda(
364 &mut fb,
365 &mut zb,
366 w as usize,
367 w,
368 h,
369 &cs,
370 &settings(w, h),
371 &sprite,
372 );
373
374 assert!(wrote > 20, "cube should cover many pixels (got {wrote})");
375 let centre = (h / 2 * w + w / 2) as usize;
376 assert_eq!(
377 fb[centre] & 0x00ff_ffff,
378 0x00_C0_40_20,
379 "got {:08x}",
380 fb[centre]
381 );
382 assert!(
384 (zb[centre] - 36.0).abs() < 3.0,
385 "centre depth {} not ≈ 36",
386 zb[centre]
387 );
388 }
389
390 #[test]
395 fn zero_high_byte_sprite_not_black() {
396 let kv6 = Kv6::solid_cube(8, 0x00_C0_40_20);
397 let sprite = Sprite::axis_aligned(kv6, [0.0, 40.0, 0.0]);
398 let (w, h) = (64u32, 64u32);
399 let n = (w * h) as usize;
400 let mut fb = vec![0u32; n];
401 let mut zb = vec![f32::INFINITY; n];
402 let cam = cam_looking_y();
403 let cs = camera_math::derive(&cam, w, h, 32.0, 32.0, 32.0);
404 let wrote = draw_sprite_dda(
405 &mut fb,
406 &mut zb,
407 w as usize,
408 w,
409 h,
410 &cs,
411 &settings(w, h),
412 &sprite,
413 );
414 assert!(wrote > 20, "cube should cover many pixels (got {wrote})");
415 let centre = (h / 2 * w + w / 2) as usize;
416 assert_eq!(
417 fb[centre] & 0x00ff_ffff,
418 0x00_C0_40_20,
419 "zero-high-byte sprite rendered as {:08x} (black bug)",
420 fb[centre]
421 );
422 }
423
424 #[test]
427 fn sprite_respects_zbuffer() {
428 let kv6 = Kv6::solid_cube(8, 0x80_FF_FF_FF);
429 let sprite = Sprite::axis_aligned(kv6, [0.0, 40.0, 0.0]);
430 let (w, h) = (32u32, 32u32);
431 let n = (w * h) as usize;
432 let cam = cam_looking_y();
433 let cs = camera_math::derive(&cam, w, h, 16.0, 16.0, 16.0);
434 let centre = (h / 2 * w + w / 2) as usize;
435
436 let mut fb = vec![0u32; n];
438 let mut zb = vec![f32::INFINITY; n];
439 fb[centre] = 0x80_11_22_33;
440 zb[centre] = 10.0;
441 let _ = draw_sprite_dda(
442 &mut fb,
443 &mut zb,
444 w as usize,
445 w,
446 h,
447 &cs,
448 &settings(w, h),
449 &sprite,
450 );
451 assert_eq!(
452 fb[centre], 0x80_11_22_33,
453 "near terrain must occlude sprite"
454 );
455
456 let mut fb2 = vec![0u32; n];
458 let mut zb2 = vec![f32::INFINITY; n];
459 fb2[centre] = 0x80_11_22_33;
460 zb2[centre] = 100.0;
461 let _ = draw_sprite_dda(
462 &mut fb2,
463 &mut zb2,
464 w as usize,
465 w,
466 h,
467 &cs,
468 &settings(w, h),
469 &sprite,
470 );
471 assert_ne!(fb2[centre], 0x80_11_22_33, "sprite must beat far terrain");
472 assert!(zb2[centre] < 100.0, "sprite depth must replace terrain's");
473 }
474
475 fn covered_rect(fb: &[u32], w: u32, h: u32) -> (u32, u32, u32, u32) {
478 let (mut x0, mut y0, mut x1, mut y1) = (w, h, 0u32, 0u32);
479 for py in 0..h {
480 for px in 0..w {
481 if fb[(py * w + px) as usize] & 0x00ff_ffff != 0 {
482 x0 = x0.min(px);
483 y0 = y0.min(py);
484 x1 = x1.max(px);
485 y1 = y1.max(py);
486 }
487 }
488 }
489 (x0, y0, x1, y1)
490 }
491
492 #[test]
497 fn posed_basis_reorients_silhouette() {
498 let kv6 = Kv6::solid_box(16, 4, 4, 0x80_C0_40_20);
501 let (w, h) = (64u32, 64u32);
502 let n = (w * h) as usize;
503 let cam = cam_looking_y();
504 let cs = camera_math::derive(&cam, w, h, 32.0, 32.0, 32.0);
505
506 let aa = Sprite::axis_aligned(kv6.clone(), [0.0, 40.0, 0.0]);
508 let mut fb = vec![0u32; n];
509 let mut zb = vec![f32::INFINITY; n];
510 let _ = draw_sprite_dda(
511 &mut fb,
512 &mut zb,
513 w as usize,
514 w,
515 h,
516 &cs,
517 &settings(w, h),
518 &aa,
519 );
520 let (ax0, ay0, ax1, ay1) = covered_rect(&fb, w, h);
521 let aa_wide = (ax1 - ax0) as i32 - (ay1 - ay0) as i32;
522 assert!(
523 aa_wide > 4,
524 "axis-aligned box should be wider than tall (got w-h={aa_wide})"
525 );
526
527 let mut posed = aa.clone();
530 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];
534 let mut zb2 = vec![f32::INFINITY; n];
535 let _ = draw_sprite_dda(
536 &mut fb2,
537 &mut zb2,
538 w as usize,
539 w,
540 h,
541 &cs,
542 &settings(w, h),
543 &posed,
544 );
545 let (bx0, by0, bx1, by1) = covered_rect(&fb2, w, h);
546 let posed_tall = (by1 - by0) as i32 - (bx1 - bx0) as i32;
547 assert!(
548 posed_tall > 4,
549 "posed box should be taller than wide (got h-w={posed_tall})"
550 );
551 }
552
553 #[test]
556 fn degenerate_basis_draws_nothing() {
557 let kv6 = Kv6::solid_cube(8, 0x80_FF_FF_FF);
558 let mut sprite = Sprite::axis_aligned(kv6, [0.0, 40.0, 0.0]);
559 sprite.f = sprite.s; let (w, h) = (32u32, 32u32);
561 let n = (w * h) as usize;
562 let mut fb = vec![0u32; n];
563 let mut zb = vec![f32::INFINITY; n];
564 let cam = cam_looking_y();
565 let cs = camera_math::derive(&cam, w, h, 16.0, 16.0, 16.0);
566 let wrote = draw_sprite_dda(
567 &mut fb,
568 &mut zb,
569 w as usize,
570 w,
571 h,
572 &cs,
573 &settings(w, h),
574 &sprite,
575 );
576 assert_eq!(wrote, 0, "singular basis must skip, not panic");
577 }
578
579 #[test]
581 fn invisible_sprite_skipped() {
582 let kv6 = Kv6::solid_cube(8, 0x80_FF_FF_FF);
583 let mut sprite = Sprite::axis_aligned(kv6, [0.0, 40.0, 0.0]);
584 sprite.flags |= roxlap_formats::sprite::SPRITE_FLAG_INVISIBLE;
585 let (w, h) = (32u32, 32u32);
586 let n = (w * h) as usize;
587 let mut fb = vec![0u32; n];
588 let mut zb = vec![f32::INFINITY; n];
589 let cam = cam_looking_y();
590 let cs = camera_math::derive(&cam, w, h, 16.0, 16.0, 16.0);
591 let wrote = draw_sprite_dda(
592 &mut fb,
593 &mut zb,
594 w as usize,
595 w,
596 h,
597 &cs,
598 &settings(w, h),
599 &sprite,
600 );
601 assert_eq!(wrote, 0);
602 }
603}