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}