1use crate::colour::luminance;
2use crate::kinds::Scratch;
3use crate::material::{Ageing, At, Material};
4use crate::math::{clamp01, lerp, lerp3, scale3, smoothstep};
5use crate::noise::{Hash, fbm3, hash3, noise2, value_noise3};
6
7const AGEING_SCRATCH: Scratch = Scratch {
8 line: 900.0,
9 smudge: [22.0, 61.0],
10 speck: 2600.0,
11};
12
13fn noise(p: [f32; 3]) -> f32 {
14 value_noise3(p, Hash::Xor)
15}
16
17fn nested(p: [f32; 3]) -> f32 {
18 value_noise3(p, Hash::Nested)
19}
20
21fn fbm(p: [f32; 3], octaves: i32) -> f32 {
22 fbm3(p, octaves, Hash::Nested)
23}
24
25pub fn apply(material: &Material, age: f32, at: &At) -> Material {
26 let age = clamp01(age);
27 if age == 0.0 || material.ageing.inactive() {
28 return *material;
29 }
30 let mut out = *material;
31 let amount = scale_age(material.ageing, age);
32 out.base = fade(out.base, amount.fade, at.position, material.ageing.seed);
33 out.base = yellow(out.base, amount.yellow, at.uv, material.ageing.seed);
34 if amount.ink > 0.0 {
35 let ink = [out.base[0], out.base[1], out.base[2], 1.0];
36 let aged = ink_age(
37 |_| ink,
38 [at.position[0], at.position[1]],
39 1.0,
40 material.ageing.seed,
41 );
42 out.base = lerp3(out.base, [aged[0], aged[1], aged[2]], amount.ink);
43 }
44 if out.clearcoat > 0.0 {
45 let coated = scratch_coat(
46 out.base,
47 out.clearcoat_roughness,
48 amount.scratch,
49 at.position,
50 material.ageing.seed,
51 );
52 out.base = coated.0;
53 out.clearcoat_roughness = coated.1;
54 } else {
55 let scratched = scratch(
56 out.base,
57 out.roughness,
58 amount.scratch,
59 at.position,
60 material.ageing.seed,
61 );
62 out.base = scratched.0;
63 out.roughness = scratched.1;
64 }
65 let worn = worn_edge(out.base, out.roughness, amount.edge, at.edge);
66 out.base = worn.0;
67 out.roughness = worn.1;
68 let dusty = dust(
69 out.base,
70 out.roughness,
71 amount.dust,
72 at.position,
73 material.ageing.seed,
74 );
75 out.base = dusty.0;
76 out.roughness = dusty.1;
77 let patina = patina(
78 out.base,
79 out.roughness,
80 out.metalness,
81 amount.patina,
82 at.position,
83 material.ageing.seed,
84 );
85 out.base = patina.0;
86 out.roughness = patina.1;
87 out.metalness = patina.2;
88 out
89}
90
91fn scale_age(ageing: Ageing, age: f32) -> Ageing {
92 Ageing {
93 fade: ageing.fade * age,
94 yellow: ageing.yellow * age,
95 ink: ageing.ink * age,
96 scratch: ageing.scratch * age,
97 edge: ageing.edge * age,
98 dust: ageing.dust * age,
99 patina: ageing.patina * age,
100 seed: ageing.seed,
101 }
102}
103
104fn fade(base: [f32; 3], amount: f32, position: [f32; 3], seed: u32) -> [f32; 3] {
105 if amount <= 0.0 {
106 return base;
107 }
108 let y = luminance(base);
109 let n = noise([
110 position[0] * 3.0 + seed as f32 * 0.17,
111 position[1] * 3.0,
112 position[2] * 3.0,
113 ]);
114 let t = clamp01(amount * (0.65 + 0.35 * n));
115 lerp3(base, [y, y, y], t)
116}
117
118pub fn yellow(base: [f32; 3], amount: f32, uv: [f32; 2], seed: u32) -> [f32; 3] {
119 if amount <= 0.0 {
120 return base;
121 }
122 let shift = seed as f32 * 0.01;
123 let fox = smoothstep(
124 0.90,
125 0.97,
126 nested([uv[0] * 140.0 + shift, uv[1] * 140.0, 11.0]),
127 ) * smoothstep(
128 0.55,
129 0.85,
130 nested([uv[0] * 12.0, uv[1] * 12.0 + shift, 12.0]),
131 );
132 let mut out = mul_mix(base, [0.86, 0.74, 0.60], fox * 0.35 * amount);
133 let blot = fbm([uv[0] * 5.0 + shift, uv[1] * 5.0, 13.0], 4);
134 let tide = smoothstep(0.60, 0.63, blot) * (1.0 - smoothstep(0.63, 0.68, blot));
135 let stain = smoothstep(0.58, 0.72, blot) * 0.25 + tide * 0.3;
136 out = mul_mix(out, [0.95, 0.91, 0.84], stain * amount);
137 let edge = 1.0 - smoothstep(0.0, 0.008, uv[1] - 0.004) * smoothstep(0.0, 0.008, uv[0]);
138 mul_mix(out, [0.93, 0.88, 0.78], edge * 0.5 * amount)
139}
140
141fn mul_mix(base: [f32; 3], tint: [f32; 3], t: f32) -> [f32; 3] {
142 let t = clamp01(t);
143 [
144 base[0] * lerp(1.0, tint[0], t),
145 base[1] * lerp(1.0, tint[1], t),
146 base[2] * lerp(1.0, tint[2], t),
147 ]
148}
149
150fn marks(position: [f32; 3], seed: u32, amount: f32, scales: Scratch) -> (f32, f32) {
151 if amount <= 0.0 {
152 return (0.0, 0.0);
153 }
154 let q = scale3_local(position, scales.line);
155 let n = noise([q[0] * 0.02 + seed as f32, q[1], q[2] * 0.02]);
156 let line = 1.0 - smoothstep(0.0, 0.045, (n - 0.5).abs());
157 let smudge = smoothstep(
158 0.52,
159 0.78,
160 noise(scale3_local(position, scales.smudge[0])) * 0.65
161 + noise(scale3_local(position, scales.smudge[1])) * 0.35,
162 );
163 let cell = [
164 (position[0] * scales.speck).floor() as i32,
165 (position[1] * scales.speck).floor() as i32,
166 (position[2] * scales.speck).floor() as i32 ^ seed as i32,
167 ];
168 let speck = if hash3(cell, Hash::Xor) > 0.9985 {
169 1.0
170 } else {
171 0.0
172 };
173 let cover = amount.min(1.0);
174 let breakup = (smudge * 0.55 + line * 0.35 + speck).clamp(0.0, 1.0) * cover;
175 (breakup, speck * cover)
176}
177
178fn scratched_albedo(base: [f32; 3], speck: f32) -> [f32; 3] {
179 lerp3(base, add_const(scale3(base, 0.9), 0.06), speck)
180}
181
182pub fn scratch(
183 base: [f32; 3],
184 roughness: f32,
185 amount: f32,
186 position: [f32; 3],
187 seed: u32,
188) -> ([f32; 3], f32) {
189 scratch_marked(base, roughness, amount, position, seed, AGEING_SCRATCH)
190}
191
192pub fn scratch_marked(
193 base: [f32; 3],
194 roughness: f32,
195 amount: f32,
196 position: [f32; 3],
197 seed: u32,
198 scales: Scratch,
199) -> ([f32; 3], f32) {
200 let (breakup, speck) = marks(position, seed, amount, scales);
201 if breakup == 0.0 && speck == 0.0 {
202 return (base, roughness);
203 }
204 let rough2 = roughness * roughness;
205 let mixed = lerp(rough2, (rough2 * 3.0 + 0.03).min(0.5), breakup);
206 (scratched_albedo(base, speck), mixed.sqrt())
207}
208
209pub fn scratch_coat(
210 base: [f32; 3],
211 coat_roughness: f32,
212 amount: f32,
213 position: [f32; 3],
214 seed: u32,
215) -> ([f32; 3], f32) {
216 scratch_marked_coat(base, coat_roughness, amount, position, seed, AGEING_SCRATCH)
217}
218
219pub fn scratch_marked_coat(
220 base: [f32; 3],
221 coat_roughness: f32,
222 amount: f32,
223 position: [f32; 3],
224 seed: u32,
225 scales: Scratch,
226) -> ([f32; 3], f32) {
227 let (breakup, speck) = marks(position, seed, amount, scales);
228 if breakup == 0.0 && speck == 0.0 {
229 return (base, coat_roughness);
230 }
231 let rough2 = coat_roughness * coat_roughness;
232 let mixed = lerp(rough2, (rough2 * 4.0 + 0.02).min(0.35), breakup);
233 (scratched_albedo(base, speck), mixed.sqrt())
234}
235
236fn scale3_local(p: [f32; 3], s: f32) -> [f32; 3] {
237 [p[0] * s, p[1] * s, p[2] * s]
238}
239
240fn add_const(p: [f32; 3], s: f32) -> [f32; 3] {
241 [p[0] + s, p[1] + s, p[2] + s]
242}
243
244pub fn worn_edge(base: [f32; 3], roughness: f32, amount: f32, edge: f32) -> ([f32; 3], f32) {
245 let w = clamp01(amount) * clamp01(edge);
246 if w <= 0.0 {
247 return (base, roughness);
248 }
249 let lifted = [
250 base[0] * lerp(1.0, 1.6, 0.6),
251 base[1] * lerp(1.0, 1.42, 0.6),
252 base[2] * lerp(1.0, 1.18, 0.6),
253 ];
254 (
255 lerp3(base, lifted, w),
256 lerp(roughness, (roughness + 0.3).min(0.92), w),
257 )
258}
259
260pub fn dust(
261 base: [f32; 3],
262 roughness: f32,
263 amount: f32,
264 position: [f32; 3],
265 seed: u32,
266) -> ([f32; 3], f32) {
267 if amount <= 0.0 {
268 return (base, roughness);
269 }
270 let n = noise([
271 position[0] * 2.5 + seed as f32 * 0.13,
272 position[1] * 2.5,
273 position[2] * 2.5,
274 ]);
275 let mask = smoothstep(0.45, 0.8, n) * amount.min(1.0);
276 (
277 lerp3(base, [0.62, 0.58, 0.50], mask),
278 lerp(roughness, (roughness + 0.2).min(1.0), mask * 0.5),
279 )
280}
281
282pub fn patina(
283 base: [f32; 3],
284 roughness: f32,
285 metalness: f32,
286 amount: f32,
287 position: [f32; 3],
288 seed: u32,
289) -> ([f32; 3], f32, f32) {
290 let amount = clamp01(amount);
291 if amount <= 0.0 {
292 return (base, roughness, metalness);
293 }
294 let shift = seed as f32 * 0.001;
295 let p = [
296 position[0] + shift,
297 position[1] + shift,
298 position[2] + shift,
299 ];
300 let bloom = smoothstep(0.35, 0.75, fbm(scale3_local(p, 900.0), 4)) * amount;
301 let rub = smoothstep(
302 0.55,
303 0.85,
304 fbm(
305 [p[0] * 300.0 + 3.0, p[1] * 300.0 + 3.0, p[2] * 300.0 + 3.0],
306 3,
307 ),
308 ) * amount;
309 let dull = (0.5 * amount + 0.5 * bloom - 0.6 * rub).clamp(0.0, 1.0);
310 let mut f0 = [
311 base[0] * lerp(1.0, 0.52, dull),
312 base[1] * lerp(1.0, 0.44, dull),
313 base[2] * lerp(1.0, 0.35, dull),
314 ];
315 let green = smoothstep(
316 0.62,
317 0.9,
318 fbm(
319 [
320 p[0] * 1500.0 + 9.0,
321 p[1] * 1500.0 + 9.0,
322 p[2] * 1500.0 + 9.0,
323 ],
324 3,
325 ),
326 ) * amount
327 * 0.45;
328 f0 = lerp3(f0, [0.24, 0.30, 0.25], green);
329 let rough = (roughness + 0.3 * dull + 0.2 * green).clamp(0.1, 0.85);
330 let metal = lerp(metalness, 0.5, green);
331 (f0, rough, metal)
332}
333
334pub fn ink_age<F>(sample: F, at: [f32; 2], age: f32, seed: u32) -> [f32; 4]
335where
336 F: Fn([f32; 2]) -> [f32; 4],
337{
338 let age = clamp01(age);
339 let origin = [at[0].round(), at[1].round()];
340 let c = sample(origin);
341 if age == 0.0 {
342 return c;
343 }
344 let aged = inkfx(&sample, origin, seed);
345 [
346 lerp(c[0], aged[0], age),
347 lerp(c[1], aged[1], age),
348 lerp(c[2], aged[2], age),
349 lerp(c[3], aged[3], age),
350 ]
351}
352
353fn inkfx<F>(sample: &F, at: [f32; 2], seed: u32) -> [f32; 4]
354where
355 F: Fn([f32; 2]) -> [f32; 4],
356{
357 let p = [at[0] as i32, at[1] as i32];
358 let q = [p[0] as f32, p[1] as f32];
359 let tap = |x: i32, y: i32| sample([x as f32, y as f32]);
360 let c = tap(p[0], p[1]);
361 let angle = noise2([q[0] * 0.006, q[1] * 0.006], seed) * std::f32::consts::TAU * 2.0;
362 let fiber = [angle.cos(), angle.sin()];
363 let mut wick = c;
364 for k in 1..=3 {
365 let d = [
366 (fiber[0] * k as f32).round() as i32,
367 (fiber[1] * k as f32).round() as i32,
368 ];
369 let fall = 0.22_f32.powi(k)
370 * (0.6 + 0.8 * noise2([q[0] * 0.21 + k as f32 * 13.0, q[1] * 0.21], seed));
371 let a = tap(p[0] + d[0], p[1] + d[1]);
372 let b = tap(p[0] - d[0], p[1] - d[1]);
373 let neighbor = max4(a, b);
374 wick = max4(wick, scale4(neighbor, fall));
375 }
376 let mut wide = [0.0; 4];
377 for y in -3..=3 {
378 for x in -3..=3 {
379 let t = tap(p[0] + x * 2, p[1] + y * 2);
380 wide = [
381 wide[0] + t[0],
382 wide[1] + t[1],
383 wide[2] + t[2],
384 wide[3] + t[3],
385 ];
386 }
387 }
388 wide = scale4(wide, 1.0 / 49.0);
389 let grain = noise2([q[0] * 0.45, q[1] * 0.45], seed) * 0.55
390 + noise2([q[0] * 0.11, q[1] * 0.11], seed) * 0.45;
391 let absorb = 0.97 + 0.06 * (grain - 0.5);
392 let voids = smoothstep(
393 0.80,
394 0.93,
395 noise2([q[0] * 0.38 + 71.0, q[1] * 0.38 + 5.0], seed),
396 );
397 let edge = [
398 (c[0] - wide[0]).max(0.0),
399 (c[1] - wide[1]).max(0.0),
400 (c[2] - wide[2]).max(0.0),
401 (c[3] - wide[3]).max(0.0),
402 ];
403 let wash = smoothstep(0.05, 0.25, c[0]) * (1.0 - smoothstep(0.55, 0.85, c[0]));
404 let local_age = 0.96 + 0.04 * noise2([q[0] * 0.0025 + 3.0, q[1] * 0.0025 + 9.0], seed);
405 let wear = smoothstep(
406 0.62,
407 0.8,
408 noise2([q[0] * 0.09 + 40.0, q[1] * 0.09 + 2.0], seed),
409 );
410 [
411 ((wick[0] * absorb * 1.12 + edge[0] * 0.45 * wash)
412 * (1.0 - 0.45 * voids * (1.0 - c[0] * c[0]))
413 * local_age)
414 .clamp(0.0, 1.0),
415 (wick[1] * (0.94 + 0.08 * grain) * (1.0 - 0.2 * voids)).clamp(0.0, 1.0),
416 (c[2] * (1.0 - 0.35 * wear)).clamp(0.0, 1.0),
417 (wick[3] * (0.8 + 0.2 * grain) * (1.0 - 0.3 * voids)).clamp(0.0, 1.0),
418 ]
419}
420
421fn max4(a: [f32; 4], b: [f32; 4]) -> [f32; 4] {
422 [
423 a[0].max(b[0]),
424 a[1].max(b[1]),
425 a[2].max(b[2]),
426 a[3].max(b[3]),
427 ]
428}
429
430fn scale4(a: [f32; 4], s: f32) -> [f32; 4] {
431 [a[0] * s, a[1] * s, a[2] * s, a[3] * s]
432}