1use crate::{Component, Entity, JsonError, Semantic, World, pbr::TextureRef, pbr::UvTransform};
16use serde::Serialize;
17use serde_json::Value;
18
19mod expression;
20pub use expression::{Expression, ExpressionBuilder, TERM_LIMIT, Term, TermId, lerp, saturate};
21
22#[derive(Clone, Debug, PartialEq, Serialize)]
25pub struct Layer {
26 pub material: Entity,
27 pub texcoord: Option<u8>,
29 pub transform: Option<UvTransform>,
31 #[serde(skip_serializing_if = "Option::is_none")]
36 pub color: Option<[Expression<ColorLane>; 3]>,
37}
38
39impl Layer {
40 pub fn new(material: Entity, texcoord: Option<u8>, transform: Option<UvTransform>) -> Layer {
42 Layer {
43 material,
44 texcoord,
45 transform,
46 color: None,
47 }
48 }
49}
50
51#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
55#[serde(transparent)]
56pub struct ColorLane(pub u8);
57
58#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
59pub enum WeightMapping {
60 Identity,
62 RgbProduct4,
65}
66impl WeightMapping {
67 pub fn evaluate(self, [r, g, b, a]: [f32; 4]) -> [f32; 4] {
70 match self {
71 Self::Identity => [r, g, b, a],
72 Self::RgbProduct4 => [
73 (1. - r) * (1. - g) * (1. - b),
74 (1. - r) * (1. - g) * b,
75 (1. - r) * g * (1. - b),
76 (1. - r) * g * b,
77 ],
78 }
79 }
80}
81
82impl WeightMapping {
83 pub fn inputs(self, slots: usize) -> usize {
85 match self {
86 Self::Identity => slots,
87 Self::RgbProduct4 => 3,
88 }
89 }
90}
91
92#[derive(Clone, Debug, PartialEq, Serialize)]
93pub struct LayerWeights {
94 pub attribute: Semantic,
99 pub mapping: WeightMapping,
100 #[serde(skip_serializing_if = "Option::is_none")]
102 pub channels: Option<Vec<Channel>>,
103 pub composition: Composition,
104 #[serde(skip_serializing_if = "Vec::is_empty")]
107 pub heights: Vec<Source>,
108 #[serde(skip_serializing_if = "std::ops::Not::not")]
111 pub saturate: bool,
112}
113
114impl LayerWeights {
115 pub fn new(attribute: Semantic, mapping: WeightMapping) -> Self {
117 LayerWeights {
118 attribute,
119 mapping,
120 channels: None,
121 composition: Composition::Sum,
122 heights: Vec::new(),
123 saturate: false,
124 }
125 }
126 pub fn per_pixel_weights(&self) -> bool {
129 self.channels.is_some() || self.composition != Composition::Sum
130 }
131 pub fn per_pixel(&self) -> bool {
135 self.per_pixel_weights() || self.saturate
136 }
137}
138
139#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
141#[serde(rename_all = "snake_case")]
142pub enum Composition {
143 Sum,
145 Over,
147 Height { contrast: f32 },
151}
152
153pub fn height_blend(weights: [f32; 4], heights: [f32; 4], slots: usize, contrast: f32) -> [f32; 4] {
156 let slots = slots.min(4);
157 let p: Vec<f32> = (0..slots).map(|i| weights[i] * heights[i]).collect();
158 let m = p.iter().copied().fold(f32::NEG_INFINITY, f32::max);
159 let u: Vec<f32> = p.iter().map(|&x| (x - (m - (1.0 - contrast))).max(0.0)).collect();
160 let sum: f32 = u.iter().sum();
161 let mut out = [0.0; 4];
162 if sum > 0.0 {
163 for (o, x) in out.iter_mut().zip(&u) {
164 *o = x / sum;
165 }
166 }
167 out
168}
169
170#[derive(Clone, Debug, PartialEq, Serialize)]
172#[serde(rename_all = "snake_case")]
173pub enum Source {
174 Vertex { semantic: Semantic, channel: u8 },
176 Mask {
178 texture: TextureRef,
179 channels: [f32; 4],
180 bias: f32,
181 },
182}
183
184#[derive(Clone, Debug, PartialEq, Serialize)]
187#[serde(rename_all = "snake_case")]
188pub enum Channel {
189 Source(Source),
190 Lerp {
192 a: Source,
193 b: Source,
194 t: Source,
195 },
196 Threshold {
198 v: Source,
199 m: Source,
200 k: [f32; 4],
201 },
202 Product {
204 sources: Vec<Source>,
205 },
206 Power {
208 v: Source,
209 exponent: u8,
210 },
211 Expression(Expression),
213}
214
215pub const PRODUCT_LIMIT: usize = 4;
217
218pub const POWER_LIMIT: u8 = 16;
220
221pub fn power(v: f32, exponent: u8) -> f32 {
225 if exponent.is_power_of_two() {
226 let mut x = v;
227 for _ in 0..exponent.trailing_zeros() {
228 x *= x;
229 }
230 x
231 } else {
232 (1..exponent).fold(v, |x, _| x * v)
233 }
234}
235
236impl Channel {
237 pub fn sources(&self) -> Vec<&Source> {
239 match self {
240 Channel::Source(s) => vec![s],
241 Channel::Lerp { a, b, t } => vec![a, b, t],
242 Channel::Threshold { v, m, .. } => vec![v, m],
243 Channel::Power { v, .. } => vec![v],
244 Channel::Product { sources } => sources.iter().collect(),
245 Channel::Expression(e) => e.sources.iter().collect(),
246 }
247 }
248 pub fn evaluate(&self, values: &[f32]) -> f32 {
250 match self {
251 Channel::Source(_) => values[0],
252 Channel::Lerp { .. } => lerp(values[0], values[1], values[2]),
253 Channel::Threshold { k, .. } => {
254 let (v, m) = (values[0], values[1]);
255 saturate(k[0] + k[1] * v + k[2] * m + k[3] * v * m)
256 }
257 Channel::Power { exponent, .. } => power(values[0], *exponent),
258 Channel::Product { sources } => values[..sources.len()].iter().fold(1., |p, v| p * v),
259 Channel::Expression(e) => e.evaluate(values),
260 }
261 }
262
263 pub fn check(&self) -> Result<(), String> {
266 match self {
267 Channel::Product { sources } if !(2..=PRODUCT_LIMIT).contains(&sources.len()) => Err(format!(
268 "product has {} sources, requires 2 to {PRODUCT_LIMIT}",
269 sources.len()
270 )),
271 Channel::Threshold { k, .. } if !k.iter().all(|x| x.is_finite()) => {
272 Err(format!("threshold {k:?} is not finite"))
273 }
274 Channel::Power { exponent, .. } if !(1..=POWER_LIMIT).contains(exponent) => {
275 Err(format!("power exponent {exponent} is not 1 to {POWER_LIMIT}"))
276 }
277 Channel::Expression(e) => {
278 e.check()?;
279 match Channel::from_expression(e)? {
280 Channel::Expression(_) => Ok(()),
281 named @ (Channel::Source(_)
282 | Channel::Lerp { .. }
283 | Channel::Threshold { .. }
284 | Channel::Product { .. }
285 | Channel::Power { .. }) => Err(format!("expression is a named form: {named:?}")),
286 }
287 }
288 Channel::Source(_)
289 | Channel::Lerp { .. }
290 | Channel::Threshold { .. }
291 | Channel::Product { .. }
292 | Channel::Power { .. } => Ok(()),
293 }
294 }
295}
296
297pub(crate) fn check_source(world: &World, what: &str, s: &Source) -> Result<(), String> {
299 match s {
300 Source::Vertex { channel, .. } if *channel > 3 => {
301 Err(format!("{what}: vertex channel {channel} past 3"))
302 }
303 Source::Vertex { .. } => Ok(()),
304 Source::Mask {
305 texture,
306 channels,
307 bias,
308 } => {
309 if !world.is_alive(texture.texture) {
310 return Err(format!("{what}: mask texture {:?} is not alive", texture.texture));
311 }
312 if !channels.iter().chain([bias]).all(|x| x.is_finite()) {
313 return Err(format!("{what}: mask weights {channels:?} + {bias} not finite"));
314 }
315 Ok(())
316 }
317 }
318}
319
320pub fn mask_value(channels: [f32; 4], bias: f32, rgba: [f32; 4]) -> f32 {
322 (0..4).map(|i| channels[i] * rgba[i]).sum::<f32>() + bias
323}
324
325pub fn over_as_sum(weights: [f32; 4], slots: usize) -> [f32; 4] {
328 let slots = slots.min(weights.len());
329 let mut out = [0.0; 4];
330 for i in 0..slots {
331 let own = if i == 0 { 1.0 } else { weights[i] };
332 out[i] = own * (i + 1..slots).map(|j| 1.0 - weights[j]).product::<f32>();
333 }
334 out
335}
336
337#[derive(Clone, Debug, PartialEq, Serialize)]
348pub struct LayeredMaterial {
349 pub layers: Vec<Option<Layer>>,
350 pub weights: LayerWeights,
351}
352impl LayeredMaterial {
353 pub fn per_pixel(&self) -> bool {
357 self.vertex_weight_blocker().is_some()
358 }
359 pub fn vertex_weight_blocker(&self) -> Option<&'static str> {
364 if self.weights.channels.is_some() {
365 Some("per-pixel weight channels")
366 } else if self.weights.composition != Composition::Sum {
367 Some("the over or height composition")
368 } else if self.weights.saturate {
369 Some("a saturated blend")
370 } else if self.layers.iter().flatten().any(|l| l.color.is_some()) {
371 Some("a layer's colour expression")
372 } else {
373 None
374 }
375 }
376 pub fn reads(&self, semantic: Semantic) -> bool {
379 let reads = |s: &Source| matches!(s, Source::Vertex { semantic: v, .. } if *v == semantic);
380 let weights = match &self.weights.channels {
381 None => self.weights.attribute == semantic,
382 Some(channels) => channels.iter().flat_map(Channel::sources).any(reads),
383 };
384 weights || self.weights.heights.iter().any(reads)
385 }
386 pub fn mask_textures(&self) -> impl Iterator<Item = &TextureRef> {
388 self.weights
389 .channels
390 .iter()
391 .flatten()
392 .flat_map(Channel::sources)
393 .chain(&self.weights.heights)
394 .filter_map(|s| match s {
395 Source::Mask { texture, .. } => Some(texture),
396 Source::Vertex { .. } => None,
397 })
398 }
399}
400
401impl Component for LayeredMaterial {
402 const NAME: &'static str = "core.layered";
403 const GEOMETRY: crate::Geometry = crate::Geometry::None;
404 const VERSION: u32 = 7;
408 fn references(&self, out: &mut Vec<Entity>) {
409 out.extend(self.layers.iter().flatten().map(|layer| layer.material));
410 out.extend(self.mask_textures().map(|t| t.texture));
411 }
412 fn to_json(&self) -> Result<Option<Value>, JsonError> {
413 crate::json::to_value(self).map(Some)
414 }
415 fn verify(&self, entity: Entity, world: &World) -> Result<(), String> {
416 if !(1..=4).contains(&self.layers.len()) {
417 return Err("layered material requires one to four slots".into());
418 }
419 if self.weights.mapping == WeightMapping::RgbProduct4 && self.layers.len() != 4 {
420 return Err("rgb_product_4 requires four slots".into());
421 }
422 if let Some(channels) = &self.weights.channels {
423 let want = self.weights.mapping.inputs(self.layers.len());
424 if channels.len() != want {
425 return Err(format!("{} channels for a mapping of {want}", channels.len()));
426 }
427 for (i, c) in channels.iter().enumerate() {
428 c.check().map_err(|e| format!("channel {i}: {e}"))?;
429 for s in c.sources() {
430 check_source(world, &format!("channel {i}"), s)?;
431 }
432 }
433 }
434 match self.weights.composition {
435 Composition::Height { contrast } => {
436 if !contrast.is_finite() {
437 return Err(format!("height composition: contrast {contrast} is not finite"));
438 }
439 if self.weights.heights.len() != self.layers.len() {
440 return Err(format!(
441 "height composition: {} heights for {} slots",
442 self.weights.heights.len(),
443 self.layers.len()
444 ));
445 }
446 for (i, s) in self.weights.heights.iter().enumerate() {
447 check_source(world, &format!("height {i}"), s)?;
448 }
449 }
450 Composition::Sum | Composition::Over if !self.weights.heights.is_empty() => {
451 return Err("heights without the height composition".into());
452 }
453 Composition::Sum | Composition::Over => {}
454 }
455 for (i, layer) in self.layers.iter().enumerate() {
456 let Some(layer) = layer else { continue };
458 if layer.material == entity || world.get::<Self>(layer.material).is_some() {
459 return Err(format!("layer {i} references a layered material"));
460 }
461 if let Some(t) = layer.transform
462 && !t
463 .offset
464 .into_iter()
465 .chain(t.scale)
466 .chain([t.rotation])
467 .all(f32::is_finite)
468 {
469 return Err(format!("layer {i} has a nonfinite UV transform"));
470 }
471 if let Some(color) = &layer.color {
472 check_color(world, layer.material, color).map_err(|e| format!("layer {i} color: {e}"))?;
473 }
474 }
475 Ok(())
476 }
477}
478
479fn check_color(world: &World, material: Entity, color: &[Expression<ColorLane>; 3]) -> Result<(), String> {
482 for (lane, e) in color.iter().enumerate() {
483 e.check().map_err(|why| format!("lane {lane}: {why}"))?;
484 if let Some(ColorLane(c)) = e.sources.iter().find(|ColorLane(c)| *c > 3) {
485 return Err(format!("lane {lane} reads colour lane {c}, past 3"));
486 }
487 }
488 let own = |lane: u8, e: &Expression<ColorLane>| {
490 e.sources.as_slice() == [ColorLane(lane)] && e.terms.as_slice() == [Term::Source(0)]
491 };
492 let scaled = |lane: u8, e: &Expression<ColorLane>| {
493 e.sources.as_slice() == [ColorLane(lane)]
494 && matches!(
495 e.terms.as_slice(),
496 [Term::Constant(_), Term::Source(0), Term::Multiply(_)]
497 )
498 };
499 let lanes = || (0u8..).zip(color);
500 if lanes().all(|(lane, e)| own(lane, e)) {
501 return Err("the material's own colour, which a layer states with no color".into());
502 }
503 let unit_factor = world
504 .get::<crate::pbr::PbrMaterial>(material)
505 .is_some_and(|m| m.base_color[..3] == [1.0; 3]);
506 if unit_factor && lanes().all(|(lane, e)| own(lane, e) || scaled(lane, e)) {
507 return Err("each lane its own times a constant: the material's base colour factor".into());
508 }
509 Ok(())
510}