use crate::{
color::cie1931::cie_1931_cmf,
optics::{
materials::GemMaterial,
raytracer::{EnvironmentSource, FacetFinish},
},
renderer::{
buffers::{GpuGemMaterial, GpuTransportParams, encode_facet_finishes, transport_env_mode},
env_map::{EnvironmentMap, rgb_to_spectral_radiance},
},
};
use glam::Vec3;
use super::{
CpuScene, MaterialForDispatch, SceneBuffersForDispatch, WelfordXyz, bruted_girdle_finishes,
camera_params_for, cpu_sample_xyz, cpu_samples, dispatch_transport,
dispatch_transport_for_class, furnace_material, round_brilliant_planes, test_camera, z_score,
};
#[derive(Debug, Clone)]
pub struct FurnaceResult {
pub analytic_target: Vec3,
pub cpu_mean: Vec3,
pub gpu_mean: Vec3,
pub cpu_relative_error: f32,
pub gpu_relative_error: f32,
pub cpu_standard_error: Vec3,
pub gpu_standard_error: Vec3,
pub cpu_gpu_z: [f64; 3],
pub total_cpu_samples: usize,
pub total_gpu_samples: usize,
}
pub(super) const FURNACE_CONVERGENCE_TOLERANCE: f32 = 0.015;
const FURNACE_Z_GATE: f64 = 4.0;
pub(super) const FROSTED_FURNACE_CONVERGENCE_TOLERANCE: f32 = 0.015;
pub(super) const SCATTERING_FURNACE_CONVERGENCE_TOLERANCE: f32 = 0.005;
pub(super) const EDGE_ROUNDING_FURNACE_CONVERGENCE_TOLERANCE: f32 = 0.015;
pub(super) const FURNACE_DEFAULT_MAX_BOUNCES: u32 = 12;
pub(super) const SCATTERING_FURNACE_MAX_BOUNCES: u32 = 48;
#[must_use]
pub fn run_furnace(ctx: &crate::renderer::gpu::GpuContext) -> FurnaceResult {
run_furnace_for(ctx, &furnace_material(), &[], FURNACE_DEFAULT_MAX_BOUNCES)
}
#[must_use]
pub fn run_furnace_frosted_girdle(ctx: &crate::renderer::gpu::GpuContext) -> FurnaceResult {
let num_planes = round_brilliant_planes().len();
run_furnace_for(
ctx,
&furnace_material(),
&bruted_girdle_finishes(num_planes),
FURNACE_DEFAULT_MAX_BOUNCES,
)
}
#[must_use]
pub fn run_furnace_scattering(ctx: &crate::renderer::gpu::GpuContext) -> FurnaceResult {
let material = furnace_material().with_scattering(1.2, 0.4);
run_furnace_for(ctx, &material, &[], SCATTERING_FURNACE_MAX_BOUNCES)
}
#[must_use]
pub fn run_furnace_edge_rounding(ctx: &crate::renderer::gpu::GpuContext) -> FurnaceResult {
let material = furnace_material().with_edge_rounding(0.03);
run_furnace_for(ctx, &material, &[], FURNACE_DEFAULT_MAX_BOUNCES)
}
#[must_use]
pub fn run_furnace_nee_equality_scattering(
ctx: &crate::renderer::gpu::GpuContext,
) -> FurnaceResult {
let material = furnace_material().with_scattering(1.2, 0.4);
run_furnace_for_env(ctx, &material, &[], true, SCATTERING_FURNACE_MAX_BOUNCES)
}
#[must_use]
pub fn run_furnace_nee_equality_frosted(ctx: &crate::renderer::gpu::GpuContext) -> FurnaceResult {
let num_planes = round_brilliant_planes().len();
run_furnace_for_env(
ctx,
&furnace_material(),
&bruted_girdle_finishes(num_planes),
true,
FURNACE_DEFAULT_MAX_BOUNCES,
)
}
fn run_furnace_for(
ctx: &crate::renderer::gpu::GpuContext,
material: &GemMaterial,
facet_finishes: &[FacetFinish],
max_bounces: u32,
) -> FurnaceResult {
run_furnace_for_env(ctx, material, facet_finishes, false, max_bounces)
}
fn run_furnace_for_env(
ctx: &crate::renderer::gpu::GpuContext,
material: &GemMaterial,
facet_finishes: &[FacetFinish],
use_hdr_nee: bool,
max_bounces: u32,
) -> FurnaceResult {
run_furnace_for_run(ctx, material, facet_finishes, use_hdr_nee, max_bounces, 0)
}
pub(super) fn run_furnace_for_run(
ctx: &crate::renderer::gpu::GpuContext,
material: &GemMaterial,
facet_finishes: &[FacetFinish],
use_hdr_nee: bool,
max_bounces: u32,
run: u32,
) -> FurnaceResult {
let camera = test_camera();
let (width, height) = (32u32, 32u32);
let planes = round_brilliant_planes();
let gpu_material = GpuGemMaterial::encode(material);
let gpu_finishes = encode_facet_finishes(facet_finishes, planes.len());
let l0 = 2.5f32;
let cpu_samples_per_pixel = 600u32;
let gpu_samples_per_pixel = 600u32;
let cpu_sample_offset = run * (cpu_samples_per_pixel + gpu_samples_per_pixel);
let gpu_sample_offset = cpu_sample_offset + cpu_samples_per_pixel;
let env_map = EnvironmentMap::uniform(4, 4, [l0, l0, l0]);
let environment = EnvironmentSource::HdrMap(&env_map);
let scene = CpuScene {
camera: &camera,
width,
height,
planes: &planes,
facet_finishes,
material,
max_bounces,
};
let cpu_flat = cpu_samples(
width,
height,
cpu_samples_per_pixel,
cpu_sample_offset,
|pixel, sample| cpu_sample_xyz(&scene, pixel, sample, environment),
);
let camera_params = camera_params_for(&camera, width, height, gpu_samples_per_pixel);
let mode = if use_hdr_nee {
transport_env_mode::HDR_MAP
} else {
transport_env_mode::UNIFORM_FURNACE
};
let params = GpuTransportParams::new(
width * height,
max_bounces,
gpu_sample_offset,
mode,
l0,
6500.0,
1.0,
1.0,
0.0,
0.0,
[1.0, 1.0, 1.0],
);
let total_gpu = (width * height * gpu_samples_per_pixel) as usize;
let gpu_dispatch = if use_hdr_nee {
let gpu_hdr = crate::renderer::env_map_gpu::HdrEnvGpuData::upload(&ctx.device, &env_map);
dispatch_transport_for_class(
ctx,
&camera_params,
¶ms,
MaterialForDispatch {
encoded: &gpu_material,
class: crate::renderer::gpu::frame::material_class::GENERIC,
},
SceneBuffersForDispatch {
planes: &planes,
facet_finishes: &gpu_finishes,
hdr_env: Some(&gpu_hdr),
},
total_gpu,
)
} else {
dispatch_transport(
ctx,
&camera_params,
¶ms,
&gpu_material,
&planes,
&gpu_finishes,
total_gpu,
)
};
summarise(&cpu_flat, &gpu_dispatch.xyz, l0)
}
fn summarise(cpu_flat: &[Vec3], gpu_xyz: &[f32], l0: f32) -> FurnaceResult {
let mut cpu_acc = WelfordXyz::default();
for v in cpu_flat {
cpu_acc.update(*v);
}
let mut gpu_acc = WelfordXyz::default();
for chunk in gpu_xyz.as_chunks::<3>().0 {
gpu_acc.update(Vec3::new(chunk[0], chunk[1], chunk[2]));
}
let target = analytic_furnace_target(l0);
let cpu_mean = cpu_acc.mean();
let gpu_mean = gpu_acc.mean();
FurnaceResult {
analytic_target: target,
cpu_mean,
gpu_mean,
cpu_relative_error: componentwise_relative_error(cpu_mean, target),
gpu_relative_error: componentwise_relative_error(gpu_mean, target),
cpu_standard_error: standard_error(&cpu_acc),
gpu_standard_error: standard_error(&gpu_acc),
cpu_gpu_z: [
z_score(&cpu_acc.x, &gpu_acc.x),
z_score(&cpu_acc.y, &gpu_acc.y),
z_score(&cpu_acc.z, &gpu_acc.z),
],
total_cpu_samples: cpu_flat.len(),
total_gpu_samples: gpu_xyz.len() / 3,
}
}
fn standard_error(acc: &WelfordXyz) -> Vec3 {
Vec3::new(
acc.x.standard_error_sq().sqrt() as f32,
acc.y.standard_error_sq().sqrt() as f32,
acc.z.standard_error_sq().sqrt() as f32,
)
}
impl FurnaceResult {
#[must_use]
pub fn passed(&self) -> bool {
self.passed_with_tolerance(FURNACE_CONVERGENCE_TOLERANCE)
}
#[must_use]
pub fn passed_with_tolerance(&self, relative_error_tolerance: f32) -> bool {
self.cpu_relative_error <= relative_error_tolerance
&& self.gpu_relative_error <= relative_error_tolerance
&& self.cpu_gpu_z.iter().all(|z| z.abs() <= FURNACE_Z_GATE)
}
#[must_use]
pub fn passed_frosted_girdle(&self) -> bool {
self.passed_with_tolerance(FROSTED_FURNACE_CONVERGENCE_TOLERANCE)
}
#[must_use]
pub fn passed_scattering(&self) -> bool {
self.passed_with_tolerance(SCATTERING_FURNACE_CONVERGENCE_TOLERANCE)
}
#[must_use]
pub fn passed_edge_rounding(&self) -> bool {
self.passed_with_tolerance(EDGE_ROUNDING_FURNACE_CONVERGENCE_TOLERANCE)
}
}
fn analytic_furnace_target(l0: f32) -> Vec3 {
let mut sum = Vec3::ZERO;
for step in 0..=(780 - 380) {
let lambda = 380.0f32 + step as f32;
let spec = rgb_to_spectral_radiance([l0, l0, l0], lambda);
sum += Vec3::from_array(cie_1931_cmf(lambda)) * spec;
}
sum / crate::color::cie1931::CIE_1931_Y_INTEGRAL_5NM
}
fn componentwise_relative_error(value: Vec3, target: Vec3) -> f32 {
let dx = (value.x - target.x).abs() / target.x.abs().max(1e-6);
let dy = (value.y - target.y).abs() / target.y.abs().max(1e-6);
let dz = (value.z - target.z).abs() / target.z.abs().max(1e-6);
dx.max(dy).max(dz)
}