Skip to main content

primase_core/
layered.rs

1//! A material blend evaluated per pixel from weight channels.
2//!
3//! Each channel is an interpolated vertex channel, or, with [`LayerWeights::channels`], a per-pixel expression of
4//! vertex channels and mask textures ([`Channel`]). The mapping turns the channels into slot weights, and the
5//! composition combines the layers: [`Composition::Sum`] is `sum(weight[i] * layer[i])` with the weights not
6//! normalized (unused weight and null layers contribute black); [`Composition::Over`] starts from slot 0 and lerps
7//! each later slot over it by its weight, in slot order, which equals Sum with weight `w_i * prod_{j>i}(1 - w_j)` per
8//! pixel; [`Composition::Height`] blends by each layer's height ([`LayerWeights::heights`], [`height_blend`]): the
9//! layers whose weighted height is near the highest show, the rest fade out within the contrast. Blend normals with the same weights and then normalize the resulting direction. An all-zero normal sum has
10//! no direction; a consumer must state its fallback rather than inventing a source normal.
11//!
12//! A layer's colour is the one its material gives, or, with [`Layer::color`], an [`Expression`] of that colour's lanes
13//! per lane (a colour moved part of the way to a constant, or scaled past one). With [`LayerWeights::saturate`] the
14//! blended base colour is [`saturate`]d per lane.
15use 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/// One ordinary material sampled with this layer's UV set and transform.
23/// Its colour, normal, mask, tint and alpha participate in the blend.
24#[derive(Clone, Debug, PartialEq, Serialize)]
25pub struct Layer {
26    pub material: Entity,
27    /// `None` samples TexCoord(0); retained separately from an explicit zero.
28    pub texcoord: Option<u8>,
29    /// Applied to the selected UVs before sampling every layer texture.
30    pub transform: Option<UvTransform>,
31    /// The colour the layer blends: red, green and blue, each an [`Expression`] of the lanes of the colour its material
32    /// gives ([`ColorLane`]), under the expressions' numeric rules; `None` blends that colour as it is. Its alpha is the
33    /// material's. A colour that is the material's own, or whose every lane is its own lane times a constant where the
34    /// material's base colour factor is one (the material's factor, which says it), is stated without it.
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub color: Option<[Expression<ColorLane>; 3]>,
37}
38
39impl Layer {
40    /// The layer of `material`, sampled with `texcoord` and `transform`, its colour the material's.
41    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/// A lane of the colour a layer's material gives at the pixel (`pbr.material`'s base colour texture times its factor,
52/// times the vertex colour where the material takes it): 0 to 2 red, green and blue, 3 alpha. The source a layer's
53/// [`Layer::color`] reads.
54#[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    /// The first N interpolated channels weight the N slots directly.
61    Identity,
62    /// Four slots: (1-r)(1-g)(1-b), (1-r)(1-g)b, (1-r)g(1-b), (1-r)gb.
63    /// Their sum is 1-r, not one: red darkens the blend.
64    RgbProduct4,
65}
66impl WeightMapping {
67    /// Map already-interpolated channels without clamping or normalization.
68    /// This is the material's pixel equation, not a strongest-layer approximation.
69    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    /// The channels it maps for `slots` slots: one per slot, or three for [`WeightMapping::RgbProduct4`].
84    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    /// A VEC3/VEC4 float or normalized unsigned vertex channel stream.
95    /// VEC3 supplies an implicit fourth channel of one, as for glTF colours.
96    /// Consumers validate the stream type on each primitive before evaluating it.
97    /// With [`LayerWeights::channels`] it is unused.
98    pub attribute: Semantic,
99    pub mapping: WeightMapping,
100    /// Per-pixel channels, one per mapping input ([`WeightMapping::inputs`]); `None`: the attribute's channels.
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub channels: Option<Vec<Channel>>,
103    pub composition: Composition,
104    /// With [`Composition::Height`], each slot's height per pixel (a layer's own height map, sampled with its own UV
105    /// set and transform), one per slot; empty otherwise.
106    #[serde(skip_serializing_if = "Vec::is_empty")]
107    pub heights: Vec<Source>,
108    /// The blended base colour is [`saturate`]d, each lane (NaN gives 0): a blend of colours past one, or of weights
109    /// past 0 to 1, held to 0 to 1 as the blend's program holds it. The other blended quantities are as they are.
110    #[serde(skip_serializing_if = "std::ops::Not::not")]
111    pub saturate: bool,
112}
113
114impl LayerWeights {
115    /// Weights from a vertex attribute's channels, summed (the original form).
116    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    /// Whether the slot weights are evaluated per pixel: per-pixel channels, or a composition other than the sum (what
127    /// a bake of the weights to a vertex stream evaluates at the vertices).
128    pub fn per_pixel_weights(&self) -> bool {
129        self.channels.is_some() || self.composition != Composition::Sum
130    }
131    /// Whether a consumer must evaluate anything per pixel beyond interpolating one vertex attribute and summing: the
132    /// weights ([`LayerWeights::per_pixel_weights`]), or a saturated blend. [`LayeredMaterial::per_pixel`] also counts
133    /// the layers' colours.
134    pub fn per_pixel(&self) -> bool {
135        self.per_pixel_weights() || self.saturate
136    }
137}
138
139/// How the weighted layers combine.
140#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
141#[serde(rename_all = "snake_case")]
142pub enum Composition {
143    /// `sum(weight[i] * layer[i])`.
144    Sum,
145    /// Slot 0, then `lerp(colour, layer[i], weight[i])` for each later slot in order.
146    Over,
147    /// Each slot's weight `w_i` times its height `h_i` ([`LayerWeights::heights`]) is `p_i`, the highest `m`; slot `i`
148    /// gets `u_i = max(p_i - (m - (1 - contrast)), 0)`, normalized by their sum ([`height_blend`]). Where every `u_i`
149    /// is 0 (a contrast of 1 or more) the blend has no weight: a consumer states its fallback.
150    Height { contrast: f32 },
151}
152
153/// [`Composition::Height`]'s weights of `slots` slots from their mapped weights and heights; all 0 where every
154/// `u_i` is (see there).
155pub 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/// A value read per pixel.
171#[derive(Clone, Debug, PartialEq, Serialize)]
172#[serde(rename_all = "snake_case")]
173pub enum Source {
174    /// Channel `channel` (0..=3) of an interpolated vertex stream.
175    Vertex { semantic: Semantic, channel: u8 },
176    /// `dot(sample.rgba, channels) + bias` of a texture sampled with its own UV set, wrap and transform.
177    Mask {
178        texture: TextureRef,
179        channels: [f32; 4],
180        bias: f32,
181    },
182}
183
184/// One weight channel per pixel: a named form of its sources, or an [`Expression`] of them for what no named form
185/// spells. Every form follows the numeric rules and is the operations of its [`Channel::expression`].
186#[derive(Clone, Debug, PartialEq, Serialize)]
187#[serde(rename_all = "snake_case")]
188pub enum Channel {
189    Source(Source),
190    /// `lerp(a, b, t)`.
191    Lerp {
192        a: Source,
193        b: Source,
194        t: Source,
195    },
196    /// `saturate(k0 + k1 v + k2 m + k3 v m)` ([`saturate`]), summed from the left with `k3 v` before its `m`.
197    Threshold {
198        v: Source,
199        m: Source,
200        k: [f32; 4],
201    },
202    /// Product of 2 to [`PRODUCT_LIMIT`] sources, multiplied from left to right without clamping.
203    Product {
204        sources: Vec<Source>,
205    },
206    /// `v^exponent` ([`power`]), `exponent` 1 to 16.
207    Power {
208        v: Source,
209        exponent: u8,
210    },
211    /// Operations on its sources, in its canonical spelling and no named form's ([`Channel::from_expression`]).
212    Expression(Expression),
213}
214
215/// Maximum number of factors in [`Channel::Product`]; products require at least two.
216pub const PRODUCT_LIMIT: usize = 4;
217
218/// The largest exponent of [`Channel::Power`].
219pub const POWER_LIMIT: u8 = 16;
220
221/// `v^exponent` in f32 in a stated order, so a program computing it alike gives the same bits: by squaring where
222/// `exponent` is a power of two (`v^8` is three squarings, as a program's `(1 - a)^8` is written), otherwise by
223/// multiplying `v` in from the left (`((v * v) * v) * ...`).
224pub 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    /// The sources it reads.
238    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    /// The channel's value from its sources' values (in [`Channel::sources`] order).
249    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    /// Its own checks (its sources' apart): finite threshold factors, a bounded power exponent or product arity, an
264    /// expression in its canonical spelling and no named form.
265    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
297/// A weight or height source's own checks: a vertex channel within 0..=3, a mask texture alive with finite weights.
298pub(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
320/// The value of a mask texture's sample `rgba` (0..1 per channel).
321pub 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
325/// [`Composition::Over`]'s per-pixel weights as Sum weights: `w_i * prod_{j>i}(1 - w_j)`, slot 0 at full weight, over
326/// the first `slots` slots: at most the four a layered material has (more are the four).
327pub 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/// One to four ordered material slots. A null slot contributes black without
338/// redistributing its weight. Referenced materials must not themselves be layered.
339/// Consumers validate each referenced entity's material dialect; core checks
340/// entity existence and disallows nested blends independently of dialect.
341///
342/// A `pbr.material` on the same entity is its host, as for `pbr.detail`: it states the material's alpha mode and
343/// sidedness, which the blend does not, and it is the complete material for every target that does not draw the blend.
344/// A consumer that draws the blend draws the layers with the host's alpha mode and sidedness. A layered material
345/// without a host is not drawn with a default a writer picks: the writer leaves it out and reports it, and a program
346/// that wants it drawn states a pass that gives it a `pbr.material`.
347#[derive(Clone, Debug, PartialEq, Serialize)]
348pub struct LayeredMaterial {
349    pub layers: Vec<Option<Layer>>,
350    pub weights: LayerWeights,
351}
352impl LayeredMaterial {
353    /// Whether a consumer must evaluate anything per pixel beyond interpolating one vertex attribute and summing the
354    /// layers' colours as their materials give them: its weights' ([`LayerWeights::per_pixel`]), or a layer's colour
355    /// ([`Layer::color`]).
356    pub fn per_pixel(&self) -> bool {
357        self.vertex_weight_blocker().is_some()
358    }
359    /// What of it a consumer that blends by vertex weights alone (one interpolated attribute, the layers' colours
360    /// summed as their materials give them) cannot draw, named; `None` where it can draw all of it. A bake of the
361    /// weights to a vertex stream (`bake-layer-channels`) removes the first two; a saturated blend and a layer's colour
362    /// have no vertex-weight form.
363    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    /// Whether its weights read vertex stream `semantic`: the attribute, or with per-pixel channels a vertex source;
377    /// or a height a vertex source.
378    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    /// The mask textures its channels and heights sample.
387    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    /// 2: per-pixel `channels` and the `composition`; 3: each mask texture's `filters`; 4: the height composition
405    /// and its `heights`; 5: bounded product channels; 6: expression channels, and a threshold saturating NaN to 0;
406    /// 7: a layer's `color` and the weights' `saturate`.
407    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            // that the material is alive, the world checks for every reference
457            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
479/// A layer's [`Layer::color`] of `material`'s colour: each lane canonical and reading lanes 0 to 3, and the colour
480/// not one the layer states without it (the material's own, or its factor).
481fn 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    // the lane's own lane, and its own lane times a constant
489    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}