1use crate::components::Decal;
9use crate::gfx::transform::trs_matrix;
10use crate::math::sqrt;
11use alloc::vec::Vec;
12
13#[derive(Debug, Clone, Copy, PartialEq)]
22pub struct DecalRecord {
23 pub model: [[f32; 4]; 4],
25 pub inv_model: [[f32; 4]; 4],
27 pub texture_slot: usize,
29 pub tint: [f32; 4],
31}
32
33impl DecalRecord {
34 pub fn aabb(&self) -> ([f32; 3], [f32; 3]) {
41 crate::gfx::frustum::transform_aabb([-0.5; 3], [0.5; 3], self.model)
42 }
43}
44
45pub fn decal_model_matrix(
49 position: [f32; 3],
50 rotation_deg: [f32; 3],
51 size: [f32; 3],
52) -> [[f32; 4]; 4] {
53 trs_matrix(position, rotation_deg, size)
54}
55
56pub fn invert_decal_model(model: [[f32; 4]; 4]) -> Option<[[f32; 4]; 4]> {
64 let col0 = [model[0][0], model[0][1], model[0][2]];
67 let col1 = [model[1][0], model[1][1], model[1][2]];
68 let col2 = [model[2][0], model[2][1], model[2][2]];
69 let s0 = sqrt(col0[0] * col0[0] + col0[1] * col0[1] + col0[2] * col0[2]);
70 let s1 = sqrt(col1[0] * col1[0] + col1[1] * col1[1] + col1[2] * col1[2]);
71 let s2 = sqrt(col2[0] * col2[0] + col2[1] * col2[1] + col2[2] * col2[2]);
72 if !(s0.is_finite() && s1.is_finite() && s2.is_finite()) || s0 == 0.0 || s1 == 0.0 || s2 == 0.0
73 {
74 return None;
75 }
76 let r0 = [col0[0] / s0, col0[1] / s0, col0[2] / s0];
78 let r1 = [col1[0] / s1, col1[1] / s1, col1[2] / s1];
79 let r2 = [col2[0] / s2, col2[1] / s2, col2[2] / s2];
80 let inv_lin = [
83 [r0[0] / s0, r1[0] / s1, r2[0] / s2],
85 [r0[1] / s0, r1[1] / s1, r2[1] / s2],
87 [r0[2] / s0, r1[2] / s1, r2[2] / s2],
89 ];
90 let t = [model[3][0], model[3][1], model[3][2]];
91 let inv_t = [
92 -(inv_lin[0][0] * t[0] + inv_lin[1][0] * t[1] + inv_lin[2][0] * t[2]),
93 -(inv_lin[0][1] * t[0] + inv_lin[1][1] * t[1] + inv_lin[2][1] * t[2]),
94 -(inv_lin[0][2] * t[0] + inv_lin[1][2] * t[1] + inv_lin[2][2] * t[2]),
95 ];
96 Some([
97 [inv_lin[0][0], inv_lin[0][1], inv_lin[0][2], 0.0],
98 [inv_lin[1][0], inv_lin[1][1], inv_lin[1][2], 0.0],
99 [inv_lin[2][0], inv_lin[2][1], inv_lin[2][2], 0.0],
100 [inv_t[0], inv_t[1], inv_t[2], 1.0],
101 ])
102}
103
104pub fn build_decal_records(decals: &[&Decal], texture_count: usize) -> Vec<DecalRecord> {
114 let mut out = Vec::new();
115 for d in decals {
116 if !d.visible {
117 continue;
118 }
119 if !(d.size[0] > 0.0 && d.size[1] > 0.0 && d.size[2] > 0.0) {
120 continue;
121 }
122 let slot = match d.texture {
123 None => 0,
124 Some(handle) => {
125 let slot = handle.index();
126 if slot >= texture_count {
127 continue;
128 }
129 slot
130 }
131 };
132 let model = decal_model_matrix(d.position, d.rotation_deg, d.size);
133 let inv_model = match invert_decal_model(model) {
134 Some(m) => m,
135 None => continue,
136 };
137 out.push(DecalRecord {
138 model,
139 inv_model,
140 texture_slot: slot,
141 tint: d.tint,
142 });
143 }
144 out
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use crate::gfx::transform::{IDENTITY, mat4_mul};
151
152 fn near(a: [[f32; 4]; 4], b: [[f32; 4]; 4]) -> bool {
153 a.iter().zip(b.iter()).all(|(ac, bc)| {
154 ac.iter()
155 .zip(bc.iter())
156 .all(|(av, bv)| (av - bv).abs() < 1e-4)
157 })
158 }
159
160 #[test]
161 fn unit_decal_has_identity_model() {
162 let m = decal_model_matrix([0.0; 3], [0.0; 3], [1.0; 3]);
163 assert!(near(m, IDENTITY));
164 }
165
166 #[test]
167 fn translation_scale_compose_into_model() {
168 let m = decal_model_matrix([2.0, 3.0, -4.0], [0.0; 3], [0.5, 1.0, 2.0]);
169 let expected: [[f32; 4]; 4] = [
171 [0.5, 0.0, 0.0, 0.0],
172 [0.0, 1.0, 0.0, 0.0],
173 [0.0, 0.0, 2.0, 0.0],
174 [2.0, 3.0, -4.0, 1.0],
175 ];
176 assert!(near(m, expected));
177 }
178
179 #[test]
180 fn inverse_round_trips_through_model() {
181 let m = decal_model_matrix([1.0, -2.0, 0.5], [30.0, -15.0, 45.0], [0.4, 1.2, 0.7]);
182 let inv = invert_decal_model(m).expect("non-degenerate model is invertible");
183 assert!(near(mat4_mul(m, inv), IDENTITY));
184 assert!(near(mat4_mul(inv, m), IDENTITY));
185 }
186
187 #[test]
188 fn degenerate_size_rejects_inverse() {
189 let m = decal_model_matrix([0.0; 3], [0.0; 3], [0.0, 1.0, 1.0]);
190 assert!(invert_decal_model(m).is_none());
191 }
192
193 #[test]
194 fn invisible_decal_is_skipped_in_records() {
195 let d = Decal {
196 visible: false,
197 ..Default::default()
198 };
199 assert!(build_decal_records(&[&d], 0).is_empty());
200 }
201
202 #[test]
203 fn degenerate_size_is_skipped_in_records() {
204 let d = Decal {
205 size: [1.0, 0.0, 1.0],
206 ..Default::default()
207 };
208 assert!(build_decal_records(&[&d], 0).is_empty());
209 }
210
211 #[test]
212 fn decal_without_texture_uses_fallback_slot() {
213 let d = Decal::default();
214 let recs = build_decal_records(&[&d], 0);
215 assert_eq!(recs.len(), 1);
216 assert_eq!(recs[0].texture_slot, 0);
217 }
218
219 #[test]
220 fn decal_texture_handle_is_used_directly_as_the_slot() {
221 let d = Decal {
224 texture: Some(crate::ecs::TextureHandle(3)),
225 ..Default::default()
226 };
227 let recs = build_decal_records(&[&d], 5);
228 assert_eq!(recs.len(), 1);
229 assert_eq!(recs[0].texture_slot, 3);
230
231 let past = Decal {
232 texture: Some(crate::ecs::TextureHandle(9)),
233 ..Default::default()
234 };
235 assert!(build_decal_records(&[&past], 5).is_empty());
236 }
237
238 #[test]
239 fn aabb_of_unit_decal_at_origin_is_half_unit_box() {
240 let d = Decal::default();
241 let recs = build_decal_records(&[&d], 0);
242 let (mn, mx) = recs[0].aabb();
243 assert!((mn[0] + 0.5).abs() < 1e-5 && (mx[0] - 0.5).abs() < 1e-5);
244 assert!((mn[1] + 0.5).abs() < 1e-5 && (mx[1] - 0.5).abs() < 1e-5);
245 assert!((mn[2] + 0.5).abs() < 1e-5 && (mx[2] - 0.5).abs() < 1e-5);
246 }
247
248 #[test]
249 fn aabb_translates_with_decal_position() {
250 let d = Decal {
251 position: [10.0, 5.0, -3.0],
252 ..Default::default()
253 };
254 let recs = build_decal_records(&[&d], 0);
255 let (mn, mx) = recs[0].aabb();
256 assert!((mn[0] - 9.5).abs() < 1e-5 && (mx[0] - 10.5).abs() < 1e-5);
258 assert!((mn[1] - 4.5).abs() < 1e-5 && (mx[1] - 5.5).abs() < 1e-5);
259 assert!((mn[2] + 3.5).abs() < 1e-5 && (mx[2] + 2.5).abs() < 1e-5);
260 }
261
262 #[test]
263 fn aabb_expands_under_size() {
264 let d = Decal {
265 size: [4.0, 0.5, 8.0],
266 ..Default::default()
267 };
268 let recs = build_decal_records(&[&d], 0);
269 let (mn, mx) = recs[0].aabb();
270 assert!((mx[0] - mn[0] - 4.0).abs() < 1e-5);
271 assert!((mx[1] - mn[1] - 0.5).abs() < 1e-5);
272 assert!((mx[2] - mn[2] - 8.0).abs() < 1e-5);
273 }
274
275 #[test]
276 fn aabb_includes_rotated_decal_extents() {
277 let d = Decal {
281 size: [2.0, 1.0, 2.0],
282 rotation_deg: [0.0, 45.0, 0.0],
283 ..Default::default()
284 };
285 let recs = build_decal_records(&[&d], 0);
286 let (mn, mx) = recs[0].aabb();
287 let span_x = mx[0] - mn[0];
288 let span_z = mx[2] - mn[2];
289 let expected = 2.0 * core::f32::consts::SQRT_2;
290 assert!((span_x - expected).abs() < 1e-4);
291 assert!((span_z - expected).abs() < 1e-4);
292 assert!((mx[1] - mn[1] - 1.0).abs() < 1e-5);
294 }
295}