1use serde::Serialize;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
9#[serde(rename_all = "snake_case")]
10pub enum NormalSpace {
11 World,
13 View,
15 Tangent,
17}
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
20#[serde(rename_all = "snake_case")]
21pub enum NormalEncoding {
22 Xyz,
24 XyzUnorm,
26 Xy,
28 XyUnorm,
30 XyzBiased {
32 bias_bits: u32,
34 },
35 Octahedral {
41 stored: LaneAffine,
43 read: LaneAffine,
45 },
46 OctahedralDiamond,
52 CubeFace {
62 minor: LaneAffine,
64 payload: LaneAffine,
66 },
67}
68#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
71pub struct LaneAffine {
72 pub scale_bits: u32,
74 pub bias_bits: u32,
76}
77impl Serialize for LaneAffine {
78 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
79 use serde::ser::SerializeStruct;
80 let mut out = serializer.serialize_struct("LaneAffine", 2)?;
81 out.serialize_field("scale", &self.scale())?;
82 out.serialize_field("bias", &self.bias())?;
83 out.end()
84 }
85}
86impl LaneAffine {
87 pub const UNORM: Self = Self {
89 scale_bits: 0x3F00_0000,
90 bias_bits: 0x3F00_0000,
91 };
92
93 pub const SIGNED: Self = Self {
95 scale_bits: 0x4000_0000,
96 bias_bits: 0xBF80_0000,
97 };
98
99 pub fn new(scale: f32, bias: f32) -> Self {
101 Self {
102 scale_bits: scale.to_bits(),
103 bias_bits: bias.to_bits(),
104 }
105 }
106
107 pub fn scale(&self) -> f32 {
109 f32::from_bits(self.scale_bits)
110 }
111
112 pub fn bias(&self) -> f32 {
114 f32::from_bits(self.bias_bits)
115 }
116
117 pub fn apply(&self, v: f32) -> f32 {
119 v * self.scale() + self.bias()
120 }
121}
122#[derive(Clone, Copy, Debug, PartialEq, Eq)]
124pub(super) enum Axis {
125 X,
126 Y,
127 Z,
128}
129impl Axis {
130 pub(super) const ALL: [Axis; 3] = [Axis::X, Axis::Y, Axis::Z];
131
132 pub(super) fn of<T: Copy>(self, [x, y, z]: [T; 3]) -> T {
134 match self {
135 Axis::X => x,
136 Axis::Y => y,
137 Axis::Z => z,
138 }
139 }
140}
141
142#[derive(Clone, Copy, Debug)]
145pub(super) struct CubeFace {
146 pub(super) major: Axis,
147 pub(super) minor: [(Axis, f64); 2],
148}
149impl CubeFace {
150 pub(super) fn new(axis: Axis, negative: bool) -> CubeFace {
152 use Axis::{X, Y, Z};
153 let (major, minor) = match (axis, negative) {
154 (X, false) => (X, [(Z, -1.0), (Y, -1.0)]),
155 (X, true) => (X, [(Z, 1.0), (Y, -1.0)]),
156 (Y, false) => (Y, [(X, 1.0), (Z, 1.0)]),
157 (Y, true) => (Y, [(X, 1.0), (Z, -1.0)]),
158 (Z, false) => (Z, [(X, 1.0), (Y, -1.0)]),
159 (Z, true) => (Z, [(X, -1.0), (Y, -1.0)]),
160 };
161 CubeFace { major, minor }
162 }
163
164 pub(super) fn place<T: Copy>(&self, major: T, first: T, second: T, none: T) -> [T; 3] {
166 let [(a, _), (b, _)] = self.minor;
167 Axis::ALL.map(|axis| {
168 if axis == self.major {
169 major
170 } else if axis == a {
171 first
172 } else if axis == b {
173 second
174 } else {
175 none
176 }
177 })
178 }
179}
180#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] fn unorm8(x: f32) -> u8 {
183 (x.clamp(0.0, 1.0) * 255.0).round_ties_even() as u8
184}
185
186#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] fn nine_bits(v: f64) -> u16 {
189 v.round().clamp(0.0, 510.0) as u16
190}
191
192#[allow(clippy::cast_possible_truncation)] pub(super) fn narrow(v: f64) -> f32 {
195 v as f32
196}
197impl NormalEncoding {
198 pub fn decode_cube_face(
204 minor: LaneAffine,
205 payload: LaneAffine,
206 lanes: [f32; 4],
207 steps: [f32; 4],
208 ) -> Result<([f64; 3], f32), String> {
209 let [a, b, side, lane_3] = lanes;
210 let thirds = f64::from(lane_3) * 3.0;
211 let nearest = thirds.round();
212 let [_, _, step_side, step_axis] = steps;
213 let axis_index = [0.0, 1.0, 2.0].iter().position(|&candidate| candidate == nearest);
215 let axis = axis_index
216 .and_then(|i| Axis::ALL.get(i))
217 .filter(|_| (thirds - nearest).abs() / 3.0 < f64::from(step_axis) / 2.0);
218 let Some(&axis) = axis else {
219 return Err(format!(
220 "a cube-face lane 3 of {lane_3} is not an axis (0, 1/3 or 2/3 within half a step of {step_axis})"
221 ));
222 };
223 let from_half = f64::from(side) - 0.5;
224 if from_half.abs() < f64::from(step_side) {
225 return Err(format!(
226 "a cube-face lane 2 of {side} is within a step ({step_side}) of one half: its sign cannot be read"
227 ));
228 }
229 let negative = from_half > 0.0;
230 let face = CubeFace::new(axis, negative);
231 let unmap = |v: f32| (f64::from(v) - f64::from(minor.bias())) / f64::from(minor.scale());
232 let (sc, tc) = (unmap(a), unmap(b));
233 let [(_, first_sign), (_, second_sign)] = face.minor;
234 let depth = (1.0 - sc * sc - tc * tc).max(0.0).sqrt() * if negative { -1.0 } else { 1.0 };
235 let n = face.place(depth, sc * first_sign, tc * second_sign, 0.0);
236 let length = n.iter().map(|c| c * c).sum::<f64>().sqrt();
237 let p = ((narrow(from_half.abs()) - payload.bias()) / payload.scale()).clamp(0.0, 1.0);
238 Ok((n.map(|c| c / length), p))
239 }
240
241 pub fn encode_cube_face(minor: LaneAffine, payload: LaneAffine, normal: [f32; 3], p: f32) -> [f32; 4] {
245 let [x, y, z] = normal;
246 let (axis, thirds) = if z.abs() >= x.abs() && z.abs() >= y.abs() {
247 (Axis::Z, 2.0)
248 } else if y.abs() >= x.abs() {
249 (Axis::Y, 1.0)
250 } else {
251 (Axis::X, 0.0)
252 };
253 let negative = axis.of(normal) < 0.0;
254 let [first, second] = CubeFace::new(axis, negative).minor;
255 let lane =
256 |(of, sign): (Axis, f64)| minor.apply(if sign < 0.0 { -of.of(normal) } else { of.of(normal) });
257 let offset = p * payload.scale() + payload.bias();
258 [
259 lane(first),
260 lane(second),
261 if negative { 0.5 + offset } else { 0.5 - offset },
262 thirds / 3.0,
263 ]
264 }
265
266 pub fn decode_octahedral_diamond(lanes: [f32; 3]) -> ([f64; 3], u8) {
269 let [r, g, b] = lanes.map(unorm8);
270 let u9 = f64::from(b >> 1 & 1) * 256.0 + f64::from(r);
271 let v9 = f64::from(b >> 2 & 1) * 256.0 + f64::from(g);
272 let pu = (u9 / 255.0 - 1.0).clamp(-1.0, 1.0);
273 let pv = (v9 / 255.0 - 1.0).clamp(-1.0, 1.0);
274 let (x, y) = ((pu + pv) / 2.0, (pu - pv) / 2.0);
275 let z = (1.0 - x.abs() - y.abs()) * if b & 1 == 1 { 1.0 } else { -1.0 };
276 let n = (x * x + y * y + z * z).sqrt();
277 ([x / n, y / n, z / n], b >> 3)
278 }
279
280 pub fn encode_octahedral_diamond(normal: [f64; 3], payload: u8) -> [u8; 3] {
283 let [x, y, z] = normal;
284 let sum = x.abs() + y.abs() + z.abs();
285 let (x, y) = (x / sum, y / sum);
286 let nine = |c: f64| nine_bits(c * 255.0 + 255.0);
287 let (u9, v9) = (nine(x + y), nine(x - y));
288 let sign = u8::from(z >= 0.0);
289 [
290 (u9 & 0xff) as u8,
291 (v9 & 0xff) as u8,
292 sign | ((u9 >> 8) as u8) << 1 | ((v9 >> 8) as u8) << 2 | (payload & 0x1f) << 3,
293 ]
294 }
295}
296#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
301pub struct ScalarEncoding {
302 pub power: u8,
305 pub root: u8,
307 pub scale_bits: u32,
310}
311impl Serialize for ScalarEncoding {
312 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
313 use serde::ser::SerializeStruct;
314 let mut out = serializer.serialize_struct(
316 "ScalarEncoding",
317 2usize.saturating_add(usize::from(self.root != 1)),
318 )?;
319 out.serialize_field("power", &self.power)?;
320 if self.root != 1 {
321 out.serialize_field("root", &self.root)?;
322 } else {
323 out.skip_field("root")?;
324 }
325 out.serialize_field("scale", &f32::from_bits(self.scale_bits))?;
326 out.end()
327 }
328}
329impl ScalarEncoding {
330 pub const LINEAR: Self = Self {
332 power: 1,
333 root: 1,
334 scale_bits: 0x3F80_0000,
335 };
336
337 pub fn linear_over(scale: f32) -> Self {
339 Self::power_of(1, scale)
340 }
341
342 pub fn square_root_of(scale: f32) -> Self {
344 Self::power_of(2, scale)
345 }
346
347 pub fn power_of(power: u8, scale: f32) -> Self {
349 Self {
350 power: power.max(1),
351 root: 1,
352 scale_bits: scale.to_bits(),
353 }
354 }
355
356 pub fn square_of(scale: f32) -> Self {
358 Self {
359 power: 1,
360 root: 2,
361 scale_bits: scale.to_bits(),
362 }
363 }
364
365 pub fn decode(&self, stored: f32) -> f32 {
367 let raised = stored.powi(i32::from(self.power));
368 let value = match self.root {
369 0 | 1 => raised,
370 2 => raised.max(0.0).sqrt(),
371 root => raised.max(0.0).powf(1.0 / f32::from(root)),
372 };
373 value * f32::from_bits(self.scale_bits)
374 }
375}
376impl ScalarEncoding {
377 pub fn check(&self) -> Result<(), String> {
379 let scale = f32::from_bits(self.scale_bits);
380 if self.power == 0 || self.root == 0 || !scale.is_finite() {
381 return Err(format!(
382 "an encoding of power {}, root {} and scale {scale}: power and root are never 0, the scale is finite",
383 self.power, self.root
384 ));
385 }
386 Ok(())
387 }
388}
389
390#[cfg(test)]
391#[allow(clippy::unwrap_used, reason = "hand-worked values")]
392mod tests {
393 use super::*;
394
395 #[test]
398 fn an_octahedral_encoding_states_its_stored_and_read_maps() {
399 assert_eq!(LaneAffine::UNORM, LaneAffine::new(0.5, 0.5));
400 assert_eq!(LaneAffine::SIGNED, LaneAffine::new(2.0, -1.0));
401 for e in [-1.0f32, -0.25, 0.0, 0.5, 1.0] {
402 assert_eq!(LaneAffine::SIGNED.apply(LaneAffine::UNORM.apply(e)), e);
403 }
404 let measured = NormalEncoding::Octahedral {
405 stored: LaneAffine::new(0.4995, 0.4995),
406 read: LaneAffine::SIGNED,
407 };
408 assert_ne!(
409 measured,
410 NormalEncoding::Octahedral {
411 stored: LaneAffine::UNORM,
412 read: LaneAffine::SIGNED,
413 }
414 );
415 assert_eq!(
416 serde_json::to_value(measured).unwrap(),
417 serde_json::json!({"octahedral": {
418 "stored": {"scale": 0.4995f32, "bias": 0.4995f32},
419 "read": {"scale": 2.0, "bias": -1.0},
420 }})
421 );
422 }
423 #[test]
429 fn the_cube_face_decode_inverts_its_encoding_and_the_measured_lanes() {
430 let k = 511.0f32 / 1023.0;
431 let minor = LaneAffine::new(k, k);
432 assert_eq!(minor.scale_bits, 0x3EFF_BFF0);
433 let payload = LaneAffine::new(0.498_046_88, 0.001_466_275_7);
434 let steps = [1.0 / 1023.0, 1.0 / 1023.0, 1.0 / 1023.0, 1.0 / 3.0];
435 let encoding = NormalEncoding::CubeFace { minor, payload };
436 assert_eq!(
437 serde_json::to_value(encoding).unwrap()["cube_face"]["minor"]["scale"],
438 serde_json::json!(k)
439 );
440 let measured: [([f32; 3], [f32; 4]); 10] = [
442 ([0.36, 0.48, 0.8], [0.679335, 0.259746, 0.498534, 0.666667]),
443 ([0.36, 0.48, -0.8], [0.319687, 0.259746, 0.501466, 0.666667]),
444 ([-0.48, 0.36, -0.8], [0.739277, 0.319687, 0.501466, 0.666667]),
445 ([0.8, 0.36, 0.48], [0.259746, 0.319687, 0.498534, 0.0]),
446 ([-0.8, 0.36, -0.48], [0.259746, 0.319687, 0.501466, 0.0]),
447 ([0.36, 0.8, -0.48], [0.679335, 0.259746, 0.498534, 0.333333]),
448 ([-0.48, -0.8, 0.36], [0.259746, 0.319687, 0.501466, 0.333333]),
449 ([0.0, 0.0, -1.0], [0.499511, 0.499511, 0.501466, 0.666667]),
450 ([0.0, 0.0, 1.0], [0.499511, 0.499511, 0.498534, 0.666667]),
451 ([-0.6, 0.0, -0.8], [0.799218, 0.499511, 0.501466, 0.666667]),
452 ];
453 for (n, t0) in measured {
454 let lanes = NormalEncoding::encode_cube_face(minor, payload, n, 0.0);
455 assert!(
456 lanes.iter().zip(&t0).all(|(a, b)| (a - b).abs() < 1e-6),
457 "{n:?}: {lanes:?} against {t0:?}"
458 );
459 let (back, p) = NormalEncoding::decode_cube_face(minor, payload, t0, steps).unwrap();
460 let dot: f64 = back.iter().zip(&n).map(|(a, b)| a * f64::from(*b)).sum();
461 assert!(dot > 1.0 - 1e-6 && p < 1e-3, "{n:?}: {back:?} payload {p}");
462 }
463 let quantise = |lanes: [f32; 4]| {
464 let q = |v: f32, levels: f32| (v * levels).round() / levels;
465 [
466 q(lanes[0], 1023.0),
467 q(lanes[1], 1023.0),
468 q(lanes[2], 1023.0),
469 q(lanes[3], 3.0),
470 ]
471 };
472 let round_trip = |n: [f64; 3], p: f32| {
473 let length = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
474 let n = n.map(|c| (c / length) as f32);
475 let lanes = quantise(NormalEncoding::encode_cube_face(minor, payload, n, p));
476 let (back, read) = NormalEncoding::decode_cube_face(minor, payload, lanes, steps).unwrap();
477 let dot: f64 = back.iter().zip(&n).map(|(a, b)| a * f64::from(*b)).sum();
478 (dot, (read - p).abs())
479 };
480 let mut worst = (1.0f64, 0.0f32);
482 for x in [-1.0, 0.0, 1.0] {
483 for y in [-1.0, 0.0, 1.0] {
484 for z in [-1.0, 0.0, 1.0] {
485 if [x, y, z] == [0.0; 3] {
486 continue;
487 }
488 for p in [0.0, 0.5, 1.0] {
489 let (dot, off) = round_trip([x, y, z], p);
490 worst = (worst.0.min(dot), worst.1.max(off));
491 }
492 }
493 }
494 }
495 for i in 0..40 {
497 for j in 0..80 {
498 let (theta, phi) = (
499 std::f64::consts::PI * (f64::from(i) + 0.5) / 40.0,
500 std::f64::consts::TAU * f64::from(j) / 80.0,
501 );
502 let (dot, off) = round_trip(
503 [theta.sin() * phi.cos(), theta.sin() * phi.sin(), theta.cos()],
504 0.25,
505 );
506 worst = (worst.0.min(dot), worst.1.max(off));
507 }
508 }
509 assert!(
511 worst.0 > 1.0 - 2e-5 && worst.1 < 2.0 / 1023.0,
512 "worst dot {}, payload off {}",
513 worst.0,
514 worst.1
515 );
516 let refused =
517 |lanes: [f32; 4]| NormalEncoding::decode_cube_face(minor, payload, lanes, steps).unwrap_err();
518 assert!(refused([0.5, 0.5, 0.49, 1.0]).contains("is not an axis"));
519 let fine = [steps[0], steps[1], steps[2], 1.0 / 1023.0];
521 assert!(
522 NormalEncoding::decode_cube_face(minor, payload, [0.5, 0.5, 0.49, 0.5], fine)
523 .unwrap_err()
524 .contains("is not an axis")
525 );
526 assert!(
527 NormalEncoding::decode_cube_face(minor, payload, [0.5, 0.5, 0.49, 341.0 / 1023.0], fine).is_ok()
528 );
529 assert!(refused([0.5, 0.5, 0.5 + 0.5 / 1023.0, 0.0]).contains("its sign cannot be read"));
530 }
531 #[test]
535 fn the_diamond_octahedral_decode_inverts_its_encoding() {
536 let unorm = |lanes: [u8; 3]| lanes.map(|v| f32::from(v) / 255.0);
537 let near = |a: [f64; 3], b: [f64; 3]| a.iter().zip(&b).all(|(x, y)| (x - y).abs() < 1e-9);
538 let (n, class) = NormalEncoding::decode_octahedral_diamond(unorm([255, 255, 5 << 3 | 1]));
539 assert!(near(n, [0.0, 0.0, 1.0]) && class == 5, "{n:?}");
540 let (n, _) = NormalEncoding::decode_octahedral_diamond(unorm([254, 254, 5 << 3 | 0b111]));
541 assert!(near(n, [1.0, 0.0, 0.0]), "{n:?}");
542 let (n, _) = NormalEncoding::decode_octahedral_diamond(unorm([0, 254, 5 << 3 | 0b101]));
543 assert!(near(n, [0.0, -1.0, 0.0]), "{n:?}");
544 assert_eq!(
545 NormalEncoding::encode_octahedral_diamond([0.0, 0.0, 1.0], 5),
546 [255, 255, 5 << 3 | 1]
547 );
548 assert_eq!(
549 NormalEncoding::encode_octahedral_diamond([1.0, 0.0, 0.0], 5),
550 [254, 254, 5 << 3 | 0b111]
551 );
552 assert_eq!(
553 NormalEncoding::encode_octahedral_diamond([0.0, -1.0, 0.0], 5),
554 [0, 254, 5 << 3 | 0b101]
555 );
556 let mut worst = 1.0f64;
558 for i in 0..40 {
559 for j in 0..80 {
560 let (theta, phi) = (
561 std::f64::consts::PI * (f64::from(i) + 0.5) / 40.0,
562 std::f64::consts::TAU * f64::from(j) / 80.0,
563 );
564 let n = [theta.sin() * phi.cos(), theta.sin() * phi.sin(), theta.cos()];
565 let class = (i % 6) as u8;
566 let lanes = NormalEncoding::encode_octahedral_diamond(n, class);
567 let (back, payload) = NormalEncoding::decode_octahedral_diamond(unorm(lanes));
568 assert_eq!(payload, class);
569 worst = worst.min(n[0] * back[0] + n[1] * back[1] + n[2] * back[2]);
570 }
571 }
572 assert!(worst > 1.0 - 2e-4, "worst dot {worst}");
574 }
575 #[test]
576 fn a_scalar_encoding_decodes_what_it_stores() {
577 let e = ScalarEncoding::square_root_of(512.0);
579 assert_eq!(e.decode(0.5), 128.0);
580 assert_eq!(ScalarEncoding::LINEAR.decode(0.25), 0.25);
581 assert_eq!(ScalarEncoding::square_root_of(1.0).decode(0.5), 0.25);
582 let fourth = ScalarEncoding::power_of(4, 4.0);
584 assert_eq!(fourth.decode(0.5), 0.25);
585 assert_eq!(ScalarEncoding::linear_over(16.0).decode(0.25), 4.0);
586 assert_eq!(
588 serde_json::to_value(fourth).unwrap(),
589 serde_json::json!({"power": 4, "scale": 4.0})
590 );
591 assert_eq!(
592 serde_json::to_value(ScalarEncoding::linear_over(0.0625)).unwrap(),
593 serde_json::json!({"power": 1, "scale": 0.0625})
594 );
595 assert_eq!(ScalarEncoding::power_of(0, 2.0), ScalarEncoding::linear_over(2.0));
597 let square = ScalarEncoding::square_of(0.95);
599 assert_eq!(square.decode(0.25), 0.475);
600 assert_eq!(square.decode(-0.25), 0.0);
601 assert_eq!(
602 serde_json::to_value(square).unwrap(),
603 serde_json::json!({"power": 1, "root": 2, "scale": 0.95f32})
604 );
605 }
606}