use crate::{Component, Entity, JsonError, Semantic, World, pbr::TextureRef, pbr::UvTransform};
use serde::Serialize;
use serde_json::Value;
mod expression;
pub use expression::{Expression, ExpressionBuilder, TERM_LIMIT, Term, TermId, lerp, saturate};
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Layer {
pub material: Entity,
pub texcoord: Option<u8>,
pub transform: Option<UvTransform>,
#[serde(skip_serializing_if = "Option::is_none")]
pub color: Option<[Expression<ColorLane>; 3]>,
}
impl Layer {
pub fn new(material: Entity, texcoord: Option<u8>, transform: Option<UvTransform>) -> Layer {
Layer {
material,
texcoord,
transform,
color: None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(transparent)]
pub struct ColorLane(pub u8);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
pub enum WeightMapping {
Identity,
RgbProduct4,
}
impl WeightMapping {
pub fn evaluate(self, [r, g, b, a]: [f32; 4]) -> [f32; 4] {
match self {
Self::Identity => [r, g, b, a],
Self::RgbProduct4 => [
(1. - r) * (1. - g) * (1. - b),
(1. - r) * (1. - g) * b,
(1. - r) * g * (1. - b),
(1. - r) * g * b,
],
}
}
}
impl WeightMapping {
pub fn inputs(self, slots: usize) -> usize {
match self {
Self::Identity => slots,
Self::RgbProduct4 => 3,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct LayerWeights {
pub attribute: Semantic,
pub mapping: WeightMapping,
#[serde(skip_serializing_if = "Option::is_none")]
pub channels: Option<Vec<Channel>>,
pub composition: Composition,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub heights: Vec<Source>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub saturate: bool,
}
impl LayerWeights {
pub fn new(attribute: Semantic, mapping: WeightMapping) -> Self {
LayerWeights {
attribute,
mapping,
channels: None,
composition: Composition::Sum,
heights: Vec::new(),
saturate: false,
}
}
pub fn per_pixel_weights(&self) -> bool {
self.channels.is_some() || self.composition != Composition::Sum
}
pub fn per_pixel(&self) -> bool {
self.per_pixel_weights() || self.saturate
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Composition {
Sum,
Over,
Height { contrast: f32 },
}
pub fn height_blend(weights: [f32; 4], heights: [f32; 4], slots: usize, contrast: f32) -> [f32; 4] {
let slots = slots.min(4);
let p: Vec<f32> = (0..slots).map(|i| weights[i] * heights[i]).collect();
let m = p.iter().copied().fold(f32::NEG_INFINITY, f32::max);
let u: Vec<f32> = p.iter().map(|&x| (x - (m - (1.0 - contrast))).max(0.0)).collect();
let sum: f32 = u.iter().sum();
let mut out = [0.0; 4];
if sum > 0.0 {
for (o, x) in out.iter_mut().zip(&u) {
*o = x / sum;
}
}
out
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Source {
Vertex { semantic: Semantic, channel: u8 },
Mask {
texture: TextureRef,
channels: [f32; 4],
bias: f32,
},
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Channel {
Source(Source),
Lerp {
a: Source,
b: Source,
t: Source,
},
Threshold {
v: Source,
m: Source,
k: [f32; 4],
},
Product {
sources: Vec<Source>,
},
Power {
v: Source,
exponent: u8,
},
Expression(Expression),
}
pub const PRODUCT_LIMIT: usize = 4;
pub const POWER_LIMIT: u8 = 16;
pub fn power(v: f32, exponent: u8) -> f32 {
if exponent.is_power_of_two() {
let mut x = v;
for _ in 0..exponent.trailing_zeros() {
x *= x;
}
x
} else {
(1..exponent).fold(v, |x, _| x * v)
}
}
impl Channel {
pub fn sources(&self) -> Vec<&Source> {
match self {
Channel::Source(s) => vec![s],
Channel::Lerp { a, b, t } => vec![a, b, t],
Channel::Threshold { v, m, .. } => vec![v, m],
Channel::Power { v, .. } => vec![v],
Channel::Product { sources } => sources.iter().collect(),
Channel::Expression(e) => e.sources.iter().collect(),
}
}
pub fn evaluate(&self, values: &[f32]) -> f32 {
match self {
Channel::Source(_) => values[0],
Channel::Lerp { .. } => lerp(values[0], values[1], values[2]),
Channel::Threshold { k, .. } => {
let (v, m) = (values[0], values[1]);
saturate(k[0] + k[1] * v + k[2] * m + k[3] * v * m)
}
Channel::Power { exponent, .. } => power(values[0], *exponent),
Channel::Product { sources } => values[..sources.len()].iter().fold(1., |p, v| p * v),
Channel::Expression(e) => e.evaluate(values),
}
}
pub fn check(&self) -> Result<(), String> {
match self {
Channel::Product { sources } if !(2..=PRODUCT_LIMIT).contains(&sources.len()) => Err(format!(
"product has {} sources, requires 2 to {PRODUCT_LIMIT}",
sources.len()
)),
Channel::Threshold { k, .. } if !k.iter().all(|x| x.is_finite()) => {
Err(format!("threshold {k:?} is not finite"))
}
Channel::Power { exponent, .. } if !(1..=POWER_LIMIT).contains(exponent) => {
Err(format!("power exponent {exponent} is not 1 to {POWER_LIMIT}"))
}
Channel::Expression(e) => {
e.check()?;
match Channel::from_expression(e)? {
Channel::Expression(_) => Ok(()),
named @ (Channel::Source(_)
| Channel::Lerp { .. }
| Channel::Threshold { .. }
| Channel::Product { .. }
| Channel::Power { .. }) => Err(format!("expression is a named form: {named:?}")),
}
}
Channel::Source(_)
| Channel::Lerp { .. }
| Channel::Threshold { .. }
| Channel::Product { .. }
| Channel::Power { .. } => Ok(()),
}
}
}
pub(crate) fn check_source(world: &World, what: &str, s: &Source) -> Result<(), String> {
match s {
Source::Vertex { channel, .. } if *channel > 3 => {
Err(format!("{what}: vertex channel {channel} past 3"))
}
Source::Vertex { .. } => Ok(()),
Source::Mask {
texture,
channels,
bias,
} => {
if !world.is_alive(texture.texture) {
return Err(format!("{what}: mask texture {:?} is not alive", texture.texture));
}
if !channels.iter().chain([bias]).all(|x| x.is_finite()) {
return Err(format!("{what}: mask weights {channels:?} + {bias} not finite"));
}
Ok(())
}
}
}
pub fn mask_value(channels: [f32; 4], bias: f32, rgba: [f32; 4]) -> f32 {
(0..4).map(|i| channels[i] * rgba[i]).sum::<f32>() + bias
}
pub fn over_as_sum(weights: [f32; 4], slots: usize) -> [f32; 4] {
let slots = slots.min(weights.len());
let mut out = [0.0; 4];
for i in 0..slots {
let own = if i == 0 { 1.0 } else { weights[i] };
out[i] = own * (i + 1..slots).map(|j| 1.0 - weights[j]).product::<f32>();
}
out
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct LayeredMaterial {
pub layers: Vec<Option<Layer>>,
pub weights: LayerWeights,
}
impl LayeredMaterial {
pub fn per_pixel(&self) -> bool {
self.vertex_weight_blocker().is_some()
}
pub fn vertex_weight_blocker(&self) -> Option<&'static str> {
if self.weights.channels.is_some() {
Some("per-pixel weight channels")
} else if self.weights.composition != Composition::Sum {
Some("the over or height composition")
} else if self.weights.saturate {
Some("a saturated blend")
} else if self.layers.iter().flatten().any(|l| l.color.is_some()) {
Some("a layer's colour expression")
} else {
None
}
}
pub fn reads(&self, semantic: Semantic) -> bool {
let reads = |s: &Source| matches!(s, Source::Vertex { semantic: v, .. } if *v == semantic);
let weights = match &self.weights.channels {
None => self.weights.attribute == semantic,
Some(channels) => channels.iter().flat_map(Channel::sources).any(reads),
};
weights || self.weights.heights.iter().any(reads)
}
pub fn mask_textures(&self) -> impl Iterator<Item = &TextureRef> {
self.weights
.channels
.iter()
.flatten()
.flat_map(Channel::sources)
.chain(&self.weights.heights)
.filter_map(|s| match s {
Source::Mask { texture, .. } => Some(texture),
Source::Vertex { .. } => None,
})
}
}
impl Component for LayeredMaterial {
const NAME: &'static str = "core.layered";
const GEOMETRY: crate::Geometry = crate::Geometry::None;
const VERSION: u32 = 7;
fn references(&self, out: &mut Vec<Entity>) {
out.extend(self.layers.iter().flatten().map(|layer| layer.material));
out.extend(self.mask_textures().map(|t| t.texture));
}
fn to_json(&self) -> Result<Option<Value>, JsonError> {
crate::json::to_value(self).map(Some)
}
fn verify(&self, entity: Entity, world: &World) -> Result<(), String> {
if !(1..=4).contains(&self.layers.len()) {
return Err("layered material requires one to four slots".into());
}
if self.weights.mapping == WeightMapping::RgbProduct4 && self.layers.len() != 4 {
return Err("rgb_product_4 requires four slots".into());
}
if let Some(channels) = &self.weights.channels {
let want = self.weights.mapping.inputs(self.layers.len());
if channels.len() != want {
return Err(format!("{} channels for a mapping of {want}", channels.len()));
}
for (i, c) in channels.iter().enumerate() {
c.check().map_err(|e| format!("channel {i}: {e}"))?;
for s in c.sources() {
check_source(world, &format!("channel {i}"), s)?;
}
}
}
match self.weights.composition {
Composition::Height { contrast } => {
if !contrast.is_finite() {
return Err(format!("height composition: contrast {contrast} is not finite"));
}
if self.weights.heights.len() != self.layers.len() {
return Err(format!(
"height composition: {} heights for {} slots",
self.weights.heights.len(),
self.layers.len()
));
}
for (i, s) in self.weights.heights.iter().enumerate() {
check_source(world, &format!("height {i}"), s)?;
}
}
Composition::Sum | Composition::Over if !self.weights.heights.is_empty() => {
return Err("heights without the height composition".into());
}
Composition::Sum | Composition::Over => {}
}
for (i, layer) in self.layers.iter().enumerate() {
let Some(layer) = layer else { continue };
if layer.material == entity || world.get::<Self>(layer.material).is_some() {
return Err(format!("layer {i} references a layered material"));
}
if let Some(t) = layer.transform
&& !t
.offset
.into_iter()
.chain(t.scale)
.chain([t.rotation])
.all(f32::is_finite)
{
return Err(format!("layer {i} has a nonfinite UV transform"));
}
if let Some(color) = &layer.color {
check_color(world, layer.material, color).map_err(|e| format!("layer {i} color: {e}"))?;
}
}
Ok(())
}
}
fn check_color(world: &World, material: Entity, color: &[Expression<ColorLane>; 3]) -> Result<(), String> {
for (lane, e) in color.iter().enumerate() {
e.check().map_err(|why| format!("lane {lane}: {why}"))?;
if let Some(ColorLane(c)) = e.sources.iter().find(|ColorLane(c)| *c > 3) {
return Err(format!("lane {lane} reads colour lane {c}, past 3"));
}
}
let own = |lane: u8, e: &Expression<ColorLane>| {
e.sources.as_slice() == [ColorLane(lane)] && e.terms.as_slice() == [Term::Source(0)]
};
let scaled = |lane: u8, e: &Expression<ColorLane>| {
e.sources.as_slice() == [ColorLane(lane)]
&& matches!(
e.terms.as_slice(),
[Term::Constant(_), Term::Source(0), Term::Multiply(_)]
)
};
let lanes = || (0u8..).zip(color);
if lanes().all(|(lane, e)| own(lane, e)) {
return Err("the material's own colour, which a layer states with no color".into());
}
let unit_factor = world
.get::<crate::pbr::PbrMaterial>(material)
.is_some_and(|m| m.base_color[..3] == [1.0; 3]);
if unit_factor && lanes().all(|(lane, e)| own(lane, e) || scaled(lane, e)) {
return Err("each lane its own times a constant: the material's base colour factor".into());
}
Ok(())
}