Skip to main content

pfx_live/
noise_tiles.rs

1use pfx_load::{ColorSpace, Image, Pixels};
2use pfx_materials::{Hash, hash3};
3
4pub const SIZE: u32 = 512;
5pub const CELLS: u32 = 64;
6pub const LAYERS: u32 = 2;
7pub const OCTAVES: u32 = 2 * LAYERS;
8const SALT: i32 = 0x0d17;
9
10struct Lattice {
11    values: Vec<f32>,
12}
13
14impl Lattice {
15    fn new(salt: i32) -> Self {
16        let count = (CELLS * CELLS) as usize;
17        let hashes: Vec<f32> = (0..CELLS * CELLS)
18            .map(|index| {
19                let (i, j) = ((index % CELLS) as i32, (index / CELLS) as i32);
20                hash3([i, j, salt], Hash::Xor)
21            })
22            .collect();
23        let mut order: Vec<usize> = (0..count).collect();
24        order.sort_by(|&a, &b| hashes[a].total_cmp(&hashes[b]).then(a.cmp(&b)));
25        let mut values = vec![0.0; count];
26        for (rank, &index) in order.iter().enumerate() {
27            values[index] = (rank as f32 + 0.5) / count as f32;
28        }
29        Self { values }
30    }
31
32    fn at(&self, i: i64, j: i64) -> f32 {
33        let n = i64::from(CELLS);
34        self.values[(j.rem_euclid(n) * n + i.rem_euclid(n)) as usize]
35    }
36
37    fn value(&self, x: f32, y: f32) -> f32 {
38        let (fx, fy) = (x.floor(), y.floor());
39        let (i, j) = (fx as i64, fy as i64);
40        let (u, v) = (fade(x - fx), fade(y - fy));
41        let a = lerp(self.at(i, j), self.at(i + 1, j), u);
42        let b = lerp(self.at(i, j + 1), self.at(i + 1, j + 1), u);
43        lerp(a, b, v)
44    }
45}
46
47fn fade(t: f32) -> f32 {
48    t * t * (3.0 - 2.0 * t)
49}
50
51fn lerp(a: f32, b: f32, t: f32) -> f32 {
52    a + (b - a) * t
53}
54
55pub fn slice(pair: [f32; 2], depth: f32) -> f32 {
56    let k = depth.floor();
57    let w = fade(depth - k);
58    if k.rem_euclid(2.0) == 0.0 {
59        lerp(pair[0], pair[1], w)
60    } else {
61        lerp(pair[1], pair[0], w)
62    }
63}
64
65pub struct Octaves {
66    layers: Vec<[Lattice; 2]>,
67}
68
69impl Default for Octaves {
70    fn default() -> Self {
71        Self::new()
72    }
73}
74
75impl Octaves {
76    pub fn new() -> Self {
77        let layers = (0..OCTAVES as i32)
78            .map(|octave| {
79                [
80                    Lattice::new(SALT + octave * 2),
81                    Lattice::new(SALT + octave * 2 + 1),
82                ]
83            })
84            .collect();
85        Self { layers }
86    }
87
88    pub fn pair(&self, octave: u32, at: [f32; 2]) -> [f32; 2] {
89        let [even, odd] = &self.layers[octave as usize];
90        [even.value(at[0], at[1]), odd.value(at[0], at[1])]
91    }
92
93    pub fn value(&self, octave: u32, at: [f32; 3]) -> f32 {
94        slice(self.pair(octave, [at[0], at[1]]), at[2])
95    }
96
97    pub fn texel(&self, layer: u32, x: u32, y: u32) -> [f32; 4] {
98        let at = [
99            (x as f32 + 0.5) / SIZE as f32 * CELLS as f32,
100            (y as f32 + 0.5) / SIZE as f32 * CELLS as f32,
101        ];
102        let low = self.pair(layer * 2, at);
103        let high = self.pair(layer * 2 + 1, at);
104        [low[0], low[1], high[0], high[1]]
105    }
106}
107
108pub fn bytes(layer: u32) -> Vec<u8> {
109    let octaves = Octaves::new();
110    let mut out = Vec::with_capacity((SIZE * SIZE * 4) as usize);
111    for y in 0..SIZE {
112        for x in 0..SIZE {
113            out.extend(
114                octaves
115                    .texel(layer, x, y)
116                    .map(|value| (value * 255.0).round() as u8),
117            );
118        }
119    }
120    out
121}
122
123pub fn images() -> Vec<Image> {
124    (0..LAYERS)
125        .map(|layer| Image {
126            width: SIZE,
127            height: SIZE,
128            space: ColorSpace::Linear,
129            pixels: Pixels::Eight(bytes(layer)),
130        })
131        .collect()
132}
133
134pub fn sample(layers: &[Vec<u8>], octave: u32, at: [f32; 3]) -> f32 {
135    let bytes = &layers[(octave / 2) as usize];
136    let channel = (octave % 2) as usize * 2;
137    let x = at[0] / CELLS as f32 * SIZE as f32 - 0.5;
138    let y = at[1] / CELLS as f32 * SIZE as f32 - 0.5;
139    let (fx, fy) = (x.floor(), y.floor());
140    let (u, v) = (x - fx, y - fy);
141    let n = i64::from(SIZE);
142    let texel = |i: i64, j: i64| -> [f32; 2] {
143        let index = ((j.rem_euclid(n) * n + i.rem_euclid(n)) * 4) as usize + channel;
144        [f32::from(bytes[index]), f32::from(bytes[index + 1])]
145    };
146    let (i, j) = (fx as i64, fy as i64);
147    let (a, b, c, d) = (
148        texel(i, j),
149        texel(i + 1, j),
150        texel(i, j + 1),
151        texel(i + 1, j + 1),
152    );
153    let pair: [f32; 2] =
154        std::array::from_fn(|k| lerp(lerp(a[k], b[k], u), lerp(c[k], d[k], u), v) / 255.0);
155    slice(pair, at[2])
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use pfx_materials::{Hash, hash2, value_noise3};
162
163    fn noise(p: [f32; 3]) -> f32 {
164        value_noise3(p, Hash::Xor)
165    }
166
167    fn smoothstep(a: f32, b: f32, x: f32) -> f32 {
168        let t = ((x - a) / (b - a)).clamp(0.0, 1.0);
169        t * t * (3.0 - 2.0 * t)
170    }
171
172    fn unit(seed: u32, i: u32, k: i32) -> f32 {
173        hash2([i as i32, k], seed)
174    }
175
176    struct Moments {
177        count: f64,
178        sum: f64,
179        squares: f64,
180    }
181
182    impl Moments {
183        fn of(values: impl Iterator<Item = f32>) -> Self {
184            let mut out = Self {
185                count: 0.0,
186                sum: 0.0,
187                squares: 0.0,
188            };
189            for value in values {
190                out.count += 1.0;
191                out.sum += f64::from(value);
192                out.squares += f64::from(value) * f64::from(value);
193            }
194            out
195        }
196
197        fn mean(&self) -> f64 {
198            self.sum / self.count
199        }
200
201        fn deviation(&self) -> f64 {
202            (self.squares / self.count - self.mean() * self.mean())
203                .max(0.0)
204                .sqrt()
205        }
206    }
207
208    fn planes(count: u32, points: u32, extent: f32) -> Vec<[f32; 3]> {
209        let mut out = Vec::with_capacity((count * points) as usize);
210        for plane in 0..count {
211            let depth = unit(41, plane, 0) * 3.0 - 1.5;
212            for i in 0..points {
213                let a = (unit(plane + 100, i, 1) - 0.5) * extent;
214                let b = (unit(plane + 100, i, 2) - 0.5) * extent;
215                out.push([a, b, depth]);
216            }
217        }
218        out
219    }
220
221    fn tiled(layers: &[Vec<u8>], q: [f32; 3], octave: u32) -> f32 {
222        sample(layers, octave, q)
223    }
224
225    fn layers() -> Vec<Vec<u8>> {
226        (0..LAYERS).map(bytes).collect()
227    }
228
229    #[test]
230    fn the_tiles_are_the_same_bytes_every_time() {
231        use sha2::Digest;
232        for (layer, pinned) in TILE_SHA256.iter().enumerate() {
233            let first = bytes(layer as u32);
234            assert_eq!(first.len(), (SIZE * SIZE * 4) as usize);
235            assert_eq!(first, bytes(layer as u32));
236            let digest = sha2::Sha256::digest(&first);
237            let hex: String = digest.iter().map(|b| format!("{b:02x}")).collect();
238            assert_eq!(hex, *pinned, "layer {layer}");
239        }
240    }
241
242    const TILE_SHA256: [&str; LAYERS as usize] = [
243        "a972ac328a5c786fb084df8b3bf8cc5a4f6145bcf8a6b70e2a51deb2d5878177",
244        "56ef7600ad51ae1973895711f7f9633789ac8669fad7c0ef0c650aee3d0a9ee3",
245    ];
246
247    #[test]
248    fn the_tiles_tile_and_share_the_lacquer_tiles_size_and_cells() {
249        assert_eq!(SIZE, crate::lacquer::SIZE);
250        assert_eq!(CELLS, crate::lacquer::FINE_CELLS);
251        let octaves = Octaves::new();
252        for i in 0..64 {
253            let at = [
254                unit(7, i, 0) * 64.0,
255                unit(7, i, 1) * 64.0,
256                unit(7, i, 2) * 9.0,
257            ];
258            for octave in 0..OCTAVES {
259                let a = octaves.value(octave, at);
260                let b = octaves.value(octave, [at[0] + 64.0, at[1] - 128.0, at[2] + 2.0]);
261                assert!((a - b).abs() < 1e-4, "{a} {b}");
262            }
263        }
264    }
265
266    #[test]
267    fn a_slice_crosses_the_depth_lattice_continuously() {
268        let octaves = Octaves::new();
269        for i in 0..64 {
270            let at = [unit(9, i, 0) * 64.0, unit(9, i, 1) * 64.0];
271            for k in -3..4 {
272                let edge = k as f32;
273                let below = octaves.value(i % OCTAVES, [at[0], at[1], edge - 1e-4]);
274                let above = octaves.value(i % OCTAVES, [at[0], at[1], edge + 1e-4]);
275                assert!((below - above).abs() < 1e-3, "{below} {above} at {edge}");
276            }
277        }
278    }
279
280    #[test]
281    fn the_filtered_tiles_follow_the_exact_octaves() {
282        let tiles = layers();
283        let octaves = Octaves::new();
284        for octave in 0..OCTAVES {
285            let mut error = 0.0f64;
286            let mut spread = 0.0f64;
287            for i in 0..20_000 {
288                let at = [
289                    unit(octave + 11, i, 0) * 64.0,
290                    unit(octave + 11, i, 1) * 64.0,
291                    unit(octave + 11, i, 2) * 8.0,
292                ];
293                let exact = octaves.value(octave, at);
294                let read = tiled(&tiles, at, octave);
295                error += f64::from(read - exact).powi(2);
296                spread += f64::from(exact - 0.5).powi(2);
297            }
298            let relative = (error / spread).sqrt();
299            println!(
300                "octave {octave}: filtered tile against exact, relative rms error {relative:.4}"
301            );
302            assert!(relative < 0.03, "octave {octave}: {relative}");
303        }
304    }
305
306    #[test]
307    fn a_tiled_octave_has_the_hash_noises_statistics_on_planes() {
308        let tiles = layers();
309        let points = planes(400, 200, 0.4);
310        for octave in 0..OCTAVES {
311            let hashed = Moments::of(
312                points
313                    .iter()
314                    .map(|p| noise([p[0] * 160.0, p[1] * 160.0, p[2] * 7.3])),
315            );
316            let read = Moments::of(
317                points
318                    .iter()
319                    .map(|p| tiled(&tiles, [p[0] * 160.0, p[1] * 160.0, p[2] * 7.3], octave)),
320            );
321            println!(
322                "octave {octave}: mean {:.4} hashed, {:.4} tiled; deviation {:.4} hashed, {:.4} tiled",
323                hashed.mean(),
324                read.mean(),
325                hashed.deviation(),
326                read.deviation()
327            );
328            assert!((read.mean() - hashed.mean()).abs() < 0.01);
329            assert!((read.deviation() / hashed.deviation() - 1.0).abs() < 0.04);
330        }
331    }
332
333    fn line(n: f32) -> f32 {
334        1.0 - smoothstep(0.0, 0.045, (n - 0.5).abs())
335    }
336
337    #[test]
338    fn scratch_lines_and_smudges_keep_their_cover_on_planes() {
339        let tiles = layers();
340        let points = planes(1000, 400, 8.0);
341        for (face, side) in [("side", true), ("top", false)] {
342            let at = |p: &[f32; 3]| -> [f32; 3] {
343                if side {
344                    [p[0] * 14.0 + 3.0, p[1] * 700.0, p[2] * 14.0]
345                } else {
346                    [p[0] * 14.0 + 3.0, p[2] * 700.0, p[1] * 14.0]
347                }
348            };
349            let hashed = Moments::of(points.iter().map(|p| line(noise(at(p)))));
350            let read = Moments::of(points.iter().map(|p| {
351                let q = at(p);
352                let plane = if side {
353                    q
354                } else {
355                    [q[2] + 29.0, q[0] + 7.0, q[1]]
356                };
357                line(tiled(&tiles, plane, 2))
358            }));
359            println!(
360                "scratch line on a {face} face: cover {:.4} hashed, {:.4} tiled",
361                hashed.mean(),
362                read.mean()
363            );
364            assert!((read.mean() / hashed.mean() - 1.0).abs() < 0.06);
365        }
366        let smudge = |a: f32, b: f32| smoothstep(0.52, 0.78, a * 0.65 + b * 0.35);
367        let hashed = Moments::of(
368            points
369                .iter()
370                .map(|p| smudge(noise(p.map(|v| v * 20.0)), noise(p.map(|v| v * 50.0)))),
371        );
372        let read = Moments::of(points.iter().map(|p| {
373            smudge(
374                tiled(&tiles, p.map(|v| v * 20.0), 3),
375                tiled(&tiles, p.map(|v| v * 50.0), 1),
376            )
377        }));
378        println!(
379            "scratch smudge: cover {:.4} hashed, {:.4} tiled; deviation {:.4} hashed, {:.4} tiled",
380            hashed.mean(),
381            read.mean(),
382            hashed.deviation(),
383            read.deviation()
384        );
385        assert!((read.mean() - hashed.mean()).abs() < 0.01);
386        assert!((read.deviation() / hashed.deviation() - 1.0).abs() < 0.08);
387    }
388
389    #[test]
390    fn the_shader_reads_the_tiles_with_their_constants() {
391        let line = format!("const tile_cells: f32 = {}.0;", CELLS);
392        assert!(crate::maps::MAPS_WGSL.contains(&line), "{line}");
393    }
394}