use super::super::{
super::{
camera::Ray, environment::EnvironmentSource, intersect::build_plane_soa, sampling::hash_u32,
},
inner::trace_spectral_ray_inner,
};
use crate::{geometry::cuts::StandardGemCuts, optics::materials::GemMaterial};
use glam::Vec3;
#[test]
fn nee_on_and_off_converge_to_the_same_mean_on_a_uniform_hdr_map() {
const L0: f32 = 2.0;
const SAMPLES: u32 = 20_000;
const TOLERANCE: f32 = 0.05;
let planes = StandardGemCuts::standard_round_brilliant();
let plane_soa = build_plane_soa(&planes);
let material = GemMaterial::new_custom("G7 furnace probe", 1.5, 0.0, 0.0, [0.0, 0.0, 0.0])
.with_scattering(1.0, 0.2);
let env_map = crate::renderer::env_map::EnvironmentMap::uniform(4, 2, [L0, L0, L0]);
let environment = EnvironmentSource::HdrMap(&env_map);
let ray = Ray {
origin: Vec3::new(0.0, 2.5, 0.0),
dir: Vec3::new(0.1, -1.0, 0.05).normalize(),
};
let mut sum_on = Vec3::ZERO;
let mut sum_off = Vec3::ZERO;
for i in 0..SAMPLES {
let seed = 42_000 + i;
let hero_rand = (hash_u32(seed) as f32) / 4_294_967_295.0;
sum_on += trace_spectral_ray_inner(
ray,
&planes,
&plane_soa,
&[],
&material,
12,
environment,
seed,
hero_rand,
None,
true,
true,
true,
None,
);
sum_off += trace_spectral_ray_inner(
ray,
&planes,
&plane_soa,
&[],
&material,
12,
environment,
seed,
hero_rand,
None,
true,
true,
false,
None,
);
}
let mean_on = sum_on / SAMPLES as f32;
let mean_off = sum_off / SAMPLES as f32;
let rel_err = |v: f32, t: f32| (v - t).abs() / t.abs().max(1e-6);
let (ex, ey, ez) = (
rel_err(mean_on.x, mean_off.x),
rel_err(mean_on.y, mean_off.y),
rel_err(mean_on.z, mean_off.z),
);
println!(
"[G7 NEE furnace] mean_on={mean_on:?} mean_off={mean_off:?} \
rel_err=({ex:.4}, {ey:.4}, {ez:.4})"
);
assert!(
ex <= TOLERANCE && ey <= TOLERANCE && ez <= TOLERANCE,
"NEE-on and NEE-off should converge to the same mean radiance on a uniform \
HDR map (mean_on={mean_on:?}, mean_off={mean_off:?}, rel_err=({ex}, {ey}, \
{ez}), tolerance={TOLERANCE})"
);
}
#[test]
fn nee_on_and_off_agree_on_a_two_hemisphere_hdr_map() {
const L0: f32 = 3.0;
const SAMPLES: u32 = 40_000;
const TOLERANCE: f32 = 0.05;
const WIDTH: usize = 8;
const HEIGHT: usize = 4;
let planes = StandardGemCuts::standard_round_brilliant();
let plane_soa = build_plane_soa(&planes);
let material =
GemMaterial::new_custom("2c two-hemisphere probe", 1.5, 0.0, 0.0, [0.0, 0.0, 0.0])
.with_scattering(1.0, 0.2);
let mut pixels = vec![[0.0f32, 0.0, 0.0]; WIDTH * HEIGHT];
for row in 0..HEIGHT / 2 {
for col in 0..WIDTH {
pixels[row * WIDTH + col] = [L0, L0, L0];
}
}
let env_map = crate::renderer::env_map::EnvironmentMap::from_rgb(WIDTH, HEIGHT, pixels)
.expect("WIDTH * HEIGHT pixels for a WIDTH x HEIGHT map");
let environment = EnvironmentSource::HdrMap(&env_map);
let ray = Ray {
origin: Vec3::new(0.0, 2.5, 0.0),
dir: Vec3::new(0.1, -1.0, 0.05).normalize(),
};
let mut sum_on = Vec3::ZERO;
let mut sum_off = Vec3::ZERO;
for i in 0..SAMPLES {
let seed = 77_000 + i;
let hero_rand = (hash_u32(seed) as f32) / 4_294_967_295.0;
sum_on += trace_spectral_ray_inner(
ray,
&planes,
&plane_soa,
&[],
&material,
12,
environment,
seed,
hero_rand,
None,
true,
true,
true,
None,
);
sum_off += trace_spectral_ray_inner(
ray,
&planes,
&plane_soa,
&[],
&material,
12,
environment,
seed,
hero_rand,
None,
true,
true,
false,
None,
);
}
let mean_on = sum_on / SAMPLES as f32;
let mean_off = sum_off / SAMPLES as f32;
let rel_err = |v: f32, t: f32| (v - t).abs() / t.abs().max(1e-6);
let (ex, ey, ez) = (
rel_err(mean_on.x, mean_off.x),
rel_err(mean_on.y, mean_off.y),
rel_err(mean_on.z, mean_off.z),
);
println!(
"[2c two-hemisphere NEE] mean_on={mean_on:?} mean_off={mean_off:?} \
rel_err=({ex:.4}, {ey:.4}, {ez:.4})"
);
assert!(
ex <= TOLERANCE && ey <= TOLERANCE && ez <= TOLERANCE,
"NEE-on and NEE-off should agree within tolerance on a directional \
(two-hemisphere) HDR map, not only a uniform one (mean_on={mean_on:?}, \
mean_off={mean_off:?}, rel_err=({ex}, {ey}, {ez}), tolerance={TOLERANCE})"
);
}
#[test]
fn frosted_girdle_nee_on_and_off_converge_to_the_same_mean_on_a_uniform_hdr_map() {
use super::super::super::camera::Camera;
const L0: f32 = 2.0;
const SAMPLES_PER_PIXEL: u32 = 48;
const GRID: usize = 10;
const TOLERANCE: f32 = 0.06;
let planes = StandardGemCuts::standard_round_brilliant();
let plane_soa = build_plane_soa(&planes);
let finishes = crate::geometry::girdle_facet_finishes(&planes);
let material =
GemMaterial::new_custom("G8 frosted furnace probe", 1.5, 0.0, 0.0, [0.0, 0.0, 0.0]);
let env_map = crate::renderer::env_map::EnvironmentMap::uniform(4, 2, [L0, L0, L0]);
let environment = EnvironmentSource::HdrMap(&env_map);
let camera = Camera::new(0.35, 0.28, 5.0, 18.0);
let mut sum_on = Vec3::ZERO;
let mut sum_off = Vec3::ZERO;
let mut count = 0u32;
for iy in 0..GRID {
for ix in 0..GRID {
let ray = camera.generate_ray(ix as f32, iy as f32, GRID as f32, GRID as f32, 0.5, 0.5);
for s in 0..SAMPLES_PER_PIXEL {
let pixel_id = (iy as u32) * (GRID as u32) + (ix as u32);
let seed = hash_u32(pixel_id ^ hash_u32(s ^ 0x6667_8899));
let hero_rand = (hash_u32(seed) as f32) / 4_294_967_295.0;
sum_on += trace_spectral_ray_inner(
ray,
&planes,
&plane_soa,
&finishes,
&material,
12,
environment,
seed,
hero_rand,
None,
true,
true,
true,
None,
);
sum_off += trace_spectral_ray_inner(
ray,
&planes,
&plane_soa,
&finishes,
&material,
12,
environment,
seed,
hero_rand,
None,
true,
true,
false,
None,
);
count += 1;
}
}
}
let mean_on = sum_on / count as f32;
let mean_off = sum_off / count as f32;
let rel_err = |v: f32, t: f32| (v - t).abs() / t.abs().max(1e-6);
let (ex, ey, ez) = (
rel_err(mean_on.x, mean_off.x),
rel_err(mean_on.y, mean_off.y),
rel_err(mean_on.z, mean_off.z),
);
println!(
"[G8 frosted-girdle NEE furnace] mean_on={mean_on:?} mean_off={mean_off:?} \
rel_err=({ex:.4}, {ey:.4}, {ez:.4})"
);
assert!(
ex <= TOLERANCE && ey <= TOLERANCE && ez <= TOLERANCE,
"frosted-girdle NEE-on and NEE-off should converge to the same mean radiance \
on a uniform HDR map (mean_on={mean_on:?}, mean_off={mean_off:?}, \
rel_err=({ex}, {ey}, {ez}), tolerance={TOLERANCE})"
);
}
#[test]
fn dispersive_scattering_nee_on_and_off_converge_to_the_same_mean() {
const SAMPLES: u32 = 20_000;
const TOLERANCE: f32 = 0.02;
const MAX_BOUNCES: u32 = 64;
let planes = StandardGemCuts::standard_round_brilliant();
let plane_soa = build_plane_soa(&planes);
let material = GemMaterial::diamond().with_scattering(1.5, 0.3);
let env_map = crate::renderer::env_map::EnvironmentMap::uniform(4, 2, [2.0, 2.0, 2.0]);
let environment = EnvironmentSource::HdrMap(&env_map);
let ray = Ray {
origin: Vec3::new(0.0, 2.5, 0.0),
dir: Vec3::new(0.12, -1.0, 0.06).normalize(),
};
let mut sum_on = Vec3::ZERO;
let mut sum_off = Vec3::ZERO;
for i in 0..SAMPLES {
let seed = 77_000 + i;
let hero_rand = (hash_u32(seed) as f32) / 4_294_967_295.0;
sum_on += trace_spectral_ray_inner(
ray,
&planes,
&plane_soa,
&[],
&material,
MAX_BOUNCES,
environment,
seed,
hero_rand,
None,
true,
true,
true,
None,
);
sum_off += trace_spectral_ray_inner(
ray,
&planes,
&plane_soa,
&[],
&material,
MAX_BOUNCES,
environment,
seed,
hero_rand,
None,
true,
true,
false,
None,
);
}
let mean_on = sum_on / SAMPLES as f32;
let mean_off = sum_off / SAMPLES as f32;
let rel_err = |v: f32, t: f32| (v - t).abs() / t.abs().max(1e-6);
let (ex, ey, ez) = (
rel_err(mean_on.x, mean_off.x),
rel_err(mean_on.y, mean_off.y),
rel_err(mean_on.z, mean_off.z),
);
println!(
"[dispersive scattering NEE] mean_on={mean_on:?} mean_off={mean_off:?} \
rel_err=({ex:.4}, {ey:.4}, {ez:.4})"
);
assert!(
ex <= TOLERANCE && ey <= TOLERANCE && ez <= TOLERANCE,
"a dispersive scattering material's NEE-on and NEE-off means should converge \
(mean_on={mean_on:?}, mean_off={mean_off:?}, rel_err=({ex}, {ey}, {ez}), \
tolerance={TOLERANCE})"
);
}
#[test]
fn ruby_scattering_nee_on_and_off_agree_within_half_a_percent() {
const SAMPLES: u32 = 20_000;
const TOLERANCE: f32 = 0.005;
const MAX_BOUNCES: u32 = 64;
let planes = StandardGemCuts::standard_round_brilliant();
let plane_soa = build_plane_soa(&planes);
let material = GemMaterial::ruby().with_scattering(1.2, 0.4);
let env_map = crate::renderer::env_map::EnvironmentMap::uniform(4, 2, [2.0, 2.0, 2.0]);
let environment = EnvironmentSource::HdrMap(&env_map);
let ray = Ray {
origin: Vec3::new(0.0, 2.5, 0.0),
dir: Vec3::new(0.1, -1.0, 0.05).normalize(),
};
let mut sum_on = Vec3::ZERO;
let mut sum_off = Vec3::ZERO;
for i in 0..SAMPLES {
let seed = 91_000 + i;
let hero_rand = (hash_u32(seed) as f32) / 4_294_967_295.0;
sum_on += trace_spectral_ray_inner(
ray,
&planes,
&plane_soa,
&[],
&material,
MAX_BOUNCES,
environment,
seed,
hero_rand,
None,
true,
true,
true,
None,
);
sum_off += trace_spectral_ray_inner(
ray,
&planes,
&plane_soa,
&[],
&material,
MAX_BOUNCES,
environment,
seed,
hero_rand,
None,
true,
true,
false,
None,
);
}
let mean_on = sum_on / SAMPLES as f32;
let mean_off = sum_off / SAMPLES as f32;
let rel_err = |v: f32, t: f32| (v - t).abs() / t.abs().max(1e-6);
let (ex, ey, ez) = (
rel_err(mean_on.x, mean_off.x),
rel_err(mean_on.y, mean_off.y),
rel_err(mean_on.z, mean_off.z),
);
println!(
"[ruby scattering NEE] mean_on={mean_on:?} mean_off={mean_off:?} \
rel_err=({ex:.4}, {ey:.4}, {ez:.4})"
);
assert!(
ex <= TOLERANCE && ey <= TOLERANCE && ez <= TOLERANCE,
"ruby's absorbing scattering medium NEE-on and NEE-off means should agree \
within {TOLERANCE} (mean_on={mean_on:?}, mean_off={mean_off:?}, \
rel_err=({ex}, {ey}, {ez}))"
);
}