use std::f32::consts::PI;
use crate::scene::Vec3;
mod dispersion;
pub(super) use dispersion::dispersion_light_contribution;
mod transmission;
pub(super) use transmission::transmission_volume_light_contribution;
pub(super) const DIELECTRIC_F0: f32 = 0.04;
pub(super) const MIN_ROUGHNESS: f32 = 0.04;
const MIN_DENOMINATOR: f32 = 0.0001;
const MIN_N_DOT_V: f32 = 0.001;
const DIRECTIONAL_LUX_TO_SCENE_RADIANCE: f32 = 1.0 / 10_000.0;
#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) struct PbrMaterial {
pub(super) base: Vec3,
pub(super) metallic: f32,
pub(super) roughness: f32,
}
impl PbrMaterial {
pub(super) fn new(base: Vec3, metallic: f32, roughness: f32) -> Self {
Self {
base,
metallic: clamp_unit(metallic),
roughness: roughness_or_min(roughness),
}
}
pub(super) fn f0(self) -> Vec3 {
mix_vec3(
Vec3::new(DIELECTRIC_F0, DIELECTRIC_F0, DIELECTRIC_F0),
self.base,
self.metallic,
)
}
}
pub(super) fn roughness_or_min(value: f32) -> f32 {
if value.is_finite() {
value.clamp(MIN_ROUGHNESS, 1.0)
} else {
1.0
}
}
pub(super) fn punctual_light_contribution(
material: PbrMaterial,
normal: Vec3,
view: Vec3,
incoming: Vec3,
radiance: Vec3,
) -> Vec3 {
let incoming = normalize_or(incoming, Vec3::ZERO);
let n_dot_l = dot_vec3(normal, incoming).max(0.0);
if n_dot_l <= f32::EPSILON {
return Vec3::ZERO;
}
let n_dot_v = dot_vec3(normal, view).max(MIN_N_DOT_V);
let half_vector = normalize_or(add_vec3(view, incoming), normal);
let n_dot_h = dot_vec3(normal, half_vector).max(0.0);
let v_dot_h = dot_vec3(view, half_vector).max(0.0);
let alpha = material.roughness * material.roughness;
let distribution = distribution_ggx(n_dot_h, alpha);
let geometry = geometry_smith(n_dot_v, n_dot_l, material.roughness);
let fresnel = fresnel_schlick(v_dot_h, material.f0());
let specular = scale_vec3(
fresnel,
distribution * geometry / (4.0 * n_dot_v * n_dot_l).max(MIN_DENOMINATOR),
);
let diffuse_energy = scale_vec3(
subtract_vec3(Vec3::new(1.0, 1.0, 1.0), fresnel),
1.0 - material.metallic,
);
let diffuse = scale_vec3(multiply_vec3(diffuse_energy, material.base), PI.recip());
scale_vec3(
multiply_vec3(add_vec3(diffuse, specular), radiance),
n_dot_l,
)
}
pub(super) fn clearcoat_light_contribution(
normal: Vec3,
view: Vec3,
incoming: Vec3,
radiance: Vec3,
factor: f32,
roughness: f32,
) -> Vec3 {
let factor = clamp_unit(factor);
if factor <= f32::EPSILON {
return Vec3::ZERO;
}
let incoming = normalize_or(incoming, Vec3::ZERO);
let n_dot_l = dot_vec3(normal, incoming).max(0.0);
if n_dot_l <= f32::EPSILON {
return Vec3::ZERO;
}
let n_dot_v = dot_vec3(normal, view).max(MIN_N_DOT_V);
let half_vector = normalize_or(add_vec3(view, incoming), normal);
let n_dot_h = dot_vec3(normal, half_vector).max(0.0);
let v_dot_h = dot_vec3(view, half_vector).max(0.0);
let roughness = roughness_or_min(roughness);
let alpha = roughness * roughness;
let distribution = distribution_ggx(n_dot_h, alpha);
let geometry = geometry_smith(n_dot_v, n_dot_l, roughness);
let fresnel = fresnel_schlick(
v_dot_h,
Vec3::new(DIELECTRIC_F0, DIELECTRIC_F0, DIELECTRIC_F0),
);
let specular = scale_vec3(
fresnel,
distribution * geometry * factor / (4.0 * n_dot_v * n_dot_l).max(MIN_DENOMINATOR),
);
scale_vec3(multiply_vec3(specular, radiance), n_dot_l)
}
pub(super) fn sheen_light_contribution(
normal: Vec3,
view: Vec3,
incoming: Vec3,
radiance: Vec3,
color: Vec3,
roughness: f32,
) -> Vec3 {
let color = clamp_vec3_unit(color);
if max_component(color) <= f32::EPSILON {
return Vec3::ZERO;
}
let incoming = normalize_or(incoming, Vec3::ZERO);
let n_dot_l = dot_vec3(normal, incoming).max(0.0);
if n_dot_l <= f32::EPSILON {
return Vec3::ZERO;
}
let n_dot_v = dot_vec3(normal, view).max(MIN_N_DOT_V);
let half_vector = normalize_or(add_vec3(view, incoming), normal);
let n_dot_h = dot_vec3(normal, half_vector).max(0.0);
let roughness = roughness_or_min(roughness);
let alpha = roughness * roughness;
let distribution = distribution_ggx(n_dot_h, alpha);
let geometry = geometry_smith(n_dot_v, n_dot_l, roughness);
let sheen = scale_vec3(
color,
distribution * geometry / (4.0 * n_dot_v * n_dot_l).max(MIN_DENOMINATOR),
);
scale_vec3(multiply_vec3(sheen, radiance), n_dot_l)
}
#[allow(clippy::too_many_arguments)]
pub(super) fn anisotropy_light_contribution(
material: PbrMaterial,
normal: Vec3,
tangent: Vec3,
tangent_handedness: f32,
view: Vec3,
incoming: Vec3,
radiance: Vec3,
strength: f32,
rotation: f32,
texture_direction_strength: Vec3,
) -> Vec3 {
let strength = clamp_unit(strength) * clamp_unit(texture_direction_strength.z);
if strength <= f32::EPSILON {
return Vec3::ZERO;
}
let incoming = normalize_or(incoming, Vec3::ZERO);
let n_dot_l = dot_vec3(normal, incoming).max(0.0);
if n_dot_l <= f32::EPSILON {
return Vec3::ZERO;
}
let normal = normalize_or(normal, Vec3::new(0.0, 0.0, 1.0));
let view = normalize_or(view, normal);
let tangent = normalize_or(
subtract_vec3(tangent, scale_vec3(normal, dot_vec3(tangent, normal))),
fallback_tangent(normal),
);
let bitangent = normalize_or(
scale_vec3(cross_vec3(normal, tangent), tangent_handedness.signum()),
fallback_tangent(normal),
);
let direction = rotated_anisotropy_direction(texture_direction_strength, rotation);
let anisotropic_tangent = normalize_or(
add_vec3(
scale_vec3(tangent, direction.0),
scale_vec3(bitangent, direction.1),
),
tangent,
);
let anisotropic_bitangent = normalize_or(cross_vec3(normal, anisotropic_tangent), bitangent);
let half_vector = normalize_or(add_vec3(view, incoming), normal);
let n_dot_v = dot_vec3(normal, view).max(MIN_N_DOT_V);
let n_dot_h = dot_vec3(normal, half_vector).max(0.0);
let v_dot_h = dot_vec3(view, half_vector).max(0.0);
let t_dot_v = dot_vec3(anisotropic_tangent, view);
let b_dot_v = dot_vec3(anisotropic_bitangent, view);
let t_dot_l = dot_vec3(anisotropic_tangent, incoming);
let b_dot_l = dot_vec3(anisotropic_bitangent, incoming);
let t_dot_h = dot_vec3(anisotropic_tangent, half_vector);
let b_dot_h = dot_vec3(anisotropic_bitangent, half_vector);
let base_alpha = material.roughness * material.roughness;
let tangent_alpha = mix_scalar(base_alpha, 1.0, strength * strength);
let bitangent_alpha = base_alpha;
let distribution =
distribution_ggx_anisotropic(n_dot_h, t_dot_h, b_dot_h, tangent_alpha, bitangent_alpha);
let visibility = visibility_ggx_anisotropic(
n_dot_l,
n_dot_v,
b_dot_v,
t_dot_v,
t_dot_l,
b_dot_l,
tangent_alpha,
bitangent_alpha,
);
let fresnel = fresnel_schlick(v_dot_h, material.f0());
let specular = scale_vec3(fresnel, distribution * visibility * strength);
scale_vec3(multiply_vec3(specular, radiance), n_dot_l)
}
#[allow(clippy::too_many_arguments)]
pub(super) fn iridescence_light_contribution(
material: PbrMaterial,
normal: Vec3,
view: Vec3,
incoming: Vec3,
radiance: Vec3,
factor: f32,
ior: f32,
thickness_minimum_nm: f32,
thickness_maximum_nm: f32,
texture_strength: f32,
thickness_texture: f32,
) -> Vec3 {
let factor = clamp_unit(factor) * clamp_unit(texture_strength);
if factor <= f32::EPSILON {
return Vec3::ZERO;
}
let incoming = normalize_or(incoming, Vec3::ZERO);
let n_dot_l = dot_vec3(normal, incoming).max(0.0);
if n_dot_l <= f32::EPSILON {
return Vec3::ZERO;
}
let normal = normalize_or(normal, Vec3::new(0.0, 0.0, 1.0));
let view = normalize_or(view, normal);
let half_vector = normalize_or(add_vec3(view, incoming), normal);
let n_dot_v = dot_vec3(normal, view).max(MIN_N_DOT_V);
let n_dot_h = dot_vec3(normal, half_vector).max(0.0);
let v_dot_h = dot_vec3(view, half_vector).max(0.0);
let roughness = roughness_or_min(material.roughness);
let alpha = roughness * roughness;
let distribution = distribution_ggx(n_dot_h, alpha);
let geometry = geometry_smith(n_dot_v, n_dot_l, roughness);
let thickness = mix_scalar(
finite_non_negative(thickness_minimum_nm),
finite_non_negative(thickness_maximum_nm),
thickness_texture,
);
let film_color = iridescence_film_color(thickness, ior);
let tinted_f0 = multiply_vec3(material.f0(), film_color);
let fresnel = fresnel_schlick(v_dot_h, tinted_f0);
let specular = scale_vec3(
multiply_vec3(fresnel, film_color),
distribution * geometry * factor / (4.0 * n_dot_v * n_dot_l).max(MIN_DENOMINATOR),
);
scale_vec3(multiply_vec3(specular, radiance), n_dot_l)
}
pub(super) fn environment_split_sum_contribution(
material: PbrMaterial,
normal: Vec3,
view: Vec3,
diffuse_irradiance: Vec3,
prefiltered_specular: Vec3,
brdf_scale_bias: (f32, f32),
) -> Vec3 {
let n_dot_v = dot_vec3(normal, view).max(MIN_N_DOT_V);
let fresnel = fresnel_schlick(n_dot_v, material.f0());
let diffuse_energy = scale_vec3(
subtract_vec3(Vec3::new(1.0, 1.0, 1.0), fresnel),
1.0 - material.metallic,
);
let diffuse = multiply_vec3(
multiply_vec3(diffuse_energy, material.base),
diffuse_irradiance,
);
let f0 = material.f0();
let specular_factor = add_vec3(
scale_vec3(f0, brdf_scale_bias.0),
Vec3::new(brdf_scale_bias.1, brdf_scale_bias.1, brdf_scale_bias.1),
);
let specular = multiply_vec3(prefiltered_specular, specular_factor);
add_vec3(diffuse, specular)
}
pub(super) fn directional_illuminance_lux(value: f32) -> f32 {
finite_non_negative(value) * DIRECTIONAL_LUX_TO_SCENE_RADIANCE
}
pub(super) fn punctual_intensity_candela(value: f32) -> f32 {
finite_non_negative(value)
}
pub(super) fn inverse_square_range_attenuation(to_light: Vec3, range: Option<f32>) -> f32 {
let distance_squared = dot_vec3(to_light, to_light).max(MIN_DENOMINATOR);
let inverse_square = distance_squared.recip();
let Some(range) = range else {
return inverse_square;
};
if range <= f32::EPSILON || !range.is_finite() {
return 0.0;
}
let distance = distance_squared.sqrt();
let range_falloff = (1.0 - (distance / range).powi(4)).clamp(0.0, 1.0);
inverse_square * range_falloff * range_falloff
}
pub(super) fn spot_cone_attenuation(
cos_angle: f32,
inner_cone_cos: f32,
outer_cone_cos: f32,
) -> f32 {
if cos_angle >= inner_cone_cos {
1.0
} else if cos_angle <= outer_cone_cos {
0.0
} else {
((cos_angle - outer_cone_cos) / (inner_cone_cos - outer_cone_cos)).clamp(0.0, 1.0)
}
}
pub(super) fn reflect_vec3(vector: Vec3, normal: Vec3) -> Vec3 {
subtract_vec3(vector, scale_vec3(normal, 2.0 * dot_vec3(vector, normal)))
}
fn distribution_ggx(n_dot_h: f32, alpha: f32) -> f32 {
let alpha_squared = alpha * alpha;
let denominator = n_dot_h * n_dot_h * (alpha_squared - 1.0) + 1.0;
alpha_squared / (PI * denominator * denominator).max(MIN_DENOMINATOR)
}
fn distribution_ggx_anisotropic(
n_dot_h: f32,
t_dot_h: f32,
b_dot_h: f32,
tangent_alpha: f32,
bitangent_alpha: f32,
) -> f32 {
let alpha_product = (tangent_alpha * bitangent_alpha).max(MIN_DENOMINATOR);
let f = Vec3::new(
bitangent_alpha * t_dot_h,
tangent_alpha * b_dot_h,
alpha_product * n_dot_h,
);
let w2 = alpha_product / dot_vec3(f, f).max(MIN_DENOMINATOR);
alpha_product * w2 * w2 / PI
}
#[allow(clippy::too_many_arguments)]
fn visibility_ggx_anisotropic(
n_dot_l: f32,
n_dot_v: f32,
b_dot_v: f32,
t_dot_v: f32,
t_dot_l: f32,
b_dot_l: f32,
tangent_alpha: f32,
bitangent_alpha: f32,
) -> f32 {
let ggx_v = n_dot_l
* length_vec3(Vec3::new(
tangent_alpha * t_dot_v,
bitangent_alpha * b_dot_v,
n_dot_v,
));
let ggx_l = n_dot_v
* length_vec3(Vec3::new(
tangent_alpha * t_dot_l,
bitangent_alpha * b_dot_l,
n_dot_l,
));
(0.5 / (ggx_v + ggx_l).max(MIN_DENOMINATOR)).clamp(0.0, 1.0)
}
fn geometry_smith(n_dot_v: f32, n_dot_l: f32, roughness: f32) -> f32 {
let k = ((roughness + 1.0) * (roughness + 1.0)) / 8.0;
geometry_schlick_ggx(n_dot_v, k) * geometry_schlick_ggx(n_dot_l, k)
}
fn geometry_schlick_ggx(n_dot: f32, k: f32) -> f32 {
n_dot / (n_dot * (1.0 - k) + k).max(MIN_DENOMINATOR)
}
fn fresnel_schlick(cos_theta: f32, f0: Vec3) -> Vec3 {
let factor = (1.0 - cos_theta.clamp(0.0, 1.0)).powi(5);
add_vec3(
f0,
scale_vec3(subtract_vec3(Vec3::new(1.0, 1.0, 1.0), f0), factor),
)
}
fn finite_non_negative(value: f32) -> f32 {
if value.is_finite() {
value.max(0.0)
} else {
0.0
}
}
fn clamp_unit(value: f32) -> f32 {
if value.is_finite() {
value.clamp(0.0, 1.0)
} else {
0.0
}
}
fn clamp_vec3_unit(value: Vec3) -> Vec3 {
Vec3::new(
clamp_unit(value.x),
clamp_unit(value.y),
clamp_unit(value.z),
)
}
fn max_component(value: Vec3) -> f32 {
value.x.max(value.y).max(value.z)
}
fn add_vec3(left: Vec3, right: Vec3) -> Vec3 {
Vec3::new(left.x + right.x, left.y + right.y, left.z + right.z)
}
fn subtract_vec3(left: Vec3, right: Vec3) -> Vec3 {
Vec3::new(left.x - right.x, left.y - right.y, left.z - right.z)
}
fn scale_vec3(value: Vec3, scale: f32) -> Vec3 {
Vec3::new(value.x * scale, value.y * scale, value.z * scale)
}
fn multiply_vec3(left: Vec3, right: Vec3) -> Vec3 {
Vec3::new(left.x * right.x, left.y * right.y, left.z * right.z)
}
fn mix_vec3(left: Vec3, right: Vec3, amount: f32) -> Vec3 {
let amount = clamp_unit(amount);
add_vec3(scale_vec3(left, 1.0 - amount), scale_vec3(right, amount))
}
fn mix_scalar(left: f32, right: f32, amount: f32) -> f32 {
let amount = clamp_unit(amount);
left * (1.0 - amount) + right * amount
}
fn dot_vec3(left: Vec3, right: Vec3) -> f32 {
left.x * right.x + left.y * right.y + left.z * right.z
}
fn length_vec3(vector: Vec3) -> f32 {
dot_vec3(vector, vector).sqrt()
}
fn cross_vec3(left: Vec3, right: Vec3) -> Vec3 {
Vec3::new(
left.y * right.z - left.z * right.y,
left.z * right.x - left.x * right.z,
left.x * right.y - left.y * right.x,
)
}
fn normalize_or(vector: Vec3, fallback: Vec3) -> Vec3 {
let length = length_vec3(vector);
if length <= f32::EPSILON || !length.is_finite() {
fallback
} else {
Vec3::new(vector.x / length, vector.y / length, vector.z / length)
}
}
fn fallback_tangent(normal: Vec3) -> Vec3 {
let axis = if normal.z.abs() < 0.9 {
Vec3::new(0.0, 0.0, 1.0)
} else {
Vec3::new(0.0, 1.0, 0.0)
};
normalize_or(cross_vec3(axis, normal), Vec3::new(1.0, 0.0, 0.0))
}
fn rotated_anisotropy_direction(texture_direction_strength: Vec3, rotation: f32) -> (f32, f32) {
let raw_x = texture_direction_strength.x;
let raw_y = texture_direction_strength.y;
let length = (raw_x * raw_x + raw_y * raw_y).sqrt();
let (x, y) = if length <= f32::EPSILON || !length.is_finite() {
(1.0, 0.0)
} else {
(raw_x / length, raw_y / length)
};
let rotation = if rotation.is_finite() { rotation } else { 0.0 };
let sin = rotation.sin();
let cos = rotation.cos();
(x * cos - y * sin, x * sin + y * cos)
}
fn iridescence_film_color(thickness_nm: f32, ior: f32) -> Vec3 {
let thickness_nm = finite_non_negative(thickness_nm);
let ior = if ior.is_finite() && ior > 0.0 {
ior
} else {
1.3
};
let phase = (thickness_nm * ior / 650.0) * PI * 1.25;
Vec3::new(
(phase.sin() * 0.5 + 0.5).clamp(0.0, 1.0),
((phase + 2.0 * PI / 3.0).sin() * 0.5 + 0.5).clamp(0.0, 1.0),
((phase + 4.0 * PI / 3.0).sin() * 0.5 + 0.5).clamp(0.0, 1.0),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pbr_material_uses_gltf_dielectric_and_metallic_f0() {
let base = Vec3::new(0.8, 0.2, 0.1);
let dielectric = PbrMaterial::new(base, 0.0, 0.5);
assert_eq!(
dielectric.f0(),
Vec3::new(DIELECTRIC_F0, DIELECTRIC_F0, DIELECTRIC_F0)
);
let metal = PbrMaterial::new(base, 1.0, 0.5);
assert_eq!(metal.f0(), base);
}
#[test]
fn punctual_light_units_do_not_apply_scene_tuned_divisors_or_clamps() {
assert_eq!(punctual_intensity_candela(800.0), 800.0);
let near = inverse_square_range_attenuation(Vec3::new(0.0, 0.0, 1.0), Some(10.0));
let far = inverse_square_range_attenuation(Vec3::new(0.0, 0.0, 2.0), Some(10.0));
assert!(
near > far * 3.5,
"KHR_lights_punctual point/spot intensity must use inverse-square distance falloff"
);
}
#[test]
fn directional_lux_is_calibrated_to_scene_linear_radiance() {
assert_eq!(directional_illuminance_lux(0.0), 0.0);
assert!(
(directional_illuminance_lux(10_000.0) - 1.0).abs() < 1e-6,
"a default 10k-lux directional light must be calibrated to renderer scene-linear \
units instead of being injected as raw 10000x HDR radiance"
);
}
#[test]
fn clearcoat_light_contribution_adds_dielectric_lobe() {
let normal = Vec3::new(0.0, 0.0, 1.0);
let view = normal;
let incoming = normal;
let radiance = Vec3::new(1.0, 1.0, 1.0);
let off = clearcoat_light_contribution(normal, view, incoming, radiance, 0.0, 0.1);
let on = clearcoat_light_contribution(normal, view, incoming, radiance, 1.0, 0.1);
assert_eq!(off, Vec3::ZERO);
assert!(
on.x > 0.0 && on.y > 0.0 && on.z > 0.0,
"clearcoat must add a white dielectric specular lobe"
);
}
#[test]
fn sheen_light_contribution_adds_colored_lobe() {
let normal = Vec3::new(0.0, 0.0, 1.0);
let view = normal;
let incoming = normal;
let radiance = Vec3::new(1.0, 1.0, 1.0);
let off = sheen_light_contribution(normal, view, incoming, radiance, Vec3::ZERO, 0.35);
let red = sheen_light_contribution(
normal,
view,
incoming,
radiance,
Vec3::new(1.0, 0.0, 0.0),
0.35,
);
assert_eq!(off, Vec3::ZERO);
assert!(
red.x > 0.0 && red.y == 0.0 && red.z == 0.0,
"sheen must add a colored texture/factor-driven lobe"
);
}
#[test]
fn anisotropy_light_contribution_uses_strength_texture_and_direction() {
let material = PbrMaterial::new(Vec3::new(0.8, 0.8, 0.8), 1.0, 0.42);
let normal = Vec3::new(0.0, 0.0, 1.0);
let tangent = Vec3::new(1.0, 0.0, 0.0);
let view = normalize_or(Vec3::new(0.25, 0.0, 1.0), normal);
let incoming = normalize_or(Vec3::new(0.25, 0.0, 1.0), normal);
let radiance = Vec3::new(1.0, 1.0, 1.0);
let off = anisotropy_light_contribution(
material,
normal,
tangent,
1.0,
view,
incoming,
radiance,
0.0,
0.0,
Vec3::new(1.0, 0.5, 1.0),
);
let along_tangent = anisotropy_light_contribution(
material,
normal,
tangent,
1.0,
view,
incoming,
radiance,
1.0,
0.0,
Vec3::new(1.0, 0.5, 1.0),
);
let along_bitangent = anisotropy_light_contribution(
material,
normal,
tangent,
1.0,
view,
incoming,
radiance,
1.0,
PI * 0.5,
Vec3::new(1.0, 0.5, 1.0),
);
assert_eq!(off, Vec3::ZERO);
assert!(
along_tangent.x > along_bitangent.x * 1.5,
"anisotropy must shape the specular lobe around the tangent-space direction; \
tangent={along_tangent:?} bitangent={along_bitangent:?}"
);
let zero_texture_strength = anisotropy_light_contribution(
material,
normal,
tangent,
1.0,
view,
incoming,
radiance,
1.0,
0.0,
Vec3::new(1.0, 0.5, 0.0),
);
assert_eq!(
zero_texture_strength,
Vec3::ZERO,
"anisotropyTexture blue channel must multiply anisotropyStrength"
);
}
#[test]
fn iridescence_light_contribution_uses_factor_thickness_and_textures() {
let material = PbrMaterial::new(Vec3::new(0.72, 0.72, 0.72), 0.0, 0.35);
let normal = Vec3::new(0.0, 0.0, 1.0);
let view = normalize_or(Vec3::new(0.2, 0.0, 1.0), normal);
let incoming = normalize_or(Vec3::new(0.15, 0.0, 1.0), normal);
let radiance = Vec3::new(1.0, 1.0, 1.0);
let off = iridescence_light_contribution(
material, normal, view, incoming, radiance, 0.0, 1.3, 100.0, 400.0, 1.0, 1.0,
);
let thin = iridescence_light_contribution(
material, normal, view, incoming, radiance, 1.0, 1.3, 100.0, 250.0, 1.0, 0.0,
);
let thick = iridescence_light_contribution(
material, normal, view, incoming, radiance, 1.0, 1.3, 100.0, 650.0, 1.0, 1.0,
);
let texture_off = iridescence_light_contribution(
material, normal, view, incoming, radiance, 1.0, 1.3, 100.0, 650.0, 0.0, 1.0,
);
assert_eq!(off, Vec3::ZERO);
assert_eq!(texture_off, Vec3::ZERO);
assert!(
max_component(thin) > 0.0 && max_component(thick) > 0.0,
"iridescence should add a thin-film colored specular contribution"
);
assert_ne!(
dominant_channel(thin),
dominant_channel(thick),
"iridescence thickness must shift the visible hue, not only brighten uniformly"
);
}
fn dominant_channel(value: Vec3) -> usize {
if value.x >= value.y && value.x >= value.z {
0
} else if value.y >= value.z {
1
} else {
2
}
}
}