roxlap_formats/sprite.rs
1//! KV6 sprite — a parsed [`Kv6`] paired with a world-space pose.
2//!
3//! Mirror of voxlap's `vx5sprite` (`voxlap5.h:63-79`) for the kv6
4//! case (`flags & SPRITE_FLAG_KFA == 0`). Pure data — owns the
5//! [`Kv6`] voxel grid plus the four `point3d` fields voxlap calls
6//! `p` (pivot position), `s` (x-basis), `h` (y-basis), `f`
7//! (z-basis), and the bitfield `flags`.
8//!
9//! Rendering lives in `roxlap-core` (`draw_sprite`); this module
10//! only models the data shape so downstream tools (file
11//! converters, model inspectors, asset pipelines) can build and
12//! manipulate sprites without depending on the full renderer.
13//!
14//! Voxlap's 64-byte layout, for reference:
15//!
16//! ```text
17//! point3d p; // position
18//! int32_t flags; // bit 0: 0=normal shading
19//! // bit 1: 0=kv6data, 1=kfatype
20//! // bit 2: 0=normal, 1=invisible
21//! // bit 3: 0=z-tested, 1=overlay (no-z)
22//! point3d s; // x-basis (kv6data.xsiz direction)
23//! kv6data *voxnum; // (or kfatype *kfaptr if flag bit 1 set)
24//! point3d h; // y-basis
25//! int32_t kfatim;
26//! point3d f; // z-basis
27//! int32_t okfatim;
28//! ```
29
30use crate::kv6::Kv6;
31
32/// Voxlap's sprite-flags bit 0: disable normal-based face shading.
33pub const SPRITE_FLAG_NO_SHADING: u32 = 1 << 0;
34/// Voxlap's sprite-flags bit 1: voxnum points at a `kfatype`
35/// (animated). When clear (default), points at a `kv6data`.
36pub const SPRITE_FLAG_KFA: u32 = 1 << 1;
37/// Voxlap's sprite-flags bit 2: skip rendering entirely.
38pub const SPRITE_FLAG_INVISIBLE: u32 = 1 << 2;
39/// Voxlap's sprite-flags bit 3: render without z-buffer test.
40pub const SPRITE_FLAG_NO_Z: u32 = 1 << 3;
41
42/// A KV6 voxel sprite positioned in world space.
43///
44/// Mirror of voxlap's `vx5sprite` for the kv6 case
45/// (`flags & SPRITE_FLAG_KFA == 0`; see [`SPRITE_FLAG_KFA`]).
46/// Owns its [`Kv6`] by value. `p` / `s` / `h` / `f` are voxlap's
47/// per-axis world-space basis: `s` is the `kv6.xsiz` direction,
48/// `h` the `ysiz` direction, `f` the `zsiz` direction. For an
49/// axis-aligned sprite, `s = [1,0,0]`, `h = [0,1,0]`,
50/// `f = [0,0,1]`.
51#[derive(Debug, Clone)]
52pub struct Sprite {
53 /// Voxel data + bounding-box pivots. Loaded from a `.kv6`
54 /// file via [`crate::kv6::parse`] or built procedurally.
55 pub kv6: Kv6,
56 /// World-space position of the sprite's pivot (xpiv, ypiv,
57 /// zpiv inside the kv6 maps to this point).
58 pub p: [f32; 3],
59 /// World-space basis vector for the kv6's local +x. Length
60 /// scales the sprite along that axis (typically `1.0` for
61 /// unit-scale).
62 pub s: [f32; 3],
63 /// World-space basis vector for the kv6's local +y.
64 pub h: [f32; 3],
65 /// World-space basis vector for the kv6's local +z.
66 pub f: [f32; 3],
67 /// Voxlap-style flags bitfield. See [`SPRITE_FLAG_NO_SHADING`],
68 /// [`SPRITE_FLAG_KFA`], [`SPRITE_FLAG_INVISIBLE`],
69 /// [`SPRITE_FLAG_NO_Z`].
70 pub flags: u32,
71}
72
73impl Sprite {
74 /// Convenience constructor for an axis-aligned sprite at
75 /// world position `pos`. Basis is identity, flags = 0
76 /// (kv6 + normal shading + visible + z-tested).
77 ///
78 /// # Examples
79 ///
80 /// ```
81 /// use roxlap_formats::kv6::Kv6;
82 /// use roxlap_formats::sprite::Sprite;
83 ///
84 /// # let kv6 = Kv6 {
85 /// # xsiz: 1, ysiz: 1, zsiz: 1,
86 /// # xpiv: 0.5, ypiv: 0.5, zpiv: 0.5,
87 /// # voxels: vec![], xlen: vec![0], ylen: vec![vec![0]],
88 /// # palette: None,
89 /// # };
90 /// // ... after `let kv6 = kv6::parse(&bytes)?;` or similar:
91 /// let sprite = Sprite::axis_aligned(kv6, [1024.0, 1024.0, 100.0]);
92 /// assert_eq!(sprite.flags, 0);
93 /// assert_eq!(sprite.s, [1.0, 0.0, 0.0]);
94 /// ```
95 #[must_use]
96 pub fn axis_aligned(kv6: Kv6, pos: [f32; 3]) -> Self {
97 Self {
98 kv6,
99 p: pos,
100 s: [1.0, 0.0, 0.0],
101 h: [0.0, 1.0, 0.0],
102 f: [0.0, 0.0, 1.0],
103 flags: 0,
104 }
105 }
106
107 /// Carve a sphere out of this sprite's voxel model, controlling
108 /// the colour of the interior the cut exposes. Thin delegate to
109 /// [`Kv6::carve_sphere_with_colfunc`] — `centre` / `radius`,
110 /// `solid`, and `colfunc` are all in **kv6-local** voxel
111 /// coordinates (the sprite pose `p`/`s`/`h`/`f` is untouched). See
112 /// that method for why the `solid` occupancy predicate is required.
113 pub fn carve_sphere_with_colfunc<S, C>(
114 &mut self,
115 centre: [i32; 3],
116 radius: u32,
117 solid: S,
118 colfunc: C,
119 ) where
120 S: Fn(i32, i32, i32) -> bool,
121 C: Fn(i32, i32, i32) -> u32,
122 {
123 self.kv6
124 .carve_sphere_with_colfunc(centre, radius, solid, colfunc);
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn carve_sphere_delegates_to_kv6_and_leaves_pose() {
134 const BASE: u32 = 0x8033_4455;
135 let kv6 = Kv6::from_fn_shaded(16, 16, 16, |_, _, _| Some(BASE));
136 let mut sprite = Sprite::axis_aligned(kv6, [10.0, 20.0, 30.0]);
137 let before = sprite.kv6.voxels.len();
138
139 sprite.carve_sphere_with_colfunc([8, 8, 8], 4, |_, _, _| true, |_, _, _| 0x8000_FF00);
140
141 // The hollowed-out shell has more surface voxels than the
142 // solid hull did, so the model definitely changed.
143 assert_ne!(sprite.kv6.voxels.len(), before);
144 // Pose untouched — carving is kv6-local only.
145 assert_eq!(sprite.p, [10.0, 20.0, 30.0]);
146 assert_eq!(sprite.s, [1.0, 0.0, 0.0]);
147 }
148}