Skip to main content

pfx_materials/
ageing.rs

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}