Skip to main content

pfx_materials/
material.rs

1use serde::{Deserialize, Serialize};
2
3use crate::content::ContentLayer;
4
5pub const LAYER_CAP: usize = 4;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub enum Family {
10    #[default]
11    Plain,
12    Paper,
13    Plaster,
14    Stone,
15    Cloth,
16    Metal,
17    Occluder,
18    Petal,
19    Leaf,
20    Bark,
21    Cord,
22    Wood,
23    Lacquer,
24    Glass,
25    Liquid,
26    Emissive,
27    Chrome,
28}
29
30impl Family {
31    fn id(self) -> u32 {
32        match self {
33            Self::Plain => 0,
34            Self::Paper => 1,
35            Self::Plaster => 2,
36            Self::Stone => 3,
37            Self::Cloth => 4,
38            Self::Metal => 5,
39            Self::Occluder => 7,
40            Self::Petal => 8,
41            Self::Leaf => 9,
42            Self::Bark => 10,
43            Self::Cord => 11,
44            Self::Wood => 12,
45            Self::Lacquer => 13,
46            Self::Glass => 14,
47            Self::Liquid => 15,
48            Self::Emissive => 17,
49            Self::Chrome => 18,
50        }
51    }
52
53    fn from_id(id: u32) -> Self {
54        match id {
55            1 => Self::Paper,
56            2 => Self::Plaster,
57            3 => Self::Stone,
58            4 => Self::Cloth,
59            5 => Self::Metal,
60            7 => Self::Occluder,
61            8 => Self::Petal,
62            9 => Self::Leaf,
63            10 => Self::Bark,
64            11 => Self::Cord,
65            12 => Self::Wood,
66            13 => Self::Lacquer,
67            14 => Self::Glass,
68            15 => Self::Liquid,
69            17 => Self::Emissive,
70            18 => Self::Chrome,
71            _ => Self::Plain,
72        }
73    }
74}
75
76#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
77#[serde(rename_all = "snake_case")]
78pub enum Content {
79    #[default]
80    None,
81    Ink,
82    Decal,
83    Screen,
84    Photo,
85    Scroll,
86    Print,
87    Field,
88}
89
90impl Content {
91    fn id(self) -> u32 {
92        match self {
93            Self::None => 0,
94            Self::Ink => 1,
95            Self::Decal => 2,
96            Self::Screen => 3,
97            Self::Photo => 4,
98            Self::Scroll => 5,
99            Self::Print => 6,
100            Self::Field => 7,
101        }
102    }
103
104    fn from_id(id: u32) -> Self {
105        match id {
106            1 => Self::Ink,
107            2 => Self::Decal,
108            3 => Self::Screen,
109            4 => Self::Photo,
110            5 => Self::Scroll,
111            6 => Self::Print,
112            7 => Self::Field,
113            _ => Self::None,
114        }
115    }
116}
117
118#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
119#[serde(rename_all = "snake_case")]
120pub enum NormalSource {
121    #[default]
122    Flat,
123    Bump,
124    Map,
125}
126
127impl NormalSource {
128    fn id(self) -> u32 {
129        match self {
130            Self::Flat => 0,
131            Self::Bump => 1,
132            Self::Map => 2,
133        }
134    }
135
136    fn from_id(id: u32) -> Self {
137        match id {
138            1 => Self::Bump,
139            2 => Self::Map,
140            _ => Self::Flat,
141        }
142    }
143}
144
145#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
146#[serde(rename_all = "snake_case")]
147pub enum NoiseKind {
148    #[default]
149    None,
150    Fibre,
151    Crinkle,
152    PlankWood,
153    WallMottle,
154    Grime,
155    Leaf,
156    Bark,
157    Flow,
158    Value,
159    Fbm,
160    Scratch,
161    CoatWobble,
162}
163
164impl NoiseKind {
165    fn id(self) -> u32 {
166        match self {
167            Self::None => 0,
168            Self::Fibre => 1,
169            Self::Crinkle => 2,
170            Self::PlankWood => 3,
171            Self::WallMottle => 4,
172            Self::Grime => 5,
173            Self::Leaf => 7,
174            Self::Bark => 8,
175            Self::Flow => 14,
176            Self::Value => 15,
177            Self::Fbm => 16,
178            Self::Scratch => 17,
179            Self::CoatWobble => 18,
180        }
181    }
182
183    fn from_id(id: u32) -> Self {
184        match id {
185            1 => Self::Fibre,
186            2 => Self::Crinkle,
187            3 => Self::PlankWood,
188            4 => Self::WallMottle,
189            5 => Self::Grime,
190            7 => Self::Leaf,
191            8 => Self::Bark,
192            14 => Self::Flow,
193            15 => Self::Value,
194            16 => Self::Fbm,
195            17 => Self::Scratch,
196            18 => Self::CoatWobble,
197            _ => Self::None,
198        }
199    }
200}
201
202#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
203#[serde(deny_unknown_fields)]
204pub struct NoiseLayer {
205    pub kind: NoiseKind,
206    pub frequency: f32,
207    pub amplitude: f32,
208    pub seed: u32,
209    #[serde(default, skip_serializing_if = "unset")]
210    pub params: [f32; PARAMS],
211}
212
213pub const PARAMS: usize = 12;
214pub const GPU_PARAMS: usize = PARAMS + 4;
215
216pub const SCRATCH_STRETCH: f32 = 0.02;
217
218pub fn gpu_params(layer: &NoiseLayer) -> [f32; GPU_PARAMS] {
219    let p = layer.params;
220    let mut out = [0.0; GPU_PARAMS];
221    out[..PARAMS].copy_from_slice(&p);
222    match layer.kind {
223        NoiseKind::PlankWood if p[0] > 0.0 => {
224            out[PARAMS] = 1.0 / p[0];
225            out[PARAMS + 1] = 1.0 - p[7];
226        }
227        NoiseKind::Scratch => out[PARAMS] = p[0] * SCRATCH_STRETCH,
228        _ => {}
229    }
230    out
231}
232
233fn unset(params: &[f32; PARAMS]) -> bool {
234    params.iter().all(|value| *value == 0.0)
235}
236
237impl Default for NoiseLayer {
238    fn default() -> Self {
239        Self {
240            kind: NoiseKind::None,
241            frequency: 1.0,
242            amplitude: 0.0,
243            seed: 0,
244            params: [0.0; PARAMS],
245        }
246    }
247}
248
249impl NoiseLayer {
250    pub fn new(kind: NoiseKind, frequency: f32, amplitude: f32, seed: u32) -> Self {
251        Self {
252            kind,
253            frequency,
254            amplitude,
255            seed,
256            params: [0.0; PARAMS],
257        }
258    }
259
260    pub fn with_params(self, params: [f32; PARAMS]) -> Self {
261        Self { params, ..self }
262    }
263
264    pub fn active(self) -> bool {
265        self.kind != NoiseKind::None
266            && self.amplitude != 0.0
267            && (!matches!(self.kind, NoiseKind::PlankWood | NoiseKind::CoatWobble)
268                || self.params[0] > 0.0)
269    }
270}
271
272#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
273#[serde(deny_unknown_fields)]
274pub struct Normal {
275    pub source: NormalSource,
276    pub strength: f32,
277}
278
279impl Default for Normal {
280    fn default() -> Self {
281        Self {
282            source: NormalSource::Flat,
283            strength: 0.0,
284        }
285    }
286}
287
288#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
289#[serde(deny_unknown_fields)]
290pub struct Maps {
291    pub layer: f32,
292    pub tile: f32,
293    pub normal: f32,
294    pub albedo: f32,
295}
296
297impl Default for Maps {
298    fn default() -> Self {
299        Self {
300            layer: -1.0,
301            tile: 1.0,
302            normal: 1.0,
303            albedo: 1.0,
304        }
305    }
306}
307
308#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
309#[serde(deny_unknown_fields)]
310pub struct Ageing {
311    pub fade: f32,
312    pub yellow: f32,
313    pub ink: f32,
314    pub scratch: f32,
315    pub edge: f32,
316    pub dust: f32,
317    pub patina: f32,
318    pub seed: u32,
319}
320
321impl Default for Ageing {
322    fn default() -> Self {
323        Self {
324            fade: 0.0,
325            yellow: 0.0,
326            ink: 0.0,
327            scratch: 0.0,
328            edge: 0.0,
329            dust: 0.0,
330            patina: 0.0,
331            seed: 0,
332        }
333    }
334}
335
336impl Ageing {
337    pub fn inactive(self) -> bool {
338        self.fade == 0.0
339            && self.yellow == 0.0
340            && self.ink == 0.0
341            && self.scratch == 0.0
342            && self.edge == 0.0
343            && self.dust == 0.0
344            && self.patina == 0.0
345    }
346}
347
348#[derive(Clone, Copy, Debug, PartialEq)]
349pub struct At {
350    pub position: [f32; 3],
351    pub uv: [f32; 2],
352    pub normal: [f32; 3],
353    pub edge: f32,
354    pub time: f32,
355}
356
357impl Default for At {
358    fn default() -> Self {
359        Self {
360            position: [0.0; 3],
361            uv: [0.0; 2],
362            normal: [0.0, 0.0, 1.0],
363            edge: 0.0,
364            time: 0.0,
365        }
366    }
367}
368
369#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
370#[serde(default, deny_unknown_fields)]
371pub struct Material {
372    pub family: Family,
373    pub base: [f32; 3],
374    pub roughness: f32,
375    pub metalness: f32,
376    pub specular: f32,
377    pub clearcoat: f32,
378    pub clearcoat_roughness: f32,
379    pub sheen: f32,
380    pub transmission: f32,
381    pub ior: f32,
382    pub dispersion: f32,
383    pub thickness: f32,
384    pub subsurface: f32,
385    pub subsurface_tint: [f32; 3],
386    pub absorption: f32,
387    pub thin_film: f32,
388    pub thin_film_ior: f32,
389    pub thin_film_amount: f32,
390    pub emission: [f32; 3],
391    pub fresnel_power: f32,
392    pub normal: Normal,
393    pub maps: Maps,
394    pub content: Content,
395    pub content_layer: ContentLayer,
396    pub layers: [NoiseLayer; LAYER_CAP],
397    pub ageing: Ageing,
398}
399
400impl Default for Material {
401    fn default() -> Self {
402        Self {
403            family: Family::Plain,
404            base: [0.5, 0.5, 0.5],
405            roughness: 0.5,
406            metalness: 0.0,
407            specular: 0.04,
408            clearcoat: 0.0,
409            clearcoat_roughness: 0.05,
410            sheen: 0.0,
411            transmission: 0.0,
412            ior: 1.5,
413            dispersion: 0.0,
414            thickness: 0.0,
415            subsurface: 0.0,
416            subsurface_tint: [1.0, 1.0, 1.0],
417            absorption: 0.0,
418            thin_film: 0.0,
419            thin_film_ior: 1.5,
420            thin_film_amount: 0.0,
421            emission: [0.0; 3],
422            fresnel_power: 5.0,
423            normal: Normal::default(),
424            maps: Maps::default(),
425            content: Content::None,
426            content_layer: ContentLayer::default(),
427            layers: [NoiseLayer::default(); LAYER_CAP],
428            ageing: Ageing::default(),
429        }
430    }
431}
432
433impl Material {
434    pub fn push_layer(&mut self, layer: NoiseLayer) {
435        if let Some(slot) = self
436            .layers
437            .iter_mut()
438            .find(|layer| layer.kind == NoiseKind::None)
439        {
440            *slot = layer;
441        }
442    }
443
444    pub fn to_toml(&self) -> Result<String, String> {
445        let recipe = Recipe::from(*self);
446        toml::to_string(&recipe).map_err(|err| err.to_string())
447    }
448
449    pub fn from_toml(text: &str) -> Result<Self, String> {
450        let recipe: Recipe = toml::from_str(text).map_err(|err| err.to_string())?;
451        recipe.into_material()
452    }
453}
454
455#[derive(Serialize, Deserialize)]
456#[serde(default, deny_unknown_fields)]
457pub(crate) struct Recipe {
458    family: Family,
459    base: [f32; 3],
460    roughness: f32,
461    metalness: f32,
462    specular: f32,
463    clearcoat: f32,
464    clearcoat_roughness: f32,
465    sheen: f32,
466    transmission: f32,
467    ior: f32,
468    dispersion: f32,
469    thickness: f32,
470    subsurface: f32,
471    subsurface_tint: [f32; 3],
472    absorption: f32,
473    thin_film: f32,
474    thin_film_ior: f32,
475    thin_film_amount: f32,
476    emission: [f32; 3],
477    fresnel_power: f32,
478    normal: Normal,
479    maps: Maps,
480    content: Content,
481    content_layer: ContentLayer,
482    layers: Vec<NoiseLayer>,
483    ageing: Ageing,
484}
485
486impl Default for Recipe {
487    fn default() -> Self {
488        Self::from(Material::default())
489    }
490}
491
492impl From<Material> for Recipe {
493    fn from(m: Material) -> Self {
494        Self {
495            family: m.family,
496            base: m.base,
497            roughness: m.roughness,
498            metalness: m.metalness,
499            specular: m.specular,
500            clearcoat: m.clearcoat,
501            clearcoat_roughness: m.clearcoat_roughness,
502            sheen: m.sheen,
503            transmission: m.transmission,
504            ior: m.ior,
505            dispersion: m.dispersion,
506            thickness: m.thickness,
507            subsurface: m.subsurface,
508            subsurface_tint: m.subsurface_tint,
509            absorption: m.absorption,
510            thin_film: m.thin_film,
511            thin_film_ior: m.thin_film_ior,
512            thin_film_amount: m.thin_film_amount,
513            emission: m.emission,
514            fresnel_power: m.fresnel_power,
515            normal: m.normal,
516            maps: m.maps,
517            content: m.content,
518            content_layer: m.content_layer,
519            layers: m
520                .layers
521                .into_iter()
522                .filter(|layer| layer.kind != NoiseKind::None)
523                .collect(),
524            ageing: m.ageing,
525        }
526    }
527}
528
529impl Recipe {
530    pub(crate) fn into_material(self) -> Result<Material, String> {
531        if self.layers.len() > LAYER_CAP {
532            return Err(format!(
533                "a material takes at most {LAYER_CAP} noise layers, got {}",
534                self.layers.len()
535            ));
536        }
537        let mut layers = [NoiseLayer::default(); LAYER_CAP];
538        for (slot, layer) in layers.iter_mut().zip(self.layers) {
539            *slot = layer;
540        }
541        Ok(Material {
542            family: self.family,
543            base: self.base,
544            roughness: self.roughness,
545            metalness: self.metalness,
546            specular: self.specular,
547            clearcoat: self.clearcoat,
548            clearcoat_roughness: self.clearcoat_roughness,
549            sheen: self.sheen,
550            transmission: self.transmission,
551            ior: self.ior,
552            dispersion: self.dispersion,
553            thickness: self.thickness,
554            subsurface: self.subsurface,
555            subsurface_tint: self.subsurface_tint,
556            absorption: self.absorption,
557            thin_film: self.thin_film,
558            thin_film_ior: self.thin_film_ior,
559            thin_film_amount: self.thin_film_amount,
560            emission: self.emission,
561            fresnel_power: self.fresnel_power,
562            normal: self.normal,
563            maps: self.maps,
564            content: self.content,
565            content_layer: self.content_layer,
566            layers,
567            ageing: self.ageing,
568        })
569    }
570}
571
572fn pack_id(id: u32) -> f32 {
573    id as f32
574}
575
576fn unpack_id(v: f32) -> u32 {
577    v.round().max(0.0) as u32
578}
579
580#[repr(C, align(16))]
581#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
582pub struct PackedMaterial {
583    pub base_roughness: [f32; 4],
584    pub metal_spec_coat: [f32; 4],
585    pub sheen_trans_ior_disp: [f32; 4],
586    pub thick_sub_film: [f32; 4],
587    pub tint_absorption: [f32; 4],
588    pub emission_film: [f32; 4],
589    pub normal_maps: [f32; 4],
590    pub maps_content: [f32; 4],
591    pub age_a: [f32; 4],
592    pub age_b: [f32; 4],
593    pub layer0: [f32; 4],
594    pub layer1: [f32; 4],
595    pub layer2: [f32; 4],
596    pub layer3: [f32; 4],
597    pub tail: [f32; 4],
598    pub params: [[f32; 4]; PARAM_ROWS],
599}
600
601pub const PARAM_ROWS: usize = LAYER_CAP * GPU_PARAMS / 4;
602
603impl PackedMaterial {
604    pub const SIZE: usize = 240 + PARAM_ROWS * 16;
605    pub const ALIGN: usize = 16;
606
607    pub fn pack(m: &Material) -> Self {
608        let layer = |i: usize| {
609            let layer = m.layers[i];
610            [
611                pack_id(layer.kind.id()),
612                layer.frequency,
613                layer.amplitude,
614                f32::from_bits(layer.seed),
615            ]
616        };
617        Self {
618            base_roughness: [m.base[0], m.base[1], m.base[2], m.roughness],
619            metal_spec_coat: [m.metalness, m.specular, m.clearcoat, m.clearcoat_roughness],
620            sheen_trans_ior_disp: [m.sheen, m.transmission, m.ior, m.dispersion],
621            thick_sub_film: [m.thickness, m.subsurface, m.thin_film, m.thin_film_ior],
622            tint_absorption: [
623                m.subsurface_tint[0],
624                m.subsurface_tint[1],
625                m.subsurface_tint[2],
626                m.absorption,
627            ],
628            emission_film: [
629                m.emission[0],
630                m.emission[1],
631                m.emission[2],
632                m.thin_film_amount,
633            ],
634            normal_maps: [
635                pack_id(m.normal.source.id()),
636                m.normal.strength,
637                m.maps.layer,
638                m.maps.tile,
639            ],
640            maps_content: [
641                m.maps.normal,
642                m.maps.albedo,
643                pack_id(m.content.id()),
644                pack_id(m.family.id()),
645            ],
646            age_a: [
647                m.ageing.fade,
648                m.ageing.yellow,
649                m.ageing.ink,
650                m.ageing.scratch,
651            ],
652            age_b: [
653                m.ageing.edge,
654                m.ageing.dust,
655                m.ageing.patina,
656                m.fresnel_power,
657            ],
658            layer0: layer(0),
659            layer1: layer(1),
660            layer2: layer(2),
661            layer3: layer(3),
662            tail: [f32::from_bits(m.ageing.seed), 0.0, 0.0, 0.0],
663            params: std::array::from_fn(|row| {
664                let params = gpu_params(&m.layers[row / (GPU_PARAMS / 4)]);
665                let at = row % (GPU_PARAMS / 4) * 4;
666                [params[at], params[at + 1], params[at + 2], params[at + 3]]
667            }),
668        }
669    }
670
671    pub fn unpack(self) -> Material {
672        let layer = |row: [f32; 4], index: usize| {
673            let kind = NoiseKind::from_id(unpack_id(row[0]));
674            let params: [f32; PARAMS] =
675                std::array::from_fn(|k| self.params[index * GPU_PARAMS / 4 + k / 4][k % 4]);
676            NoiseLayer {
677                kind,
678                frequency: row[1],
679                amplitude: row[2],
680                seed: row[3].to_bits(),
681                params,
682            }
683        };
684        Material {
685            family: Family::from_id(unpack_id(self.maps_content[3])),
686            base: [
687                self.base_roughness[0],
688                self.base_roughness[1],
689                self.base_roughness[2],
690            ],
691            roughness: self.base_roughness[3],
692            metalness: self.metal_spec_coat[0],
693            specular: self.metal_spec_coat[1],
694            clearcoat: self.metal_spec_coat[2],
695            clearcoat_roughness: self.metal_spec_coat[3],
696            sheen: self.sheen_trans_ior_disp[0],
697            transmission: self.sheen_trans_ior_disp[1],
698            ior: self.sheen_trans_ior_disp[2],
699            dispersion: self.sheen_trans_ior_disp[3],
700            thickness: self.thick_sub_film[0],
701            subsurface: self.thick_sub_film[1],
702            thin_film: self.thick_sub_film[2],
703            thin_film_ior: self.thick_sub_film[3],
704            subsurface_tint: [
705                self.tint_absorption[0],
706                self.tint_absorption[1],
707                self.tint_absorption[2],
708            ],
709            absorption: self.tint_absorption[3],
710            emission: [
711                self.emission_film[0],
712                self.emission_film[1],
713                self.emission_film[2],
714            ],
715            thin_film_amount: self.emission_film[3],
716            fresnel_power: self.age_b[3],
717            normal: Normal {
718                source: NormalSource::from_id(unpack_id(self.normal_maps[0])),
719                strength: self.normal_maps[1],
720            },
721            maps: Maps {
722                layer: self.normal_maps[2],
723                tile: self.normal_maps[3],
724                normal: self.maps_content[0],
725                albedo: self.maps_content[1],
726            },
727            content: Content::from_id(unpack_id(self.maps_content[2])),
728            content_layer: ContentLayer::default(),
729            layers: [
730                layer(self.layer0, 0),
731                layer(self.layer1, 1),
732                layer(self.layer2, 2),
733                layer(self.layer3, 3),
734            ],
735            ageing: Ageing {
736                fade: self.age_a[0],
737                yellow: self.age_a[1],
738                ink: self.age_a[2],
739                scratch: self.age_a[3],
740                edge: self.age_b[0],
741                dust: self.age_b[1],
742                patina: self.age_b[2],
743                seed: self.tail[0].to_bits(),
744            },
745        }
746    }
747}