use serde::Serialize;
use serde_json::Value;
use crate::layered::{Source, check_source, mask_value};
use crate::pbr::TextureRef;
use crate::shader::ScalarEncoding;
use crate::{Component, Entity, JsonError, World};
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct DetailedMaterial {
pub mix: DetailMix,
pub lowering: SurfaceLowering,
pub frozen: Vec<Frozen>,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Frozen {
FrontFacing,
VertexInput {
semantic: String,
index: u32,
value: [f32; 4],
},
EngineConstant { name: String, value: Vec<f32> },
EngineTexture { name: String, value: [f32; 4] },
}
impl Frozen {
fn finite(&self) -> bool {
match self {
Frozen::FrontFacing => true,
Frozen::VertexInput { value, .. } | Frozen::EngineTexture { value, .. } => {
value.iter().all(|v| v.is_finite())
}
Frozen::EngineConstant { value, .. } => value.iter().all(|v| v.is_finite()),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DetailMix {
DetailLerp(DetailLerp),
}
pub const REVISION: u32 = 4;
pub const DETAIL_LIMIT: usize = 3;
pub const LUMA: [f32; 3] = [0.29891, 0.58661, 0.11448];
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct DetailLerp {
pub base_colour: TextureRef,
pub base_normal: TextureRef,
pub base_mask: TextureRef,
pub base_saturation: f32,
pub details: Vec<Detail>,
pub weights: Vec<Weight>,
pub gain: f32,
pub normal_strength: f32,
pub colour: [f32; 3],
pub gloss_encoding: ScalarEncoding,
pub alpha: Option<usize>,
}
pub const WEIGHT_POWER_LIMIT: u16 = 512;
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Weight {
pub source: Source,
pub steps: Vec<WeightStep>,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum WeightStep {
Power(u16),
Min(f32),
Max(f32),
Affine { scale: f32, bias: f32 },
Saturate,
}
pub fn weight_power(x: f32, exponent: u16) -> f32 {
let mut square = x;
let mut out: Option<f32> = None;
for bit in 0..16 {
if exponent >> bit == 0 {
break;
}
if exponent & (1 << bit) != 0 {
out = Some(out.map_or(square, |acc| acc * square));
}
square *= square;
}
out.unwrap_or(1.0)
}
pub fn weight_steps(v: f32, steps: &[WeightStep]) -> f32 {
steps.iter().fold(v, |x, step| match *step {
WeightStep::Power(exponent) => weight_power(x, exponent),
WeightStep::Min(bound) => x.min(bound),
WeightStep::Max(bound) => x.max(bound),
WeightStep::Affine { scale, bias } => x * scale + bias,
WeightStep::Saturate => {
if x.is_nan() {
0.0
} else {
x.clamp(0.0, 1.0)
}
}
})
}
impl Weight {
pub fn evaluate(&self, v: f32) -> f32 {
weight_steps(v, &self.steps)
}
pub fn of_sample(&self, sample: [f32; 4]) -> Option<f32> {
match self.source {
Source::Mask { channels, bias, .. } => Some(self.evaluate(mask_value(channels, bias, sample))),
Source::Vertex { .. } => None,
}
}
pub fn check(&self) -> Result<(), String> {
for step in &self.steps {
match *step {
WeightStep::Power(e) if !(1..=WEIGHT_POWER_LIMIT).contains(&e) => {
return Err(format!("power exponent {e} is not 1 to {WEIGHT_POWER_LIMIT}"));
}
WeightStep::Min(bound) if !bound.is_finite() => {
return Err(format!("min bound {bound} is not finite"));
}
WeightStep::Max(bound) if !bound.is_finite() => {
return Err(format!("max bound {bound} is not finite"));
}
WeightStep::Affine { scale, bias } if !(scale.is_finite() && bias.is_finite()) => {
return Err(format!("affine step {scale} * x + {bias} is not finite"));
}
_ => {}
}
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum GlossScale {
BaseNormalZ,
Constant(f32),
}
impl GlossScale {
pub fn value(self, normal: [f32; 4]) -> f32 {
match self {
GlossScale::BaseNormalZ => normal[2],
GlossScale::Constant(c) => c,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Detail {
pub colour: TextureRef,
pub normal: TextureRef,
pub tint: [f32; 3],
pub gloss_bias: f32,
pub gloss_scale: GlossScale,
pub reflectance: ReflectanceTerm,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ReflectanceTerm {
MaskPlusBias(f32),
Bias(f32),
}
impl ReflectanceTerm {
pub fn value(self, mask: [f32; 4]) -> f32 {
match self {
ReflectanceTerm::MaskPlusBias(bias) => mask[0] + bias,
ReflectanceTerm::Bias(bias) => bias,
}
}
pub fn bias(self) -> f32 {
match self {
ReflectanceTerm::MaskPlusBias(bias) | ReflectanceTerm::Bias(bias) => bias,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct DetailSamples {
pub base_colour: [f32; 4],
pub base_normal: [f32; 4],
pub base_mask: [f32; 4],
pub details: Vec<DetailSample>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DetailSample {
pub colour: [f32; 4],
pub normal: [f32; 4],
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DetailSurface {
pub c: [f32; 3],
pub colour: [f32; 3],
pub mix: f32,
pub alpha: f32,
pub normal: [f32; 3],
pub gloss: f32,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Written {
pub albedo: [f32; 3],
pub reflectance: Option<[f32; 3]>,
pub metallic: Option<f32>,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SurfaceLowering {
GlossReflectance {
dielectric_f0: f32,
},
MetallicGloss,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PbrPixel {
pub base_color: [f32; 4],
pub normal: [f32; 3],
pub roughness: f32,
pub metallic: f32,
pub specular_color: Option<[f32; 3]>,
}
pub const SPECULAR_COLOR_FACTOR: f32 = 25.0;
fn lerp(a: f32, b: f32, t: f32) -> f32 {
a + (b - a) * t
}
pub fn chain(x: impl IntoIterator<Item = f32>, weights: &[f32]) -> f32 {
let mut x = x.into_iter();
let first = x.next().unwrap_or(0.0);
x.zip(weights).fold(first, |acc, (next, &w)| lerp(acc, next, w))
}
impl DetailLerp {
pub fn textures(&self) -> Vec<&TextureRef> {
let mut out = vec![&self.base_colour, &self.base_normal, &self.base_mask];
for d in &self.details {
out.extend([&d.colour, &d.normal]);
}
out.extend(self.weights.iter().filter_map(|w| match &w.source {
Source::Mask { texture, .. } => Some(texture),
Source::Vertex { .. } => None,
}));
out
}
pub fn weight_values(&self, sources: &[f32]) -> Vec<f32> {
self.weights
.iter()
.zip(sources)
.map(|(w, &v)| w.evaluate(v))
.collect()
}
pub fn evaluate(&self, s: &DetailSamples, weights: &[f32]) -> DetailSurface {
let (a, n, m) = (s.base_colour, s.base_normal, s.base_mask);
let pairs = || self.details.iter().zip(&s.details);
let base: [f32; 3] = if self.base_saturation == 0.0 {
[a[0], a[1], a[2]]
} else {
let luma = a[0] * LUMA[0] + a[1] * LUMA[1] + a[2] * LUMA[2];
std::array::from_fn(|i| lerp(a[i], luma, self.base_saturation))
};
let c: [f32; 3] = std::array::from_fn(|i| {
let detail = chain(pairs().map(|(d, x)| d.tint[i] * x.colour[i]), weights);
(self.gain * base[i] * detail).clamp(0.0, 1.0)
});
let f = chain(self.details.iter().map(|d| d.reflectance.value(m)), weights).clamp(0.0, 1.0);
let base = [2.0 * n[0] - 1.0, 2.0 * n[1] - 1.0];
let z = (1.0 - (base[0] * base[0] + base[1] * base[1]).min(1.0)).sqrt();
let detail = |i: usize| 2.0 * chain(s.details.iter().map(|x| x.normal[i]), weights) - 1.0;
let normal = [
base[0] + self.normal_strength * detail(0),
base[1] + self.normal_strength * detail(1),
z,
];
let scale = chain(self.details.iter().map(|d| d.gloss_scale.value(n)), weights);
let stored =
(2.0 * scale * chain(pairs().map(|(d, x)| x.normal[2] + d.gloss_bias), weights)).clamp(0.0, 1.0);
let gloss = self.gloss_encoding.decode(stored);
let alpha = self
.alpha
.and_then(|j| s.details.get(j))
.map_or(1.0, |x| (x.colour[3] * a[3]).clamp(0.0, 1.0));
DetailSurface {
c,
colour: self.colour,
mix: f,
alpha,
normal,
gloss,
}
}
fn constants_finite(&self) -> bool {
let scale = |d: &Detail| match d.gloss_scale {
GlossScale::Constant(c) => c,
GlossScale::BaseNormalZ => 0.0,
};
self.details
.iter()
.flat_map(|d| {
d.tint
.into_iter()
.chain([d.gloss_bias, d.reflectance.bias(), scale(d)])
})
.chain(self.colour)
.chain([self.gain, self.normal_strength, self.base_saturation])
.all(f32::is_finite)
}
fn check(&self) -> Result<(), String> {
if !(1..=DETAIL_LIMIT).contains(&self.details.len()) {
return Err(format!(
"{} details: a detail lerp chains 1 to {DETAIL_LIMIT}",
self.details.len()
));
}
if self.weights.len() + 1 != self.details.len() {
return Err(format!(
"{} weights for {} details: one fewer than the details",
self.weights.len(),
self.details.len()
));
}
if !self.constants_finite() {
return Err("a detail mix constant is not finite".into());
}
for w in &self.weights {
w.check().map_err(|e| format!("weight: {e}"))?;
}
if let Some(j) = self.alpha
&& j >= self.details.len()
{
return Err(format!(
"the alpha is detail {j}'s, of {} details",
self.details.len()
));
}
self.gloss_encoding
.check()
.map_err(|e| format!("gloss encoding: {e}"))
}
}
impl DetailMix {
pub fn textures(&self) -> Vec<&TextureRef> {
match self {
DetailMix::DetailLerp(mix) => mix.textures(),
}
}
}
impl SurfaceLowering {
pub fn written(self, surface: &DetailSurface) -> Written {
let (c, f) = (surface.c, surface.mix);
match self {
SurfaceLowering::GlossReflectance { dielectric_f0 } => Written {
albedo: std::array::from_fn(|i| (1.0 - f) * c[i] * surface.colour[i]),
reflectance: Some(c.map(|c| lerp(dielectric_f0, c, f))),
metallic: None,
},
SurfaceLowering::MetallicGloss => Written {
albedo: std::array::from_fn(|i| c[i] * surface.colour[i]),
reflectance: None,
metallic: Some(f),
},
}
}
pub fn evaluate(self, surface: &DetailSurface) -> PbrPixel {
let written = self.written(surface);
let [r, g, b] = written.albedo;
let n = surface.normal;
let length = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
PbrPixel {
base_color: [r, g, b, surface.alpha],
normal: n.map(|x| x / length),
roughness: 1.0 - surface.gloss,
metallic: written.metallic.unwrap_or(0.0),
specular_color: written.reflectance,
}
}
fn check(self) -> Result<(), String> {
match self {
SurfaceLowering::GlossReflectance { dielectric_f0 } if !dielectric_f0.is_finite() => {
Err(format!("dielectric reflectance {dielectric_f0} is not finite"))
}
_ => Ok(()),
}
}
}
impl Component for DetailedMaterial {
const NAME: &'static str = "pbr.detail";
const GEOMETRY: crate::Geometry = crate::Geometry::None;
const VERSION: u32 = REVISION;
fn to_json(&self) -> Result<Option<Value>, JsonError> {
crate::json::to_value(self).map(Some)
}
fn verify(&self, _e: Entity, world: &World) -> Result<(), String> {
if let Some(f) = self.frozen.iter().find(|f| !f.finite()) {
return Err(format!("frozen {f:?} is not finite"));
}
self.lowering.check()?;
match &self.mix {
DetailMix::DetailLerp(mix) => {
mix.check()?;
for w in &mix.weights {
check_source(world, "weight", &w.source)?;
}
}
}
for t in self.mix.textures() {
if !world.is_alive(t.texture) {
return Err(format!("texture {:?} is not alive", t.texture));
}
if let Some(x) = t.transform
&& !x
.offset
.iter()
.chain(&x.scale)
.chain([&x.rotation])
.all(|v| v.is_finite())
{
return Err(format!("texture transform {x:?} is not finite"));
}
}
Ok(())
}
fn references(&self, out: &mut Vec<Entity>) {
out.extend(self.mix.textures().into_iter().map(|t| t.texture));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::layered::{Channel, power};
fn weight(texture: TextureRef, flip: bool) -> Weight {
Weight {
source: Source::Mask {
texture,
channels: [0., 0., 0., if flip { 1. } else { -1. }],
bias: if flip { 0. } else { 1. },
},
steps: vec![WeightStep::Power(8)],
}
}
fn mix(world: &mut World, n: usize) -> DetailLerp {
let mut t = || TextureRef::new(world.spawn());
let (base_colour, base_normal, base_mask) = (t(), t(), t());
let tints = [[1., 0.5, 0.25], [0.5, 1., 1.], [0.25, 0.75, 2.]];
let reflectance = [
ReflectanceTerm::MaskPlusBias(0.125),
ReflectanceTerm::Bias(0.75),
ReflectanceTerm::Bias(0.375),
];
let details = (0..n)
.map(|i| Detail {
colour: t(),
normal: t(),
tint: tints[i],
gloss_bias: [0.25, -0.25, 0.5][i],
gloss_scale: GlossScale::BaseNormalZ,
reflectance: reflectance[i],
})
.collect();
DetailLerp {
weights: [weight(base_normal, false), weight(base_normal, true)][..n - 1].to_vec(),
base_colour,
base_normal,
base_mask,
base_saturation: 0.,
details,
gain: 4.55,
normal_strength: 0.5,
colour: [1., 1., 0.5],
gloss_encoding: ScalarEncoding::LINEAR,
alpha: Some(0),
}
}
fn samples(n: usize) -> DetailSamples {
let details = [
([0.5, 0.5, 0.5, 1.], [0.5, 0.5, 0.75, 0.]),
([1., 0.25, 0.5, 0.], [1., 0., 0.25, 0.]),
([0.25, 0.75, 0.125, 0.5], [0.25, 0.75, 0.5, 0.]),
];
DetailSamples {
base_colour: [0.1, 0.2, 0.05, 0.5],
base_normal: [0.75, 0.25, 0.5, 0.75],
base_mask: [0.5, 0., 0., 0.],
details: details[..n]
.iter()
.map(|&(colour, normal)| DetailSample { colour, normal })
.collect(),
}
}
#[test]
fn details_are_lerped_in_turn_over_the_base() {
let mut world = World::new();
for n in 1..=3 {
let mix = mix(&mut world, n);
let s = samples(n);
let a = s.base_normal[3];
let sources = [1. - a, a];
let weights = mix.weight_values(&sources[..n - 1]);
if n >= 2 {
assert_eq!(weights[0].to_bits(), power(1. - a, 8).to_bits());
assert_eq!(
weights[0].to_bits(),
(((0.25f32 * 0.25) * (0.25 * 0.25)) * ((0.25 * 0.25) * (0.25 * 0.25))).to_bits()
);
}
if n == 3 {
assert_eq!(weights[1].to_bits(), power(a, 8).to_bits());
}
let by_hand = |x: &[f32]| -> f32 {
let mut acc = x[0];
for (next, w) in x[1..].iter().zip(&weights) {
acc = acc + (next - acc) * w;
}
acc
};
let out = mix.evaluate(&s, &weights);
let written = SurfaceLowering::GlossReflectance { dielectric_f0: 0.03 }.written(&out);
let c: [f32; 3] = std::array::from_fn(|i| {
let x: Vec<f32> = (0..n)
.map(|j| mix.details[j].tint[i] * s.details[j].colour[i])
.collect();
(mix.gain * s.base_colour[i] * by_hand(&x)).clamp(0., 1.)
});
let terms: Vec<f32> = [0.5 + 0.125, 0.75, 0.375][..n].to_vec();
let f = by_hand(&terms).clamp(0., 1.);
assert_eq!((out.c, out.mix, out.colour), (c, f, mix.colour), "{n} details");
for (i, c) in c.iter().enumerate() {
assert_eq!(written.albedo[i], (1. - f) * c * mix.colour[i], "{n} details");
assert_eq!(
written.reflectance.map(|r| r[i]),
Some(0.03 + (c - 0.03) * f),
"{n} details"
);
assert_eq!(written.metallic, None);
}
let k = |i: usize| {
let x: Vec<f32> = s.details.iter().map(|d| d.normal[i]).collect();
2. * by_hand(&x) - 1.
};
assert_eq!(
out.normal,
[0.5 + 0.5 * k(0), -0.5 + 0.5 * k(1), (1f32 - 0.5).sqrt()],
"{n} details"
);
let g: Vec<f32> = (0..n)
.map(|j| s.details[j].normal[2] + mix.details[j].gloss_bias)
.collect();
assert_eq!(out.gloss, (2. * 0.5 * by_hand(&g)).clamp(0., 1.), "{n} details");
assert_eq!(out.alpha, 0.5);
}
let one = mix(&mut world, 1);
let s = samples(1);
let out = one.evaluate(&s, &[]);
let f = 0.5 + 0.125;
let written = SurfaceLowering::GlossReflectance { dielectric_f0: 0.03 }.written(&out);
assert_eq!(
written.reflectance.map(|r| r[0]),
Some(0.03 + ((4.55 * 0.1 * (1. * 0.5)) - 0.03) * f)
);
}
#[test]
fn the_base_colour_is_desaturated_toward_its_luma() {
let mut world = World::new();
let mut one = mix(&mut world, 1);
let s = samples(1);
let plain = one.evaluate(&s, &[]);
let a = s.base_colour;
let (gain, tint) = (one.gain, one.details[0].tint);
let by_hand = |saturation: f32| -> [f32; 3] {
let luma = a[0] * LUMA[0] + a[1] * LUMA[1] + a[2] * LUMA[2];
std::array::from_fn(|i| {
let base = a[i] + (luma - a[i]) * saturation;
(gain * base * (tint[i] * s.details[0].colour[i])).clamp(0., 1.)
})
};
let none: [f32; 3] =
std::array::from_fn(|i| (gain * a[i] * (tint[i] * s.details[0].colour[i])).clamp(0., 1.));
assert_eq!(plain.c, none);
for saturation in [0.25, 1.] {
one.base_saturation = saturation;
assert_eq!(one.evaluate(&s, &[]).c, by_hand(saturation), "{saturation}");
}
assert!((LUMA.iter().sum::<f32>() - 1.).abs() < 1e-6);
let mut grey = samples(1);
grey.base_colour = [0.25, 0.25, 0.25, 1.];
let desaturated = one.evaluate(&grey, &[]);
one.base_saturation = 0.;
let kept = one.evaluate(&grey, &[]);
for i in 0..3 {
assert!((kept.c[i] - desaturated.c[i]).abs() <= 1e-6, "lane {i}");
}
}
#[test]
fn the_ramp_steps_are_taken_in_turn() {
let ramp = [
WeightStep::Affine {
scale: 2.,
bias: -0.5,
},
WeightStep::Saturate,
];
assert_eq!(weight_steps(0.375, &ramp), 0.25);
assert_eq!(weight_steps(0.125, &ramp), 0.);
assert_eq!(weight_steps(1., &ramp), 1.);
assert_eq!(weight_steps(f32::NAN, &ramp), 0.);
assert_eq!(
weight_steps(-1., &[WeightStep::Max(0.), WeightStep::Power(8)]),
0.
);
assert_eq!(weight_steps(f32::NAN, &[WeightStep::Max(0.)]), 0.);
assert_eq!(weight_steps(0.5, &[WeightStep::Max(0.75)]), 0.75);
assert_eq!(
weight_steps(
0.5,
&[
WeightStep::Affine {
scale: 1.,
bias: 0.25
},
WeightStep::Power(2)
]
),
0.5625
);
let mut world = World::new();
let mut weight = |steps| Weight {
source: Source::Mask {
texture: TextureRef::new(world.spawn()),
channels: [0., 0., 0., 1.],
bias: 0.,
},
steps,
};
assert_eq!(weight(ramp.to_vec()).check(), Ok(()));
assert!(
weight(vec![WeightStep::Affine {
scale: f32::NAN,
bias: 0.
}])
.check()
.unwrap_err()
.contains("affine")
);
assert!(
weight(vec![WeightStep::Max(f32::INFINITY)])
.check()
.unwrap_err()
.contains("max bound")
);
assert_eq!(weight(vec![WeightStep::Saturate]).check(), Ok(()));
}
#[test]
fn the_gloss_is_decoded_by_its_encoding() {
let mut world = World::new();
let linear = mix(&mut world, 2);
let encoding = ScalarEncoding {
power: 1,
root: 2,
scale_bits: 0.95f32.to_bits(),
};
let encoded = DetailLerp {
gloss_encoding: encoding,
..linear.clone()
};
let mut s = samples(2);
s.base_normal = [0.5, 0.5, 0.25, 0.75];
s.details[1].normal = [0.5, 0.5, 0.5, 0.];
let weights = linear.weight_values(&[1. - s.base_normal[3]]);
let stored = linear.evaluate(&s, &weights).gloss;
assert!(stored > 0. && stored < 1.);
let gloss = encoded.evaluate(&s, &weights).gloss;
assert_eq!(gloss, encoding.decode(stored));
assert_eq!(gloss, stored.sqrt() * 0.95);
let e = world.spawn();
let bad = DetailedMaterial {
mix: DetailMix::DetailLerp(DetailLerp {
gloss_encoding: ScalarEncoding { power: 0, ..encoding },
..linear
}),
lowering: SurfaceLowering::GlossReflectance { dielectric_f0: 0.03 },
frozen: Vec::new(),
};
assert!(bad.verify(e, &world).unwrap_err().contains("gloss encoding"));
}
#[test]
fn a_power_is_taken_in_its_stated_order_and_bounded() {
let x = 0.737_f32;
assert_eq!(power(x, 1), x);
assert_eq!(power(x, 8).to_bits(), {
let a = x * x;
let b = a * a;
(b * b).to_bits()
});
assert_eq!(power(x, 3).to_bits(), ((x * x) * x).to_bits());
let channel = |exponent| Channel::Power {
v: Source::Vertex {
semantic: crate::Semantic::Color(0),
channel: 0,
},
exponent,
};
assert!(channel(0).check().is_err());
assert!(channel(crate::layered::POWER_LIMIT + 1).check().is_err());
assert!(channel(16).check().is_ok());
assert_eq!(channel(2).evaluate(&[0.5]), 0.25);
}
#[test]
fn a_weights_steps_are_taken_in_the_programs_order() {
let x = 0.987_654_3_f32;
let squares: Vec<f32> = std::iter::successors(Some(x), |s| Some(s * s)).take(9).collect();
assert_eq!(
weight_power(x, 260).to_bits(),
(squares[2] * squares[8]).to_bits()
);
assert_eq!(weight_power(x, 8).to_bits(), power(x, 8).to_bits());
assert_eq!(weight_power(x, 1), x);
assert_eq!(
weight_power(x, 7).to_bits(),
((squares[0] * squares[1]) * squares[2]).to_bits()
);
let mut world = World::new();
let switch_one = Weight {
source: Source::Mask {
texture: TextureRef::new(world.spawn()),
channels: [0., 0., 22., 0.],
bias: 0.,
},
steps: vec![WeightStep::Power(260), WeightStep::Min(1.)],
};
assert_eq!(switch_one.check(), Ok(()));
assert_eq!(switch_one.of_sample([0.5, 0.5, 0., 0.5]), Some(0.));
assert_eq!(switch_one.of_sample([0.5, 0.5, 1. / 11., 0.5]), Some(1.));
let z = 0.045_f32;
let v = 22. * z;
let squares: Vec<f32> = std::iter::successors(Some(v), |s| Some(s * s)).take(9).collect();
assert_eq!(
switch_one.of_sample([0.5, 0.5, z, 0.5]).unwrap().to_bits(),
(squares[2] * squares[8]).min(1.).to_bits()
);
assert_eq!(switch_one.evaluate(f32::NAN), 1.);
let old = Channel::Power {
v: Source::Vertex {
semantic: crate::Semantic::Color(0),
channel: 0,
},
exponent: 8,
};
let new = weight(TextureRef::new(world.spawn()), true);
for a in [0.1_f32, 0.37, 0.5, 0.93] {
assert_eq!(old.evaluate(&[a]).to_bits(), new.evaluate(a).to_bits());
}
}
#[test]
fn each_detail_scales_its_gloss_as_it_states() {
let mut world = World::new();
let mut two = mix(&mut world, 2);
let s = samples(2);
let n = s.base_normal;
let w = 0.25_f32;
let base = two.evaluate(&s, &[w]).gloss;
let terms: Vec<f32> = (0..2)
.map(|j| s.details[j].normal[2] + two.details[j].gloss_bias)
.collect();
let g = terms[0] + (terms[1] - terms[0]) * w;
assert_eq!(base, (2. * n[2] * g).clamp(0., 1.));
two.details[0].gloss_scale = GlossScale::Constant(0.5);
let scale = 0.5 + (n[2] - 0.5) * w;
assert_eq!(two.evaluate(&s, &[w]).gloss, (2. * scale * g).clamp(0., 1.));
}
#[test]
fn the_alpha_is_the_stated_details() {
let mut world = World::new();
let mut two = mix(&mut world, 2);
let s = samples(2);
two.alpha = Some(1);
assert_eq!(two.evaluate(&s, &[0.5]).alpha, 0.);
two.alpha = Some(0);
assert_eq!(two.evaluate(&s, &[0.5]).alpha, 0.5);
two.alpha = None;
assert_eq!(two.evaluate(&s, &[0.5]).alpha, 1.);
}
#[test]
fn a_surface_lowers_to_the_pbr_model_as_its_g_buffer_holds_it() {
let surface = DetailSurface {
c: [0.25, 0.5, 0.75],
colour: [1., 1., 0.5],
mix: 0.5,
alpha: 0.5,
normal: [0., 3., 4.],
gloss: 0.75,
};
let f0 = SurfaceLowering::GlossReflectance { dielectric_f0: 0. };
let written = f0.written(&surface);
assert_eq!(written.albedo, [0.125, 0.25, 0.1875]);
assert_eq!(written.reflectance, Some([0.125, 0.25, 0.375]));
assert_eq!(written.metallic, None);
let pixel = f0.evaluate(&surface);
assert_eq!(pixel.base_color, [0.125, 0.25, 0.1875, 0.5]);
assert_eq!(pixel.normal, [0., 0.6, 0.8]);
assert_eq!((pixel.roughness, pixel.metallic), (0.25, 0.));
assert_eq!(pixel.specular_color, Some([0.125, 0.25, 0.375]));
assert_eq!(0.04 * SPECULAR_COLOR_FACTOR, 1.);
let metal = SurfaceLowering::MetallicGloss;
let written = metal.written(&surface);
assert_eq!(written.albedo, [0.25, 0.5, 0.375]);
assert_eq!((written.reflectance, written.metallic), (None, Some(0.5)));
let pixel = metal.evaluate(&surface);
assert_eq!(pixel.base_color, [0.25, 0.5, 0.375, 0.5]);
assert_eq!(pixel.normal, [0., 0.6, 0.8]);
assert_eq!(
(pixel.roughness, pixel.metallic, pixel.specular_color),
(0.25, 0.5, None)
);
let mut world = World::new();
let e = world.spawn();
let bad = DetailedMaterial {
mix: DetailMix::DetailLerp(mix(&mut world, 1)),
lowering: SurfaceLowering::GlossReflectance {
dielectric_f0: f32::NAN,
},
frozen: Vec::new(),
};
assert!(bad.verify(e, &world).unwrap_err().contains("dielectric"));
}
#[test]
fn a_detail_material_verifies_its_textures_constants_and_weights() {
let mut world = World::new();
let good = DetailedMaterial {
mix: DetailMix::DetailLerp(mix(&mut world, 2)),
lowering: SurfaceLowering::GlossReflectance { dielectric_f0: 0.03 },
frozen: vec![
Frozen::FrontFacing,
Frozen::EngineConstant {
name: "SomeEngineConstant".into(),
value: vec![0.],
},
],
};
let e = world.spawn();
assert_eq!(good.verify(e, &world), Ok(()));
let mut refs = Vec::new();
good.references(&mut refs);
assert_eq!(refs.len(), 8, "seven textures and the weight's");
let edit = |f: &dyn Fn(&mut DetailLerp)| {
let mut bad = good.clone();
let DetailMix::DetailLerp(m) = &mut bad.mix;
f(m);
bad
};
let bad = edit(&|m| m.gain = f32::NAN);
assert!(bad.verify(e, &world).unwrap_err().contains("not finite"));
let bad = edit(&|m| m.base_saturation = f32::INFINITY);
assert!(bad.verify(e, &world).unwrap_err().contains("not finite"));
let mut bad = good.clone();
bad.frozen.push(Frozen::EngineTexture {
name: "g_DecalTexture".into(),
value: [f32::INFINITY; 4],
});
assert!(bad.verify(e, &world).unwrap_err().contains("frozen"));
let bad = edit(&|m| m.details[1].reflectance = ReflectanceTerm::Bias(f32::INFINITY));
assert!(bad.verify(e, &world).unwrap_err().contains("not finite"));
let bad = edit(&|m| m.weights[0].steps = vec![WeightStep::Power(0)]);
assert!(bad.verify(e, &world).unwrap_err().contains("exponent"));
let bad = edit(&|m| m.weights[0].steps = vec![WeightStep::Power(WEIGHT_POWER_LIMIT + 1)]);
assert!(bad.verify(e, &world).unwrap_err().contains("exponent"));
let bad = edit(&|m| m.weights[0].steps.push(WeightStep::Min(f32::NAN)));
assert!(bad.verify(e, &world).unwrap_err().contains("min bound"));
let bad = edit(&|m| m.alpha = Some(2));
assert!(bad.verify(e, &world).unwrap_err().contains("detail 2's, of 2"));
let bad = edit(&|m| m.details[0].gloss_scale = GlossScale::Constant(f32::INFINITY));
assert!(bad.verify(e, &world).unwrap_err().contains("not finite"));
let bad = edit(&|m| m.weights.clear());
assert!(bad.verify(e, &world).unwrap_err().contains("one fewer"));
let bad = edit(&|m| {
m.details.clear();
m.weights.clear();
});
assert!(bad.verify(e, &world).unwrap_err().contains("1 to 3"));
let mut four = mix(&mut world, 3);
four.details.push(four.details[0].clone());
four.weights.push(four.weights[0].clone());
let bad = DetailedMaterial {
mix: DetailMix::DetailLerp(four),
..good.clone()
};
assert!(bad.verify(e, &world).unwrap_err().contains("1 to 3"));
let dead = world.spawn();
world.despawn(dead);
let bad = edit(&|m| m.details[1].colour = TextureRef::new(dead));
assert!(bad.verify(e, &world).unwrap_err().contains("not alive"));
}
}