Skip to main content

omp_tui/scene/
material.rs

1use super::{Vec3, vec3};
2
3/// Compact physically based surface parameters in linear color space.
4///
5/// Call [`sanitized`](Self::sanitized) after changing public fields directly.
6/// The convenience constructors always return sanitized values.
7#[derive(Clone, Copy, Debug, PartialEq)]
8pub struct Material {
9	/// Diffuse albedo, metallic reflectance, or dielectric transmission tint.
10	pub base_color:   Vec3,
11	/// Surface-emitted radiance. Values above one are valid for bright emitters.
12	pub emission:     Vec3,
13	/// Microfacet roughness for non-transmissive surfaces, in `0.02..=1.0`.
14	pub roughness:    f32,
15	/// Fraction of the opaque response that is metallic, in `0..=1`.
16	pub metallic:     f32,
17	/// Fraction of energy assigned to dielectric transmission, in `0..=1`.
18	pub transmission: f32,
19	/// Index of refraction of the material interior, in `1.0001..=3.0`.
20	pub ior:          f32,
21}
22
23impl Material {
24	/// Creates an opaque Lambertian material with `color` as its linear albedo.
25	pub fn diffuse(color: Vec3) -> Self {
26		Self {
27			base_color:   color,
28			emission:     Vec3::ZERO,
29			roughness:    1.0,
30			metallic:     0.0,
31			transmission: 0.0,
32			ior:          1.5,
33		}
34		.sanitized()
35	}
36
37	/// Creates an opaque conductor with linear normal-incidence reflectance
38	/// `color` and the requested perceptual `roughness`.
39	pub fn metal(color: Vec3, roughness: f32) -> Self {
40		Self {
41			base_color: color,
42			emission: Vec3::ZERO,
43			roughness,
44			metallic: 1.0,
45			transmission: 0.0,
46			ior: 1.5,
47		}
48		.sanitized()
49	}
50
51	/// Creates an ideal specular dielectric tinted by linear `color`.
52	///
53	/// `ior` is the material's index of refraction relative to vacuum; common
54	/// glass is approximately `1.5`.
55	pub fn dielectric(color: Vec3, ior: f32) -> Self {
56		Self {
57			base_color: color,
58			emission: Vec3::ZERO,
59			roughness: 0.02,
60			metallic: 0.0,
61			transmission: 1.0,
62			ior,
63		}
64		.sanitized()
65	}
66
67	/// Creates a non-reflecting area emitter of linear `color`.
68	///
69	/// `strength` scales the emitted radiance and may be greater than one.
70	pub fn emissive(color: Vec3, strength: f32) -> Self {
71		Self {
72			base_color:   Vec3::ZERO,
73			emission:     color * finite_or(strength, 0.0).max(0.0),
74			roughness:    1.0,
75			metallic:     0.0,
76			transmission: 0.0,
77			ior:          1.5,
78		}
79		.sanitized()
80	}
81
82	/// Returns a finite, energy-bounded copy suitable for transport.
83	///
84	/// Reflectance parameters are clamped to unit range. Transmission is
85	/// reduced by the non-metallic fraction so a surface cannot spend the same
86	/// energy on both a conductor and a dielectric lobe. Emission remains HDR,
87	/// but negative and non-finite channels become zero.
88	pub fn sanitized(self) -> Self {
89		let metallic = finite_unit(self.metallic);
90		Self {
91			base_color: finite_unit_color(self.base_color),
92			emission: finite_positive_color(self.emission),
93			roughness: finite_or(self.roughness, 1.0).clamp(0.02, 1.0),
94			metallic,
95			transmission: finite_unit(self.transmission) * (1.0 - metallic),
96			ior: finite_or(self.ior, 1.5).clamp(1.0001, 3.0),
97		}
98	}
99}
100
101impl Default for Material {
102	fn default() -> Self {
103		Self::diffuse(vec3(0.8, 0.8, 0.8))
104	}
105}
106
107const fn finite_or(value: f32, fallback: f32) -> f32 {
108	if value.is_finite() { value } else { fallback }
109}
110
111const fn finite_unit(value: f32) -> f32 {
112	finite_or(value, 0.0).clamp(0.0, 1.0)
113}
114
115const fn finite_unit_color(color: Vec3) -> Vec3 {
116	vec3(finite_unit(color.x), finite_unit(color.y), finite_unit(color.z))
117}
118
119fn finite_positive_color(color: Vec3) -> Vec3 {
120	let channel = |value: f32| {
121		if value.is_finite() {
122			value.max(0.0)
123		} else {
124			0.0
125		}
126	};
127	vec3(channel(color.x), channel(color.y), channel(color.z))
128}
129
130#[cfg(test)]
131mod tests {
132	use super::*;
133
134	#[test]
135	fn sanitization_bounds_scattering_energy_and_keeps_hdr_emission() {
136		let material = Material {
137			base_color:   vec3(-1.0, 2.0, f32::NAN),
138			emission:     vec3(4.0, -2.0, f32::INFINITY),
139			roughness:    0.0,
140			metallic:     0.75,
141			transmission: 1.0,
142			ior:          99.0,
143		}
144		.sanitized();
145		assert_eq!(material.base_color, vec3(0.0, 1.0, 0.0));
146		assert_eq!(material.emission, vec3(4.0, 0.0, 0.0));
147		assert_eq!(material.roughness, 0.02);
148		assert_eq!(material.transmission, 0.25);
149		assert_eq!(material.ior, 3.0);
150	}
151}