1use serde::Serialize;
17use serde_json::Value;
18
19use crate::layered::{Source, check_source, mask_value};
20use crate::pbr::TextureRef;
21use crate::shader::ScalarEncoding;
22use crate::{Component, Entity, JsonError, World};
23
24#[derive(Clone, Debug, PartialEq, Serialize)]
26pub struct DetailedMaterial {
27 pub mix: DetailMix,
28 pub lowering: SurfaceLowering,
30 pub frozen: Vec<Frozen>,
34}
35
36#[derive(Clone, Debug, PartialEq, Serialize)]
39#[serde(rename_all = "snake_case")]
40pub enum Frozen {
41 FrontFacing,
43 VertexInput {
46 semantic: String,
47 index: u32,
48 value: [f32; 4],
49 },
50 EngineConstant { name: String, value: Vec<f32> },
53 EngineTexture { name: String, value: [f32; 4] },
55}
56
57impl Frozen {
58 fn finite(&self) -> bool {
60 match self {
61 Frozen::FrontFacing => true,
62 Frozen::VertexInput { value, .. } | Frozen::EngineTexture { value, .. } => {
63 value.iter().all(|v| v.is_finite())
64 }
65 Frozen::EngineConstant { value, .. } => value.iter().all(|v| v.is_finite()),
66 }
67 }
68}
69
70#[derive(Clone, Debug, PartialEq, Serialize)]
72#[serde(rename_all = "snake_case")]
73pub enum DetailMix {
74 DetailLerp(DetailLerp),
75}
76
77pub const REVISION: u32 = 4;
81
82pub const DETAIL_LIMIT: usize = 3;
86
87pub const LUMA: [f32; 3] = [0.29891, 0.58661, 0.11448];
90
91#[derive(Clone, Debug, PartialEq, Serialize)]
111pub struct DetailLerp {
112 pub base_colour: TextureRef,
113 pub base_normal: TextureRef,
114 pub base_mask: TextureRef,
115 pub base_saturation: f32,
118 pub details: Vec<Detail>,
120 pub weights: Vec<Weight>,
122 pub gain: f32,
123 pub normal_strength: f32,
124 pub colour: [f32; 3],
126 pub gloss_encoding: ScalarEncoding,
128 pub alpha: Option<usize>,
132}
133
134pub const WEIGHT_POWER_LIMIT: u16 = 512;
136
137#[derive(Clone, Debug, PartialEq, Serialize)]
139pub struct Weight {
140 pub source: Source,
141 pub steps: Vec<WeightStep>,
142}
143
144#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
146#[serde(rename_all = "snake_case")]
147pub enum WeightStep {
148 Power(u16),
150 Min(f32),
152 Max(f32),
155 Affine { scale: f32, bias: f32 },
157 Saturate,
159}
160
161pub fn weight_power(x: f32, exponent: u16) -> f32 {
165 let mut square = x;
166 let mut out: Option<f32> = None;
167 for bit in 0..16 {
168 if exponent >> bit == 0 {
169 break;
170 }
171 if exponent & (1 << bit) != 0 {
172 out = Some(out.map_or(square, |acc| acc * square));
173 }
174 square *= square;
175 }
176 out.unwrap_or(1.0)
177}
178
179pub fn weight_steps(v: f32, steps: &[WeightStep]) -> f32 {
181 steps.iter().fold(v, |x, step| match *step {
182 WeightStep::Power(exponent) => weight_power(x, exponent),
183 WeightStep::Min(bound) => x.min(bound),
184 WeightStep::Max(bound) => x.max(bound),
185 WeightStep::Affine { scale, bias } => x * scale + bias,
186 WeightStep::Saturate => {
188 if x.is_nan() {
189 0.0
190 } else {
191 x.clamp(0.0, 1.0)
192 }
193 }
194 })
195}
196
197impl Weight {
198 pub fn evaluate(&self, v: f32) -> f32 {
200 weight_steps(v, &self.steps)
201 }
202
203 pub fn of_sample(&self, sample: [f32; 4]) -> Option<f32> {
205 match self.source {
206 Source::Mask { channels, bias, .. } => Some(self.evaluate(mask_value(channels, bias, sample))),
207 Source::Vertex { .. } => None,
208 }
209 }
210
211 pub fn check(&self) -> Result<(), String> {
213 for step in &self.steps {
214 match *step {
215 WeightStep::Power(e) if !(1..=WEIGHT_POWER_LIMIT).contains(&e) => {
216 return Err(format!("power exponent {e} is not 1 to {WEIGHT_POWER_LIMIT}"));
217 }
218 WeightStep::Min(bound) if !bound.is_finite() => {
219 return Err(format!("min bound {bound} is not finite"));
220 }
221 WeightStep::Max(bound) if !bound.is_finite() => {
222 return Err(format!("max bound {bound} is not finite"));
223 }
224 WeightStep::Affine { scale, bias } if !(scale.is_finite() && bias.is_finite()) => {
225 return Err(format!("affine step {scale} * x + {bias} is not finite"));
226 }
227 _ => {}
228 }
229 }
230 Ok(())
231 }
232}
233
234#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
236#[serde(rename_all = "snake_case")]
237pub enum GlossScale {
238 BaseNormalZ,
240 Constant(f32),
242}
243
244impl GlossScale {
245 pub fn value(self, normal: [f32; 4]) -> f32 {
247 match self {
248 GlossScale::BaseNormalZ => normal[2],
249 GlossScale::Constant(c) => c,
250 }
251 }
252}
253
254#[derive(Clone, Debug, PartialEq, Serialize)]
256pub struct Detail {
257 pub colour: TextureRef,
258 pub normal: TextureRef,
259 pub tint: [f32; 3],
261 pub gloss_bias: f32,
263 pub gloss_scale: GlossScale,
265 pub reflectance: ReflectanceTerm,
267}
268
269#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
272#[serde(rename_all = "snake_case")]
273pub enum ReflectanceTerm {
274 MaskPlusBias(f32),
276 Bias(f32),
278}
279
280impl ReflectanceTerm {
281 pub fn value(self, mask: [f32; 4]) -> f32 {
283 match self {
284 ReflectanceTerm::MaskPlusBias(bias) => mask[0] + bias,
285 ReflectanceTerm::Bias(bias) => bias,
286 }
287 }
288
289 pub fn bias(self) -> f32 {
291 match self {
292 ReflectanceTerm::MaskPlusBias(bias) | ReflectanceTerm::Bias(bias) => bias,
293 }
294 }
295}
296
297#[derive(Clone, Debug, PartialEq)]
299pub struct DetailSamples {
300 pub base_colour: [f32; 4],
301 pub base_normal: [f32; 4],
302 pub base_mask: [f32; 4],
303 pub details: Vec<DetailSample>,
305}
306
307#[derive(Clone, Copy, Debug, PartialEq)]
309pub struct DetailSample {
310 pub colour: [f32; 4],
311 pub normal: [f32; 4],
312}
313
314#[derive(Clone, Copy, Debug, PartialEq)]
317pub struct DetailSurface {
318 pub c: [f32; 3],
320 pub colour: [f32; 3],
322 pub mix: f32,
324 pub alpha: f32,
325 pub normal: [f32; 3],
327 pub gloss: f32,
329}
330
331#[derive(Clone, Copy, Debug, PartialEq)]
333pub struct Written {
334 pub albedo: [f32; 3],
336 pub reflectance: Option<[f32; 3]>,
338 pub metallic: Option<f32>,
340}
341
342#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
346#[serde(rename_all = "snake_case")]
347pub enum SurfaceLowering {
348 GlossReflectance {
353 dielectric_f0: f32,
355 },
356 MetallicGloss,
360}
361
362#[derive(Clone, Copy, Debug, PartialEq)]
364pub struct PbrPixel {
365 pub base_color: [f32; 4],
367 pub normal: [f32; 3],
369 pub roughness: f32,
370 pub metallic: f32,
371 pub specular_color: Option<[f32; 3]>,
374}
375
376pub const SPECULAR_COLOR_FACTOR: f32 = 25.0;
379
380fn lerp(a: f32, b: f32, t: f32) -> f32 {
381 a + (b - a) * t
382}
383
384pub fn chain(x: impl IntoIterator<Item = f32>, weights: &[f32]) -> f32 {
386 let mut x = x.into_iter();
387 let first = x.next().unwrap_or(0.0);
388 x.zip(weights).fold(first, |acc, (next, &w)| lerp(acc, next, w))
389}
390
391impl DetailLerp {
392 pub fn textures(&self) -> Vec<&TextureRef> {
394 let mut out = vec![&self.base_colour, &self.base_normal, &self.base_mask];
395 for d in &self.details {
396 out.extend([&d.colour, &d.normal]);
397 }
398 out.extend(self.weights.iter().filter_map(|w| match &w.source {
399 Source::Mask { texture, .. } => Some(texture),
400 Source::Vertex { .. } => None,
401 }));
402 out
403 }
404
405 pub fn weight_values(&self, sources: &[f32]) -> Vec<f32> {
407 self.weights
408 .iter()
409 .zip(sources)
410 .map(|(w, &v)| w.evaluate(v))
411 .collect()
412 }
413
414 pub fn evaluate(&self, s: &DetailSamples, weights: &[f32]) -> DetailSurface {
416 let (a, n, m) = (s.base_colour, s.base_normal, s.base_mask);
417 let pairs = || self.details.iter().zip(&s.details);
418 let base: [f32; 3] = if self.base_saturation == 0.0 {
419 [a[0], a[1], a[2]]
420 } else {
421 let luma = a[0] * LUMA[0] + a[1] * LUMA[1] + a[2] * LUMA[2];
422 std::array::from_fn(|i| lerp(a[i], luma, self.base_saturation))
423 };
424 let c: [f32; 3] = std::array::from_fn(|i| {
425 let detail = chain(pairs().map(|(d, x)| d.tint[i] * x.colour[i]), weights);
426 (self.gain * base[i] * detail).clamp(0.0, 1.0)
427 });
428 let f = chain(self.details.iter().map(|d| d.reflectance.value(m)), weights).clamp(0.0, 1.0);
429 let base = [2.0 * n[0] - 1.0, 2.0 * n[1] - 1.0];
430 let z = (1.0 - (base[0] * base[0] + base[1] * base[1]).min(1.0)).sqrt();
431 let detail = |i: usize| 2.0 * chain(s.details.iter().map(|x| x.normal[i]), weights) - 1.0;
432 let normal = [
433 base[0] + self.normal_strength * detail(0),
434 base[1] + self.normal_strength * detail(1),
435 z,
436 ];
437 let scale = chain(self.details.iter().map(|d| d.gloss_scale.value(n)), weights);
438 let stored =
439 (2.0 * scale * chain(pairs().map(|(d, x)| x.normal[2] + d.gloss_bias), weights)).clamp(0.0, 1.0);
440 let gloss = self.gloss_encoding.decode(stored);
441 let alpha = self
442 .alpha
443 .and_then(|j| s.details.get(j))
444 .map_or(1.0, |x| (x.colour[3] * a[3]).clamp(0.0, 1.0));
445 DetailSurface {
446 c,
447 colour: self.colour,
448 mix: f,
449 alpha,
450 normal,
451 gloss,
452 }
453 }
454
455 fn constants_finite(&self) -> bool {
457 let scale = |d: &Detail| match d.gloss_scale {
458 GlossScale::Constant(c) => c,
459 GlossScale::BaseNormalZ => 0.0,
460 };
461 self.details
462 .iter()
463 .flat_map(|d| {
464 d.tint
465 .into_iter()
466 .chain([d.gloss_bias, d.reflectance.bias(), scale(d)])
467 })
468 .chain(self.colour)
469 .chain([self.gain, self.normal_strength, self.base_saturation])
470 .all(f32::is_finite)
471 }
472
473 fn check(&self) -> Result<(), String> {
476 if !(1..=DETAIL_LIMIT).contains(&self.details.len()) {
477 return Err(format!(
478 "{} details: a detail lerp chains 1 to {DETAIL_LIMIT}",
479 self.details.len()
480 ));
481 }
482 if self.weights.len() + 1 != self.details.len() {
483 return Err(format!(
484 "{} weights for {} details: one fewer than the details",
485 self.weights.len(),
486 self.details.len()
487 ));
488 }
489 if !self.constants_finite() {
490 return Err("a detail mix constant is not finite".into());
491 }
492 for w in &self.weights {
493 w.check().map_err(|e| format!("weight: {e}"))?;
494 }
495 if let Some(j) = self.alpha
496 && j >= self.details.len()
497 {
498 return Err(format!(
499 "the alpha is detail {j}'s, of {} details",
500 self.details.len()
501 ));
502 }
503 self.gloss_encoding
504 .check()
505 .map_err(|e| format!("gloss encoding: {e}"))
506 }
507}
508
509impl DetailMix {
510 pub fn textures(&self) -> Vec<&TextureRef> {
512 match self {
513 DetailMix::DetailLerp(mix) => mix.textures(),
514 }
515 }
516}
517
518impl SurfaceLowering {
519 pub fn written(self, surface: &DetailSurface) -> Written {
522 let (c, f) = (surface.c, surface.mix);
523 match self {
524 SurfaceLowering::GlossReflectance { dielectric_f0 } => Written {
525 albedo: std::array::from_fn(|i| (1.0 - f) * c[i] * surface.colour[i]),
526 reflectance: Some(c.map(|c| lerp(dielectric_f0, c, f))),
527 metallic: None,
528 },
529 SurfaceLowering::MetallicGloss => Written {
530 albedo: std::array::from_fn(|i| c[i] * surface.colour[i]),
531 reflectance: None,
532 metallic: Some(f),
533 },
534 }
535 }
536
537 pub fn evaluate(self, surface: &DetailSurface) -> PbrPixel {
539 let written = self.written(surface);
540 let [r, g, b] = written.albedo;
541 let n = surface.normal;
542 let length = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
543 PbrPixel {
544 base_color: [r, g, b, surface.alpha],
545 normal: n.map(|x| x / length),
546 roughness: 1.0 - surface.gloss,
547 metallic: written.metallic.unwrap_or(0.0),
548 specular_color: written.reflectance,
549 }
550 }
551
552 fn check(self) -> Result<(), String> {
554 match self {
555 SurfaceLowering::GlossReflectance { dielectric_f0 } if !dielectric_f0.is_finite() => {
556 Err(format!("dielectric reflectance {dielectric_f0} is not finite"))
557 }
558 _ => Ok(()),
559 }
560 }
561}
562
563impl Component for DetailedMaterial {
564 const NAME: &'static str = "pbr.detail";
565 const GEOMETRY: crate::Geometry = crate::Geometry::None;
566 const VERSION: u32 = REVISION;
567 fn to_json(&self) -> Result<Option<Value>, JsonError> {
568 crate::json::to_value(self).map(Some)
569 }
570 fn verify(&self, _e: Entity, world: &World) -> Result<(), String> {
573 if let Some(f) = self.frozen.iter().find(|f| !f.finite()) {
574 return Err(format!("frozen {f:?} is not finite"));
575 }
576 self.lowering.check()?;
577 match &self.mix {
578 DetailMix::DetailLerp(mix) => {
579 mix.check()?;
580 for w in &mix.weights {
581 check_source(world, "weight", &w.source)?;
582 }
583 }
584 }
585 for t in self.mix.textures() {
586 if !world.is_alive(t.texture) {
587 return Err(format!("texture {:?} is not alive", t.texture));
588 }
589 if let Some(x) = t.transform
590 && !x
591 .offset
592 .iter()
593 .chain(&x.scale)
594 .chain([&x.rotation])
595 .all(|v| v.is_finite())
596 {
597 return Err(format!("texture transform {x:?} is not finite"));
598 }
599 }
600 Ok(())
601 }
602 fn references(&self, out: &mut Vec<Entity>) {
603 out.extend(self.mix.textures().into_iter().map(|t| t.texture));
604 }
605}
606
607#[cfg(test)]
608mod tests {
609 use super::*;
610 use crate::layered::{Channel, power};
611
612 fn weight(texture: TextureRef, flip: bool) -> Weight {
614 Weight {
615 source: Source::Mask {
616 texture,
617 channels: [0., 0., 0., if flip { 1. } else { -1. }],
618 bias: if flip { 0. } else { 1. },
619 },
620 steps: vec![WeightStep::Power(8)],
621 }
622 }
623
624 fn mix(world: &mut World, n: usize) -> DetailLerp {
627 let mut t = || TextureRef::new(world.spawn());
628 let (base_colour, base_normal, base_mask) = (t(), t(), t());
629 let tints = [[1., 0.5, 0.25], [0.5, 1., 1.], [0.25, 0.75, 2.]];
630 let reflectance = [
631 ReflectanceTerm::MaskPlusBias(0.125),
632 ReflectanceTerm::Bias(0.75),
633 ReflectanceTerm::Bias(0.375),
634 ];
635 let details = (0..n)
636 .map(|i| Detail {
637 colour: t(),
638 normal: t(),
639 tint: tints[i],
640 gloss_bias: [0.25, -0.25, 0.5][i],
641 gloss_scale: GlossScale::BaseNormalZ,
642 reflectance: reflectance[i],
643 })
644 .collect();
645 DetailLerp {
646 weights: [weight(base_normal, false), weight(base_normal, true)][..n - 1].to_vec(),
647 base_colour,
648 base_normal,
649 base_mask,
650 base_saturation: 0.,
651 details,
652 gain: 4.55,
653 normal_strength: 0.5,
654 colour: [1., 1., 0.5],
655 gloss_encoding: ScalarEncoding::LINEAR,
656 alpha: Some(0),
657 }
658 }
659
660 fn samples(n: usize) -> DetailSamples {
661 let details = [
662 ([0.5, 0.5, 0.5, 1.], [0.5, 0.5, 0.75, 0.]),
663 ([1., 0.25, 0.5, 0.], [1., 0., 0.25, 0.]),
664 ([0.25, 0.75, 0.125, 0.5], [0.25, 0.75, 0.5, 0.]),
665 ];
666 DetailSamples {
667 base_colour: [0.1, 0.2, 0.05, 0.5],
668 base_normal: [0.75, 0.25, 0.5, 0.75],
669 base_mask: [0.5, 0., 0., 0.],
670 details: details[..n]
671 .iter()
672 .map(|&(colour, normal)| DetailSample { colour, normal })
673 .collect(),
674 }
675 }
676
677 #[test]
680 fn details_are_lerped_in_turn_over_the_base() {
681 let mut world = World::new();
682 for n in 1..=3 {
683 let mix = mix(&mut world, n);
684 let s = samples(n);
685 let a = s.base_normal[3];
686 let sources = [1. - a, a];
687 let weights = mix.weight_values(&sources[..n - 1]);
688 if n >= 2 {
689 assert_eq!(weights[0].to_bits(), power(1. - a, 8).to_bits());
690 assert_eq!(
691 weights[0].to_bits(),
692 (((0.25f32 * 0.25) * (0.25 * 0.25)) * ((0.25 * 0.25) * (0.25 * 0.25))).to_bits()
693 );
694 }
695 if n == 3 {
696 assert_eq!(weights[1].to_bits(), power(a, 8).to_bits());
697 }
698 let by_hand = |x: &[f32]| -> f32 {
700 let mut acc = x[0];
701 for (next, w) in x[1..].iter().zip(&weights) {
702 acc = acc + (next - acc) * w;
703 }
704 acc
705 };
706 let out = mix.evaluate(&s, &weights);
707 let written = SurfaceLowering::GlossReflectance { dielectric_f0: 0.03 }.written(&out);
708 let c: [f32; 3] = std::array::from_fn(|i| {
709 let x: Vec<f32> = (0..n)
710 .map(|j| mix.details[j].tint[i] * s.details[j].colour[i])
711 .collect();
712 (mix.gain * s.base_colour[i] * by_hand(&x)).clamp(0., 1.)
713 });
714 let terms: Vec<f32> = [0.5 + 0.125, 0.75, 0.375][..n].to_vec();
715 let f = by_hand(&terms).clamp(0., 1.);
716 assert_eq!((out.c, out.mix, out.colour), (c, f, mix.colour), "{n} details");
717 for (i, c) in c.iter().enumerate() {
718 assert_eq!(written.albedo[i], (1. - f) * c * mix.colour[i], "{n} details");
719 assert_eq!(
720 written.reflectance.map(|r| r[i]),
721 Some(0.03 + (c - 0.03) * f),
722 "{n} details"
723 );
724 assert_eq!(written.metallic, None);
725 }
726 let k = |i: usize| {
727 let x: Vec<f32> = s.details.iter().map(|d| d.normal[i]).collect();
728 2. * by_hand(&x) - 1.
729 };
730 assert_eq!(
731 out.normal,
732 [0.5 + 0.5 * k(0), -0.5 + 0.5 * k(1), (1f32 - 0.5).sqrt()],
733 "{n} details"
734 );
735 let g: Vec<f32> = (0..n)
736 .map(|j| s.details[j].normal[2] + mix.details[j].gloss_bias)
737 .collect();
738 assert_eq!(out.gloss, (2. * 0.5 * by_hand(&g)).clamp(0., 1.), "{n} details");
739 assert_eq!(out.alpha, 0.5);
740 }
741 let one = mix(&mut world, 1);
743 let s = samples(1);
744 let out = one.evaluate(&s, &[]);
745 let f = 0.5 + 0.125;
746 let written = SurfaceLowering::GlossReflectance { dielectric_f0: 0.03 }.written(&out);
747 assert_eq!(
748 written.reflectance.map(|r| r[0]),
749 Some(0.03 + ((4.55 * 0.1 * (1. * 0.5)) - 0.03) * f)
750 );
751 }
752
753 #[test]
756 fn the_base_colour_is_desaturated_toward_its_luma() {
757 let mut world = World::new();
758 let mut one = mix(&mut world, 1);
759 let s = samples(1);
760 let plain = one.evaluate(&s, &[]);
761 let a = s.base_colour;
762 let (gain, tint) = (one.gain, one.details[0].tint);
763 let by_hand = |saturation: f32| -> [f32; 3] {
764 let luma = a[0] * LUMA[0] + a[1] * LUMA[1] + a[2] * LUMA[2];
765 std::array::from_fn(|i| {
766 let base = a[i] + (luma - a[i]) * saturation;
767 (gain * base * (tint[i] * s.details[0].colour[i])).clamp(0., 1.)
768 })
769 };
770 let none: [f32; 3] =
771 std::array::from_fn(|i| (gain * a[i] * (tint[i] * s.details[0].colour[i])).clamp(0., 1.));
772 assert_eq!(plain.c, none);
773 for saturation in [0.25, 1.] {
774 one.base_saturation = saturation;
775 assert_eq!(one.evaluate(&s, &[]).c, by_hand(saturation), "{saturation}");
776 }
777 assert!((LUMA.iter().sum::<f32>() - 1.).abs() < 1e-6);
778 let mut grey = samples(1);
780 grey.base_colour = [0.25, 0.25, 0.25, 1.];
781 let desaturated = one.evaluate(&grey, &[]);
782 one.base_saturation = 0.;
783 let kept = one.evaluate(&grey, &[]);
784 for i in 0..3 {
785 assert!((kept.c[i] - desaturated.c[i]).abs() <= 1e-6, "lane {i}");
786 }
787 }
788
789 #[test]
792 fn the_ramp_steps_are_taken_in_turn() {
793 let ramp = [
794 WeightStep::Affine {
795 scale: 2.,
796 bias: -0.5,
797 },
798 WeightStep::Saturate,
799 ];
800 assert_eq!(weight_steps(0.375, &ramp), 0.25);
801 assert_eq!(weight_steps(0.125, &ramp), 0.);
802 assert_eq!(weight_steps(1., &ramp), 1.);
803 assert_eq!(weight_steps(f32::NAN, &ramp), 0.);
804 assert_eq!(
805 weight_steps(-1., &[WeightStep::Max(0.), WeightStep::Power(8)]),
806 0.
807 );
808 assert_eq!(weight_steps(f32::NAN, &[WeightStep::Max(0.)]), 0.);
809 assert_eq!(weight_steps(0.5, &[WeightStep::Max(0.75)]), 0.75);
810 assert_eq!(
812 weight_steps(
813 0.5,
814 &[
815 WeightStep::Affine {
816 scale: 1.,
817 bias: 0.25
818 },
819 WeightStep::Power(2)
820 ]
821 ),
822 0.5625
823 );
824 let mut world = World::new();
825 let mut weight = |steps| Weight {
826 source: Source::Mask {
827 texture: TextureRef::new(world.spawn()),
828 channels: [0., 0., 0., 1.],
829 bias: 0.,
830 },
831 steps,
832 };
833 assert_eq!(weight(ramp.to_vec()).check(), Ok(()));
834 assert!(
835 weight(vec![WeightStep::Affine {
836 scale: f32::NAN,
837 bias: 0.
838 }])
839 .check()
840 .unwrap_err()
841 .contains("affine")
842 );
843 assert!(
844 weight(vec![WeightStep::Max(f32::INFINITY)])
845 .check()
846 .unwrap_err()
847 .contains("max bound")
848 );
849 assert_eq!(weight(vec![WeightStep::Saturate]).check(), Ok(()));
850 }
851
852 #[test]
855 fn the_gloss_is_decoded_by_its_encoding() {
856 let mut world = World::new();
857 let linear = mix(&mut world, 2);
858 let encoding = ScalarEncoding {
859 power: 1,
860 root: 2,
861 scale_bits: 0.95f32.to_bits(),
862 };
863 let encoded = DetailLerp {
864 gloss_encoding: encoding,
865 ..linear.clone()
866 };
867 let mut s = samples(2);
868 s.base_normal = [0.5, 0.5, 0.25, 0.75];
869 s.details[1].normal = [0.5, 0.5, 0.5, 0.];
870 let weights = linear.weight_values(&[1. - s.base_normal[3]]);
871 let stored = linear.evaluate(&s, &weights).gloss;
872 assert!(stored > 0. && stored < 1.);
873 let gloss = encoded.evaluate(&s, &weights).gloss;
874 assert_eq!(gloss, encoding.decode(stored));
875 assert_eq!(gloss, stored.sqrt() * 0.95);
876 let e = world.spawn();
877 let bad = DetailedMaterial {
878 mix: DetailMix::DetailLerp(DetailLerp {
879 gloss_encoding: ScalarEncoding { power: 0, ..encoding },
880 ..linear
881 }),
882 lowering: SurfaceLowering::GlossReflectance { dielectric_f0: 0.03 },
883 frozen: Vec::new(),
884 };
885 assert!(bad.verify(e, &world).unwrap_err().contains("gloss encoding"));
886 }
887
888 #[test]
891 fn a_power_is_taken_in_its_stated_order_and_bounded() {
892 let x = 0.737_f32;
893 assert_eq!(power(x, 1), x);
894 assert_eq!(power(x, 8).to_bits(), {
895 let a = x * x;
896 let b = a * a;
897 (b * b).to_bits()
898 });
899 assert_eq!(power(x, 3).to_bits(), ((x * x) * x).to_bits());
900 let channel = |exponent| Channel::Power {
901 v: Source::Vertex {
902 semantic: crate::Semantic::Color(0),
903 channel: 0,
904 },
905 exponent,
906 };
907 assert!(channel(0).check().is_err());
908 assert!(channel(crate::layered::POWER_LIMIT + 1).check().is_err());
909 assert!(channel(16).check().is_ok());
910 assert_eq!(channel(2).evaluate(&[0.5]), 0.25);
911 }
912
913 #[test]
917 fn a_weights_steps_are_taken_in_the_programs_order() {
918 let x = 0.987_654_3_f32;
919 let squares: Vec<f32> = std::iter::successors(Some(x), |s| Some(s * s)).take(9).collect();
920 assert_eq!(
921 weight_power(x, 260).to_bits(),
922 (squares[2] * squares[8]).to_bits()
923 );
924 assert_eq!(weight_power(x, 8).to_bits(), power(x, 8).to_bits());
925 assert_eq!(weight_power(x, 1), x);
926 assert_eq!(
927 weight_power(x, 7).to_bits(),
928 ((squares[0] * squares[1]) * squares[2]).to_bits()
929 );
930 let mut world = World::new();
931 let switch_one = Weight {
932 source: Source::Mask {
933 texture: TextureRef::new(world.spawn()),
934 channels: [0., 0., 22., 0.],
935 bias: 0.,
936 },
937 steps: vec![WeightStep::Power(260), WeightStep::Min(1.)],
938 };
939 assert_eq!(switch_one.check(), Ok(()));
940 assert_eq!(switch_one.of_sample([0.5, 0.5, 0., 0.5]), Some(0.));
942 assert_eq!(switch_one.of_sample([0.5, 0.5, 1. / 11., 0.5]), Some(1.));
943 let z = 0.045_f32;
944 let v = 22. * z;
945 let squares: Vec<f32> = std::iter::successors(Some(v), |s| Some(s * s)).take(9).collect();
946 assert_eq!(
947 switch_one.of_sample([0.5, 0.5, z, 0.5]).unwrap().to_bits(),
948 (squares[2] * squares[8]).min(1.).to_bits()
949 );
950 assert_eq!(switch_one.evaluate(f32::NAN), 1.);
951 let old = Channel::Power {
953 v: Source::Vertex {
954 semantic: crate::Semantic::Color(0),
955 channel: 0,
956 },
957 exponent: 8,
958 };
959 let new = weight(TextureRef::new(world.spawn()), true);
960 for a in [0.1_f32, 0.37, 0.5, 0.93] {
961 assert_eq!(old.evaluate(&[a]).to_bits(), new.evaluate(a).to_bits());
962 }
963 }
964
965 #[test]
968 fn each_detail_scales_its_gloss_as_it_states() {
969 let mut world = World::new();
970 let mut two = mix(&mut world, 2);
971 let s = samples(2);
972 let n = s.base_normal;
973 let w = 0.25_f32;
974 let base = two.evaluate(&s, &[w]).gloss;
975 let terms: Vec<f32> = (0..2)
976 .map(|j| s.details[j].normal[2] + two.details[j].gloss_bias)
977 .collect();
978 let g = terms[0] + (terms[1] - terms[0]) * w;
979 assert_eq!(base, (2. * n[2] * g).clamp(0., 1.));
980 two.details[0].gloss_scale = GlossScale::Constant(0.5);
981 let scale = 0.5 + (n[2] - 0.5) * w;
982 assert_eq!(two.evaluate(&s, &[w]).gloss, (2. * scale * g).clamp(0., 1.));
983 }
984
985 #[test]
987 fn the_alpha_is_the_stated_details() {
988 let mut world = World::new();
989 let mut two = mix(&mut world, 2);
990 let s = samples(2);
991 two.alpha = Some(1);
992 assert_eq!(two.evaluate(&s, &[0.5]).alpha, 0.);
993 two.alpha = Some(0);
994 assert_eq!(two.evaluate(&s, &[0.5]).alpha, 0.5);
995 two.alpha = None;
996 assert_eq!(two.evaluate(&s, &[0.5]).alpha, 1.);
997 }
998
999 #[test]
1002 fn a_surface_lowers_to_the_pbr_model_as_its_g_buffer_holds_it() {
1003 let surface = DetailSurface {
1004 c: [0.25, 0.5, 0.75],
1005 colour: [1., 1., 0.5],
1006 mix: 0.5,
1007 alpha: 0.5,
1008 normal: [0., 3., 4.],
1009 gloss: 0.75,
1010 };
1011 let f0 = SurfaceLowering::GlossReflectance { dielectric_f0: 0. };
1012 let written = f0.written(&surface);
1013 assert_eq!(written.albedo, [0.125, 0.25, 0.1875]);
1014 assert_eq!(written.reflectance, Some([0.125, 0.25, 0.375]));
1015 assert_eq!(written.metallic, None);
1016 let pixel = f0.evaluate(&surface);
1017 assert_eq!(pixel.base_color, [0.125, 0.25, 0.1875, 0.5]);
1018 assert_eq!(pixel.normal, [0., 0.6, 0.8]);
1019 assert_eq!((pixel.roughness, pixel.metallic), (0.25, 0.));
1020 assert_eq!(pixel.specular_color, Some([0.125, 0.25, 0.375]));
1021 assert_eq!(0.04 * SPECULAR_COLOR_FACTOR, 1.);
1022 let metal = SurfaceLowering::MetallicGloss;
1023 let written = metal.written(&surface);
1024 assert_eq!(written.albedo, [0.25, 0.5, 0.375]);
1025 assert_eq!((written.reflectance, written.metallic), (None, Some(0.5)));
1026 let pixel = metal.evaluate(&surface);
1027 assert_eq!(pixel.base_color, [0.25, 0.5, 0.375, 0.5]);
1028 assert_eq!(pixel.normal, [0., 0.6, 0.8]);
1029 assert_eq!(
1030 (pixel.roughness, pixel.metallic, pixel.specular_color),
1031 (0.25, 0.5, None)
1032 );
1033 let mut world = World::new();
1035 let e = world.spawn();
1036 let bad = DetailedMaterial {
1037 mix: DetailMix::DetailLerp(mix(&mut world, 1)),
1038 lowering: SurfaceLowering::GlossReflectance {
1039 dielectric_f0: f32::NAN,
1040 },
1041 frozen: Vec::new(),
1042 };
1043 assert!(bad.verify(e, &world).unwrap_err().contains("dielectric"));
1044 }
1045
1046 #[test]
1049 fn a_detail_material_verifies_its_textures_constants_and_weights() {
1050 let mut world = World::new();
1051 let good = DetailedMaterial {
1052 mix: DetailMix::DetailLerp(mix(&mut world, 2)),
1053 lowering: SurfaceLowering::GlossReflectance { dielectric_f0: 0.03 },
1054 frozen: vec![
1055 Frozen::FrontFacing,
1056 Frozen::EngineConstant {
1057 name: "SomeEngineConstant".into(),
1058 value: vec![0.],
1059 },
1060 ],
1061 };
1062 let e = world.spawn();
1063 assert_eq!(good.verify(e, &world), Ok(()));
1064 let mut refs = Vec::new();
1065 good.references(&mut refs);
1066 assert_eq!(refs.len(), 8, "seven textures and the weight's");
1067 let edit = |f: &dyn Fn(&mut DetailLerp)| {
1068 let mut bad = good.clone();
1069 let DetailMix::DetailLerp(m) = &mut bad.mix;
1070 f(m);
1071 bad
1072 };
1073 let bad = edit(&|m| m.gain = f32::NAN);
1074 assert!(bad.verify(e, &world).unwrap_err().contains("not finite"));
1075 let bad = edit(&|m| m.base_saturation = f32::INFINITY);
1076 assert!(bad.verify(e, &world).unwrap_err().contains("not finite"));
1077 let mut bad = good.clone();
1078 bad.frozen.push(Frozen::EngineTexture {
1079 name: "g_DecalTexture".into(),
1080 value: [f32::INFINITY; 4],
1081 });
1082 assert!(bad.verify(e, &world).unwrap_err().contains("frozen"));
1083 let bad = edit(&|m| m.details[1].reflectance = ReflectanceTerm::Bias(f32::INFINITY));
1084 assert!(bad.verify(e, &world).unwrap_err().contains("not finite"));
1085 let bad = edit(&|m| m.weights[0].steps = vec![WeightStep::Power(0)]);
1086 assert!(bad.verify(e, &world).unwrap_err().contains("exponent"));
1087 let bad = edit(&|m| m.weights[0].steps = vec![WeightStep::Power(WEIGHT_POWER_LIMIT + 1)]);
1088 assert!(bad.verify(e, &world).unwrap_err().contains("exponent"));
1089 let bad = edit(&|m| m.weights[0].steps.push(WeightStep::Min(f32::NAN)));
1090 assert!(bad.verify(e, &world).unwrap_err().contains("min bound"));
1091 let bad = edit(&|m| m.alpha = Some(2));
1092 assert!(bad.verify(e, &world).unwrap_err().contains("detail 2's, of 2"));
1093 let bad = edit(&|m| m.details[0].gloss_scale = GlossScale::Constant(f32::INFINITY));
1094 assert!(bad.verify(e, &world).unwrap_err().contains("not finite"));
1095 let bad = edit(&|m| m.weights.clear());
1096 assert!(bad.verify(e, &world).unwrap_err().contains("one fewer"));
1097 let bad = edit(&|m| {
1098 m.details.clear();
1099 m.weights.clear();
1100 });
1101 assert!(bad.verify(e, &world).unwrap_err().contains("1 to 3"));
1102 let mut four = mix(&mut world, 3);
1103 four.details.push(four.details[0].clone());
1104 four.weights.push(four.weights[0].clone());
1105 let bad = DetailedMaterial {
1106 mix: DetailMix::DetailLerp(four),
1107 ..good.clone()
1108 };
1109 assert!(bad.verify(e, &world).unwrap_err().contains("1 to 3"));
1110 let dead = world.spawn();
1111 world.despawn(dead);
1112 let bad = edit(&|m| m.details[1].colour = TextureRef::new(dead));
1113 assert!(bad.verify(e, &world).unwrap_err().contains("not alive"));
1114 }
1115}