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}