Repository navigation
Expand file tree
/
Copy pathlut3d.rs
More file actions
143 lines (133 loc) · 4.87 KB
/
Copy pathlut3d.rs
File metadata and controls
143 lines (133 loc) · 4.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
//! 3D lookup tables for applying a display transform on the GPU.
use crate::Transform;
/// An `size³` RGBA lookup table: entry `r + g·size + b·size²` holds the output for input
/// `(r, g, b) / (size − 1)`, alpha 1. This is the memory order of a 3D texture whose x axis is
/// red, so it uploads directly (sample at `(c·(size−1) + 0.5) / size` with linear filtering).
#[derive(Clone, Debug, PartialEq)]
pub struct Lut3d {
pub size: usize,
pub data: Vec<[f32; 4]>,
}
impl Lut3d {
/// Samples a 3-input transform (1-output transforms are replicated to gray).
pub fn from_transform(t: &Transform, size: usize) -> Lut3d {
assert!(t.inputs() == 3 && (t.outputs() == 3 || t.outputs() == 1), "display LUTs need a 3→3 or 3→1 transform");
assert!(size >= 2, "LUT size");
let mut data = vec![[0.0f32, 0.0, 0.0, 1.0]; size * size * size];
let s = (size - 1) as f32;
let mut out = [0.0f32; 16];
for b in 0..size {
for g in 0..size {
for r in 0..size {
t.eval(&[r as f32 / s, g as f32 / s, b as f32 / s], &mut out);
let v = if t.outputs() == 1 { [out[0]; 3] } else { [out[0], out[1], out[2]] };
data[r + g * size + b * size * size] = [v[0], v[1], v[2], 1.0];
}
}
}
Lut3d { size, data }
}
/// Trilinear lookup (what GPU filtering computes).
pub fn sample(&self, rgb: [f32; 3]) -> [f32; 3] {
let n = self.size;
let loc = |v: f32| {
let p = v.clamp(0.0, 1.0) * (n - 1) as f32;
let i = (p as usize).min(n - 2);
(i, p - i as f32)
};
let (r0, fr) = loc(rgb[0]);
let (g0, fg) = loc(rgb[1]);
let (b0, fb) = loc(rgb[2]);
let at = |r: usize, g: usize, b: usize| self.data[r + g * n + b * n * n];
let mut o = [0.0f32; 3];
for (k, ov) in o.iter_mut().enumerate() {
let c = |r, g, b| at(r, g, b)[k];
let x00 = c(r0, g0, b0) + (c(r0 + 1, g0, b0) - c(r0, g0, b0)) * fr;
let x10 = c(r0, g0 + 1, b0) + (c(r0 + 1, g0 + 1, b0) - c(r0, g0 + 1, b0)) * fr;
let x01 = c(r0, g0, b0 + 1) + (c(r0 + 1, g0, b0 + 1) - c(r0, g0, b0 + 1)) * fr;
let x11 = c(r0, g0 + 1, b0 + 1) + (c(r0 + 1, g0 + 1, b0 + 1) - c(r0, g0 + 1, b0 + 1)) * fr;
let y0 = x00 + (x10 - x00) * fg;
let y1 = x01 + (x11 - x01) * fg;
*ov = y0 + (y1 - y0) * fb;
}
o
}
/// Little-endian RGBA16F texels (8 bytes each), e.g. for `wgpu::TextureFormat::Rgba16Float`.
pub fn to_rgba16f_bytes(&self) -> Vec<u8> {
let mut o = Vec::with_capacity(self.data.len() * 8);
for px in &self.data {
for v in px {
o.extend_from_slice(&f32_to_f16(*v).to_le_bytes());
}
}
o
}
/// RGBA8 texels (for backends without float 3D textures).
pub fn to_rgba8(&self) -> Vec<u8> {
self.data.iter().flat_map(|p| p.map(|v| (v.clamp(0.0, 1.0) * 255.0 + 0.5) as u8)).collect()
}
}
/// IEEE 754 binary16 bits of `v` (round to nearest even; overflow → ±inf, NaN kept).
pub fn f32_to_f16(v: f32) -> u16 {
let x = v.to_bits();
let sign = ((x >> 16) & 0x8000) as u16;
let exp = ((x >> 23) & 0xFF) as i32;
let man = x & 0x7F_FFFF;
if exp == 0xFF {
return sign | 0x7C00 | if man != 0 { 0x200 } else { 0 };
}
let e = exp - 127 + 15;
if e >= 0x1F {
return sign | 0x7C00;
}
if e <= 0 {
if e < -10 {
return sign;
}
let m = man | 0x80_0000;
let shift = (14 - e) as u32;
let half = 1u32 << (shift - 1);
let rem = m & ((1 << shift) - 1);
let mut r = m >> shift;
if rem > half || (rem == half && r & 1 == 1) {
r += 1;
}
return sign | r as u16;
}
let mut r = ((e as u32) << 10) | (man >> 13);
let rem = man & 0x1FFF;
if rem > 0x1000 || (rem == 0x1000 && r & 1 == 1) {
r += 1;
}
sign | r as u16
}
/// binary16 bits → f32.
pub fn f16_to_f32(h: u16) -> f32 {
let sign = if h & 0x8000 != 0 { -1.0 } else { 1.0 };
let e = ((h >> 10) & 0x1F) as i32;
let m = (h & 0x3FF) as f32;
match e {
0 => sign * m * 2f32.powi(-24),
0x1F => {
if m == 0.0 {
sign * f32::INFINITY
} else {
f32::NAN
}
}
_ => sign * (1.0 + m / 1024.0) * 2f32.powi(e - 15),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn half_float_roundtrip() {
for v in [0.0f32, 1.0, 0.5, 0.333, 1e-5, 65504.0, -2.25, 0.1] {
let back = f16_to_f32(f32_to_f16(v));
assert!((back - v).abs() <= v.abs() * 1e-3 + 1e-7, "{v} -> {back}");
}
assert_eq!(f32_to_f16(1.0), 0x3C00);
assert_eq!(f32_to_f16(1e6), 0x7C00);
}
}