Skip to main content

pfx_materials/
content.rs

1use serde::{Deserialize, Serialize};
2
3use crate::brdf::Shaded;
4use crate::material::Content;
5
6#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
7#[serde(default, deny_unknown_fields)]
8pub struct ContentLook {
9    pub ink_roughness: f32,
10    pub ink_emboss_coated: f32,
11    pub ink_emboss_bare: f32,
12    pub photo_gain: f32,
13    pub screen_gain: f32,
14    pub photo_window: [f32; 4],
15}
16
17impl Default for ContentLook {
18    fn default() -> Self {
19        Self {
20            ink_roughness: -1.0,
21            ink_emboss_coated: 0.0,
22            ink_emboss_bare: 0.0,
23            photo_gain: 1.0,
24            screen_gain: 1.0,
25            photo_window: [0.0, 0.0, 1.0, 1.0],
26        }
27    }
28}
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
31#[serde(rename_all = "snake_case")]
32pub enum Blend {
33    #[default]
34    Over,
35    Multiply,
36    Emit,
37}
38
39impl Blend {
40    pub fn id(self) -> u32 {
41        match self {
42            Self::Over => 0,
43            Self::Multiply => 1,
44            Self::Emit => 2,
45        }
46    }
47}
48
49#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
50#[serde(default, deny_unknown_fields)]
51pub struct ContentLayer {
52    pub slot: i32,
53    pub blend: Blend,
54    pub ink_roughness: f32,
55    pub emboss: f32,
56    pub strength: f32,
57}
58
59impl Default for ContentLayer {
60    fn default() -> Self {
61        Self {
62            slot: -1,
63            blend: Blend::Over,
64            ink_roughness: -1.0,
65            emboss: 0.0,
66            strength: 1.0,
67        }
68    }
69}
70
71impl ContentLayer {
72    pub fn active(&self) -> bool {
73        self.slot >= 0
74    }
75
76    pub fn from_kind(content: Content, clearcoat: f32, slot: i32, look: &ContentLook) -> Self {
77        let layer = Self {
78            slot,
79            ..Self::default()
80        };
81        match content {
82            Content::None => Self::default(),
83            Content::Ink => Self {
84                ink_roughness: look.ink_roughness,
85                emboss: if clearcoat > 0.0 {
86                    look.ink_emboss_coated
87                } else {
88                    look.ink_emboss_bare
89                },
90                ..layer
91            },
92            Content::Decal | Content::Scroll | Content::Print | Content::Field => layer,
93            Content::Photo => Self {
94                blend: Blend::Multiply,
95                strength: look.photo_gain,
96                ..layer
97            },
98            Content::Screen => Self {
99                blend: Blend::Emit,
100                strength: look.screen_gain,
101                ..layer
102            },
103        }
104    }
105
106    pub fn composite(&self, surface: &Shaded, texel: [f32; 4]) -> Shaded {
107        let mut out = *surface;
108        let a = texel[3].clamp(0.0, 1.0);
109        let rgb = [texel[0], texel[1], texel[2]];
110        match self.blend {
111            Blend::Over => {
112                out.base = std::array::from_fn(|c| surface.base[c] * (1.0 - a) + rgb[c]);
113                out.emission = surface.emission.map(|value| value * (1.0 - a));
114                if self.ink_roughness >= 0.0 {
115                    let alpha = surface.roughness * surface.roughness;
116                    let ink = self.ink_roughness * self.ink_roughness;
117                    out.roughness = (alpha + (ink - alpha) * a).sqrt();
118                }
119                out.subsurface = surface.subsurface * (1.0 - a).powi(4);
120            }
121            Blend::Multiply => {
122                out.base =
123                    std::array::from_fn(|c| surface.base[c] * (1.0 - a + rgb[c] * self.strength));
124            }
125            Blend::Emit => {
126                out.emission =
127                    std::array::from_fn(|c| surface.emission[c] + rgb[c] * self.strength);
128            }
129        }
130        out
131    }
132}
133
134pub fn content_uv(
135    uv: [f32; 2],
136    offset: [f32; 2],
137    scale: [f32; 2],
138    crop: [f32; 4],
139) -> Option<[f32; 2]> {
140    let at = [uv[0] * scale[0] + offset[0], uv[1] * scale[1] + offset[1]];
141    (at[0] >= crop[0] && at[1] >= crop[1] && at[0] <= crop[2] && at[1] <= crop[3]).then_some(at)
142}
143
144pub fn emboss(
145    normal: [f32; 3],
146    tangent: [f32; 3],
147    bitangent: [f32; 3],
148    gradient: [f32; 2],
149    strength: f32,
150) -> [f32; 3] {
151    let tilt: [f32; 3] =
152        std::array::from_fn(|k| (tangent[k] * gradient[0] + bitangent[k] * gradient[1]) * strength);
153    let along = tilt.iter().zip(normal).map(|(t, n)| t * n).sum::<f32>();
154    let bent: [f32; 3] = std::array::from_fn(|k| normal[k] - (tilt[k] - along * normal[k]));
155    let length = bent.iter().map(|v| v * v).sum::<f32>().sqrt().max(1e-8);
156    bent.map(|v| v / length)
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use crate::material::Material;
163
164    const LOOK: ContentLook = ContentLook {
165        ink_roughness: 0.5,
166        ink_emboss_coated: 0.3,
167        ink_emboss_bare: -0.2,
168        photo_gain: 1.25,
169        screen_gain: 2.0,
170        photo_window: [0.25, 0.0, 0.5, 1.0],
171    };
172
173    fn surface() -> Shaded {
174        Shaded::from_material(&Material {
175            base: [0.8, 0.6, 0.4],
176            roughness: 0.9,
177            subsurface: 0.5,
178            emission: [0.2, 0.1, 0.0],
179            ..Material::default()
180        })
181    }
182
183    #[test]
184    fn over_is_premultiplied_ink_with_the_looks_roughness() {
185        let layer = ContentLayer::from_kind(Content::Ink, 1.0, 0, &LOOK);
186        let base = surface();
187        let ink = [0.05, 0.02, 0.01, 0.75];
188        let out = layer.composite(&base, ink);
189        for c in 0..3 {
190            let over = base.base[c] * (1.0 - ink[3]) + ink[c];
191            assert!((out.base[c] - over).abs() < 1e-6);
192            assert!((out.emission[c] - base.emission[c] * 0.25).abs() < 1e-6);
193        }
194        let alpha = 0.9f32 * 0.9;
195        let mixed = alpha + (0.25 - alpha) * 0.75;
196        assert!((out.roughness * out.roughness - mixed).abs() < 1e-5);
197        assert!((out.subsurface - 0.5 * 0.25f32.powi(4)).abs() < 1e-7);
198        let clear = layer.composite(&base, [0.0; 4]);
199        assert_eq!(clear.base, base.base);
200        assert!((clear.roughness - base.roughness).abs() < 1e-6);
201        let decal = ContentLayer::from_kind(Content::Decal, 1.0, 1, &LOOK).composite(&base, ink);
202        assert_eq!(decal.roughness, base.roughness);
203        let neutral = ContentLayer::from_kind(Content::Ink, 1.0, 0, &ContentLook::default());
204        assert_eq!(neutral.composite(&base, ink).roughness, base.roughness);
205        assert_eq!(neutral.emboss, 0.0);
206    }
207
208    #[test]
209    fn multiply_and_emit_follow_the_photo_and_screen_gains() {
210        let base = surface();
211        let frame = [0.5, 0.25, 0.125, 1.0];
212        let photo = ContentLayer::from_kind(Content::Photo, 1.0, 3, &LOOK).composite(&base, frame);
213        for ((out, before), value) in photo.base.iter().zip(base.base).zip(frame) {
214            assert!((out - before * value * LOOK.photo_gain).abs() < 1e-6);
215        }
216        assert_eq!(photo.emission, base.emission);
217        let screen =
218            ContentLayer::from_kind(Content::Screen, 1.0, 3, &LOOK).composite(&base, frame);
219        for ((out, before), value) in screen.emission.iter().zip(base.emission).zip(frame) {
220            assert!((out - (before + value * LOOK.screen_gain)).abs() < 1e-6);
221        }
222        assert_eq!(screen.base, base.base);
223        let dark =
224            ContentLayer::from_kind(Content::Screen, 1.0, 3, &LOOK).composite(&base, [0.0; 4]);
225        assert_eq!(dark.emission, base.emission);
226        let plain = ContentLayer::from_kind(Content::Photo, 1.0, 3, &ContentLook::default());
227        assert_eq!(plain.strength, 1.0);
228    }
229
230    #[test]
231    fn ink_rises_out_of_the_surface() {
232        let n = [0.0, 0.0, 1.0];
233        let t = [1.0, 0.0, 0.0];
234        let b = [0.0, 1.0, 0.0];
235        let rising = emboss(n, t, b, [0.5, 0.0], LOOK.ink_emboss_coated);
236        assert!(rising[0] < 0.0 && rising[1].abs() < 1e-7 && rising[2] > 0.9);
237        let pressed = emboss(n, t, b, [0.5, 0.0], LOOK.ink_emboss_bare);
238        assert!(pressed[0] > 0.0);
239        let down = emboss(n, t, b, [0.0, 0.5], LOOK.ink_emboss_coated);
240        assert!(down[1] < 0.0 && down[0].abs() < 1e-7);
241        assert_eq!(emboss(n, t, b, [0.0, 0.0], 1.0), n);
242        let length = rising.iter().map(|v| v * v).sum::<f32>();
243        assert!((length - 1.0).abs() < 1e-5);
244    }
245
246    #[test]
247    fn uv_offset_and_crop() {
248        let [x, y, w, h] = LOOK.photo_window;
249        let centre = content_uv([0.5, 0.5], [x, y], [w, h], [0.0, 0.0, 1.0, 1.0]).unwrap();
250        assert!((centre[0] - 0.5).abs() < 1e-6 && (centre[1] - 0.5).abs() < 1e-6);
251        let left = content_uv([0.0, 0.2], [x, y], [w, h], [0.0, 0.0, 1.0, 1.0]).unwrap();
252        assert!((left[0] - 0.25).abs() < 1e-6);
253        let tape = [0.0, 0.0, 1.0, 2000.0 / 2048.0];
254        let scrolled = content_uv([0.4, 0.5], [0.0, 0.25], [1.0, 1.0], tape).unwrap();
255        assert!((scrolled[1] - 0.75).abs() < 1e-6);
256        assert!(content_uv([0.4, 0.8], [0.0, 0.25], [1.0, 1.0], tape).is_none());
257        assert!(content_uv([-0.01, 0.5], [0.0; 2], [1.0; 2], [0.0, 0.0, 1.0, 1.0]).is_none());
258    }
259
260    #[test]
261    fn layers_by_content_kind() {
262        assert!(!ContentLayer::from_kind(Content::None, 0.0, 2, &LOOK).active());
263        let bare = ContentLayer::from_kind(Content::Ink, 0.0, 0, &LOOK);
264        assert_eq!(bare.emboss, LOOK.ink_emboss_bare);
265        assert_eq!(bare.ink_roughness, LOOK.ink_roughness);
266        assert_eq!(
267            ContentLayer::from_kind(Content::Ink, 0.4, 0, &LOOK).emboss,
268            LOOK.ink_emboss_coated
269        );
270        assert_eq!(
271            ContentLayer::from_kind(Content::Photo, 1.0, 3, &LOOK).blend,
272            Blend::Multiply
273        );
274        assert_eq!(
275            ContentLayer::from_kind(Content::Screen, 1.0, 3, &LOOK).blend,
276            Blend::Emit
277        );
278        for kind in [
279            Content::Decal,
280            Content::Scroll,
281            Content::Print,
282            Content::Field,
283        ] {
284            assert_eq!(
285                ContentLayer::from_kind(kind, 0.0, 1, &LOOK).blend,
286                Blend::Over
287            );
288        }
289        assert_eq!(ContentLayer::default().slot, -1);
290    }
291
292    #[test]
293    fn looks_read_from_toml() {
294        let look: ContentLook = toml::from_str("screen_gain = 3.0").unwrap();
295        assert_eq!(look.screen_gain, 3.0);
296        assert_eq!(look.photo_gain, 1.0);
297        assert!(toml::from_str::<ContentLook>("ink_gain = 1.0").is_err());
298    }
299}