Skip to main content

pfx_live/
lacquer.rs

1use pfx_load::{ColorSpace, Image, Pixels};
2use pfx_materials::{CoatWobble, Hash, hash3};
3
4pub const SIZE: u32 = 512;
5pub const BROAD_RANGE: f32 = 2.5;
6pub const FINE_RANGE: f32 = 1.5;
7pub const FINE_CELLS: u32 = 64;
8pub const SLICE: f32 = 0.861_891_6;
9const BROAD_SALT: i32 = 0x55;
10const COARSE_SALT: i32 = 0x16;
11const FINE_SALT: i32 = 0x95;
12pub const BLEND: f32 = 0.5;
13const STEP: f32 = 0.35;
14
15struct Lattice {
16    cells: u32,
17    values: Vec<f32>,
18}
19
20impl Lattice {
21    fn new(cells: u32, salt: i32) -> Self {
22        let values = (0..cells * cells)
23            .map(|index| {
24                let (i, j) = ((index % cells) as i32, (index / cells) as i32);
25                hash3([i, j, salt], Hash::Xor)
26            })
27            .collect();
28        Self { cells, values }
29    }
30
31    fn at(&self, i: i64, j: i64) -> f32 {
32        let n = i64::from(self.cells);
33        self.values[(j.rem_euclid(n) * n + i.rem_euclid(n)) as usize]
34    }
35
36    fn value(&self, x: f32, y: f32) -> f32 {
37        let (fx, fy) = (x.floor(), y.floor());
38        let (i, j) = (fx as i64, fy as i64);
39        let (u, v) = (fade(x - fx), fade(y - fy));
40        let a = lerp(self.at(i, j), self.at(i + 1, j), u);
41        let b = lerp(self.at(i, j + 1), self.at(i + 1, j + 1), u);
42        lerp(a, b, v)
43    }
44
45    fn gradient(&self, x: f32, y: f32) -> [f32; 2] {
46        [
47            (self.value(x + STEP, y) - self.value(x - STEP, y)) / (2.0 * STEP),
48            (self.value(x, y + STEP) - self.value(x, y - STEP)) / (2.0 * STEP),
49        ]
50    }
51}
52
53fn fade(t: f32) -> f32 {
54    t * t * (3.0 - 2.0 * t)
55}
56
57fn lerp(a: f32, b: f32, t: f32) -> f32 {
58    a + (b - a) * t
59}
60
61pub struct Octaves {
62    broad: Lattice,
63    coarse: Lattice,
64    fine: Lattice,
65    weights: [f32; 2],
66}
67
68pub fn cells(frequency: f32) -> u32 {
69    frequency.round().clamp(1.0, SIZE as f32) as u32
70}
71
72impl Octaves {
73    pub fn new(coat: &CoatWobble) -> Self {
74        Self {
75            broad: Lattice::new(cells(coat.frequencies[1]), BROAD_SALT),
76            coarse: Lattice::new(cells(coat.frequencies[2]), COARSE_SALT),
77            fine: Lattice::new(FINE_CELLS, FINE_SALT),
78            weights: coat.weights,
79        }
80    }
81
82    pub fn broad(&self, at: [f32; 2]) -> [f32; 2] {
83        let (broad, coarse) = (self.broad.cells as f32, self.coarse.cells as f32);
84        let a = self.broad.gradient(at[0] * broad, at[1] * broad);
85        let b = self.coarse.gradient(at[0] * coarse, at[1] * coarse);
86        let [wa, wb] = self.weights;
87        [
88            (a[0] * wa + b[0] * wb) * SLICE,
89            (a[1] * wa + b[1] * wb) * SLICE,
90        ]
91    }
92
93    pub fn fine(&self, at: [f32; 2]) -> [f32; 2] {
94        let cells = FINE_CELLS as f32;
95        let g = self.fine.gradient(at[0] * cells, at[1] * cells);
96        [g[0] * SLICE, g[1] * SLICE]
97    }
98
99    pub fn texel(&self, x: u32, y: u32) -> [f32; 4] {
100        let at = [
101            (x as f32 + 0.5) / SIZE as f32,
102            (y as f32 + 0.5) / SIZE as f32,
103        ];
104        let broad = self.broad(at);
105        let fine = self.fine(at);
106        [
107            encode(broad[0], BROAD_RANGE),
108            encode(broad[1], BROAD_RANGE),
109            encode(fine[0], FINE_RANGE),
110            encode(fine[1], FINE_RANGE),
111        ]
112    }
113}
114
115fn encode(value: f32, range: f32) -> f32 {
116    (value / range * 0.5 + 0.5).clamp(0.0, 1.0)
117}
118
119pub fn decode(byte: u8, range: f32) -> f32 {
120    (f32::from(byte) / 255.0 * 2.0 - 1.0) * range
121}
122
123pub fn fine_scale(coat: &CoatWobble) -> f32 {
124    coat.frequencies[0] / FINE_CELLS as f32
125}
126
127pub fn bytes(coat: &CoatWobble) -> Vec<u8> {
128    let octaves = Octaves::new(coat);
129    let mut out = Vec::with_capacity((SIZE * SIZE * 4) as usize);
130    for y in 0..SIZE {
131        for x in 0..SIZE {
132            out.extend(
133                octaves
134                    .texel(x, y)
135                    .map(|value| (value * 255.0).round() as u8),
136            );
137        }
138    }
139    out
140}
141
142pub fn image(coat: &CoatWobble) -> Image {
143    Image {
144        width: SIZE,
145        height: SIZE,
146        space: ColorSpace::Linear,
147        pixels: Pixels::Eight(bytes(coat)),
148    }
149}
150
151pub fn sample(bytes: &[u8], at: [f32; 2]) -> [f32; 4] {
152    let x = at[0] * SIZE as f32 - 0.5;
153    let y = at[1] * SIZE as f32 - 0.5;
154    let (fx, fy) = (x.floor(), y.floor());
155    let (u, v) = (x - fx, y - fy);
156    let n = i64::from(SIZE);
157    let texel = |i: i64, j: i64| -> [f32; 4] {
158        let index = ((j.rem_euclid(n) * n + i.rem_euclid(n)) * 4) as usize;
159        std::array::from_fn(|c| f32::from(bytes[index + c]))
160    };
161    let (i, j) = (fx as i64, fy as i64);
162    let (a, b, c, d) = (
163        texel(i, j),
164        texel(i + 1, j),
165        texel(i, j + 1),
166        texel(i + 1, j + 1),
167    );
168    let mixed: [f32; 4] =
169        std::array::from_fn(|k| lerp(lerp(a[k], b[k], u), lerp(c[k], d[k], u), v) / 255.0);
170    [
171        (mixed[0] * 2.0 - 1.0) * BROAD_RANGE,
172        (mixed[1] * 2.0 - 1.0) * BROAD_RANGE,
173        (mixed[2] * 2.0 - 1.0) * FINE_RANGE,
174        (mixed[3] * 2.0 - 1.0) * FINE_RANGE,
175    ]
176}
177
178pub fn weights(face: [f32; 3]) -> [f32; 3] {
179    let size = (face[0] * face[0] + face[1] * face[1] + face[2] * face[2]).sqrt();
180    if size <= 0.0 {
181        return [0.0, 0.0, 1.0];
182    }
183    let w = face.map(|v| (v.abs() / size - BLEND).max(0.0));
184    let total = w[0] + w[1] + w[2];
185    w.map(|v| v / total)
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use pfx_materials::value_noise3_gradient;
192
193    const COAT: CoatWobble = CoatWobble::SAMPLE;
194
195    fn tangential(g: [f32; 3]) -> [f32; 2] {
196        [g[0], g[1]]
197    }
198
199    fn rms(values: &[[f32; 2]]) -> f64 {
200        let total: f64 = values
201            .iter()
202            .map(|g| f64::from(g[0] * g[0] + g[1] * g[1]))
203            .sum();
204        (total / values.len() as f64).sqrt()
205    }
206
207    fn points(count: usize, seed: u32) -> Vec<[f32; 2]> {
208        (0..count as u32)
209            .map(|i| {
210                [
211                    pfx_materials::hash2([i as i32, 1], seed),
212                    pfx_materials::hash2([i as i32, 2], seed),
213                ]
214            })
215            .collect()
216    }
217
218    #[test]
219    fn the_slice_factor_is_the_root_of_a_smoothstep_blend_of_two_layers() {
220        let steps = 200_000;
221        let mean = (0..steps)
222            .map(|i| {
223                let w = fade((i as f32 + 0.5) / steps as f32);
224                f64::from((1.0 - w) * (1.0 - w) + w * w)
225            })
226            .sum::<f64>()
227            / steps as f64;
228        assert!((mean - 26.0 / 35.0).abs() < 1e-6, "{mean}");
229        assert!((f64::from(SLICE) - (26.0f64 / 35.0).sqrt()).abs() < 1e-6);
230    }
231
232    #[test]
233    fn the_tile_is_the_same_bytes_every_time() {
234        let first = bytes(&COAT);
235        assert_eq!(first.len(), (SIZE * SIZE * 4) as usize);
236        assert_eq!(first, bytes(&COAT));
237        use sha2::Digest;
238        let digest = sha2::Sha256::digest(&first);
239        let hex: String = digest.iter().map(|b| format!("{b:02x}")).collect();
240        assert_eq!(hex, TILE_SHA256);
241    }
242
243    const TILE_SHA256: &str = "7e79b641b9311c427aee4de99958d8d1ecb4efb10a0edadcf6aa98fb1a247eac";
244
245    #[test]
246    fn no_coat_leaves_the_broad_channels_flat_and_keeps_the_fine_one() {
247        let flat = bytes(&CoatWobble::default());
248        assert!(flat.chunks(4).all(|texel| texel[..2] == [128, 128]));
249        assert!(flat.chunks(4).any(|texel| texel[2..] != [128, 128]));
250    }
251
252    #[test]
253    fn the_tile_tiles() {
254        let octaves = Octaves::new(&COAT);
255        for at in points(64, 7) {
256            let a = octaves.broad(at);
257            let b = octaves.broad([at[0] + 1.0, at[1] - 1.0]);
258            let c = octaves.fine(at);
259            let d = octaves.fine([at[0] - 1.0, at[1] + 2.0]);
260            for k in 0..2 {
261                assert!((a[k] - b[k]).abs() < 1e-3);
262                assert!((c[k] - d[k]).abs() < 1e-3);
263            }
264        }
265    }
266
267    #[test]
268    fn the_codes_cover_the_gradients() {
269        let octaves = Octaves::new(&COAT);
270        let mut broad = 0.0f32;
271        let mut fine = 0.0f32;
272        for y in (0..SIZE).step_by(3) {
273            for x in (0..SIZE).step_by(3) {
274                let at = [
275                    (x as f32 + 0.5) / SIZE as f32,
276                    (y as f32 + 0.5) / SIZE as f32,
277                ];
278                let a = octaves.broad(at);
279                let b = octaves.fine(at);
280                broad = broad.max(a[0].abs()).max(a[1].abs());
281                fine = fine.max(b[0].abs()).max(b[1].abs());
282            }
283        }
284        assert!(broad < BROAD_RANGE, "{broad}");
285        assert!(fine < FINE_RANGE, "{fine}");
286    }
287
288    #[test]
289    fn the_filtered_tile_follows_the_exact_gradients() {
290        let tile = bytes(&COAT);
291        let octaves = Octaves::new(&COAT);
292        let at = points(20_000, 11);
293        let mut broad_error = Vec::new();
294        let mut fine_error = Vec::new();
295        let mut broad_exact = Vec::new();
296        let mut fine_exact = Vec::new();
297        for p in &at {
298            let sampled = sample(&tile, *p);
299            let a = octaves.broad(*p);
300            let b = octaves.fine(*p);
301            broad_error.push([sampled[0] - a[0], sampled[1] - a[1]]);
302            fine_error.push([sampled[2] - b[0], sampled[3] - b[1]]);
303            broad_exact.push(a);
304            fine_exact.push(b);
305        }
306        let broad = rms(&broad_error) / rms(&broad_exact);
307        let fine = rms(&fine_error) / rms(&fine_exact);
308        println!("filtered tile against exact gradients: broad {broad:.4}, fine {fine:.4}");
309        assert!(broad < 0.03, "broad relative error {broad}");
310        assert!(fine < 0.03, "fine relative error {fine}");
311    }
312
313    #[test]
314    fn the_tile_matches_the_procedural_bump_on_planes() {
315        let tile = bytes(&COAT);
316        let mut procedural = Vec::new();
317        let mut sampled = Vec::new();
318        for plane in 0..64u32 {
319            let z = pfx_materials::hash2([plane as i32, 9], 3) * 0.4 - 0.2;
320            for p in points(400, plane + 100) {
321                let q = [p[0] * 0.3 - 0.15, p[1] * 0.3 - 0.15];
322                let g = COAT
323                    .frequencies
324                    .iter()
325                    .zip([1.0, COAT.weights[0], COAT.weights[1]])
326                    .map(|(&scale, weight)| {
327                        tangential(value_noise3_gradient([q[0], q[1], z], scale))
328                            .map(|v| v * weight)
329                    })
330                    .fold([0.0f32; 2], |a, b| [a[0] + b[0], a[1] + b[1]]);
331                procedural.push(g);
332                let t = sample(&tile, [q[0], q[1]]);
333                let f = sample(&tile, [q[0] * fine_scale(&COAT), q[1] * fine_scale(&COAT)]);
334                sampled.push([t[0] + f[2], t[1] + f[3]]);
335            }
336        }
337        let ratio = rms(&sampled) / rms(&procedural);
338        println!("tile against procedural bump on planes: rms ratio {ratio:.4}");
339        assert!((ratio - 1.0).abs() < 0.05, "rms ratio {ratio}");
340    }
341
342    #[test]
343    fn the_shader_reads_the_tile_with_its_constants() {
344        for (name, value) in [
345            ("broad_range", BROAD_RANGE),
346            ("fine_range", FINE_RANGE),
347            ("blend", BLEND),
348        ] {
349            let line = format!("const lacquer_{name}: f32 = {value};");
350            assert!(crate::maps::MAPS_WGSL.contains(&line), "{line}");
351        }
352    }
353
354    #[test]
355    fn faces_pick_one_plane_near_an_axis_and_blend_between() {
356        assert_eq!(weights([0.0, 0.0, -3.0]), [0.0, 0.0, 1.0]);
357        assert_eq!(weights([0.0, 2.0, 0.0]), [0.0, 1.0, 0.0]);
358        assert_eq!(weights([0.0, 0.0, 0.0]), [0.0, 0.0, 1.0]);
359        let tilted = weights([0.3f32.sin(), 0.0, 0.3f32.cos()]);
360        assert_eq!(tilted, [0.0, 0.0, 1.0]);
361        let half = weights([1.0, 0.0, 1.0]);
362        assert!((half[0] - 0.5).abs() < 1e-6 && (half[2] - 0.5).abs() < 1e-6);
363        let corner = weights([1.0, 1.0, 1.0]);
364        assert!(corner.iter().all(|w| (w - 1.0 / 3.0).abs() < 1e-6));
365    }
366}