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