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_gpu_z: [f64; 3],
pub total_cpu_samples: usize,
pub total_gpu_samples: usize,
}
const FURNACE_CONVERGENCE_TOLERANCE: f32 = 0.02;
const FURNACE_Z_GATE: f64 = 4.0;
const FROSTED_FURNACE_CONVERGENCE_TOLERANCE: f32 = 0.06;
const SCATTERING_FURNACE_CONVERGENCE_TOLERANCE: f32 = 0.08;
const EDGE_ROUNDING_FURNACE_CONVERGENCE_TOLERANCE: f32 = 0.06;
#[must_use]
pub fn run_furnace(ctx: &crate::renderer::gpu::GpuContext) -> FurnaceResult {
run_furnace_for(ctx, &furnace_material(), &[])
}
#[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),
)
}
#[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, &[])
}
#[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, &[])
}
#[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)
}
#[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,
)
}
fn run_furnace_for(
ctx: &crate::renderer::gpu::GpuContext,
material: &GemMaterial,
facet_finishes: &[FacetFinish],
) -> FurnaceResult {
run_furnace_for_env(ctx, material, facet_finishes, false)
}
fn run_furnace_for_env(
ctx: &crate::renderer::gpu::GpuContext,
material: &GemMaterial,
facet_finishes: &[FacetFinish],
use_hdr_nee: bool,
) -> 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 max_bounces = 12u32;
let l0 = 2.5f32;
let cpu_samples_per_pixel = 600u32;
let gpu_samples_per_pixel = 600u32;
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, 0, |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,
cpu_samples_per_pixel,
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,
)
};
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_dispatch.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_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_dispatch.xyz.len() / 3,
}
}
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 / 106.856
}
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)
}