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}