use crate::{
geometry::{
GpuFacetPlane,
cuts::{STANDARD_ROUND_BRILLIANT_GIRDLE_FACETS, StandardGemCuts},
},
optics::{
absorption::AbsorptionTensor,
dispersion::DispersionModel,
materials::{CrystalSystem, GemMaterial, OpticalCharacter},
raytracer::{
Camera, EnvironmentSource, FacetFinish, HERO_WAVELENGTH_ROTATION_STREAM,
PIXEL_JITTER_X_ROTATION_STREAM, PIXEL_JITTER_Y_ROTATION_STREAM,
cranley_patterson_rotate, hash_u32, low_discrepancy_base2, radical_inverse_base,
trace_spectral_ray_with_finish,
},
},
renderer::{
buffers::{GpuCameraParams, GpuGemMaterial, GpuTransportParams, encode_facet_finishes},
gpu::compute,
},
};
use glam::Vec3;
use super::z_stats::{Welford, z_score};
mod determinism;
mod furnace;
#[cfg(test)]
mod furnace_measurement;
mod image_comparison;
mod spectral_debug;
pub use determinism::{DeterminismResult, run_determinism};
pub use furnace::{
FurnaceResult, run_furnace, run_furnace_edge_rounding, run_furnace_frosted_girdle,
run_furnace_nee_equality_frosted, run_furnace_nee_equality_scattering, run_furnace_scattering,
};
pub use image_comparison::{
ImageComparisonResult, run_image_comparison, run_image_comparison_absorption_path_scale,
run_image_comparison_alexandrite, run_image_comparison_biaxial_scattering,
run_image_comparison_daylight_dome, run_image_comparison_edge_rounding,
run_image_comparison_frosted_girdle, run_image_comparison_hdr_nee,
run_image_comparison_iso_hemisphere, run_image_comparison_light_tent,
run_image_comparison_quartz, run_image_comparison_rutile, run_image_comparison_scattering,
run_image_comparison_synthetic_moissanite, run_image_comparison_tanzanite,
run_image_comparison_topaz, run_image_comparison_tourmaline, run_image_comparison_zircon,
run_specialisation_image_comparison,
};
pub use spectral_debug::{SpectralDebugResult, run_spectral_debug};
use crate::renderer::gpu::frame::{self, SHADER_SRC};
#[must_use]
pub fn furnace_material() -> GemMaterial {
GemMaterial {
name: "Phase2 furnace anchor (colourless n=1.5 cubic)".to_string(),
crystal_system: CrystalSystem::Cubic,
optical_character: OpticalCharacter::Isotropic,
dispersion: DispersionModel::Cauchy {
a: 1.5,
b: 0.0,
c: 0.0,
},
birefringence_delta: 0.0,
absorption: AbsorptionTensor::isotropic(vec![]),
c_axis: Vec3::Y,
biaxial_delta_beta_alpha: None,
scattering_sigma_s: 0.0,
scattering_g: 0.0,
edge_rounding_radius: 0.0,
absorption_path_scale: 1.0,
uniaxial_extraordinary_dispersion: None,
}
}
#[must_use]
pub fn tier3_material() -> GemMaterial {
GemMaterial::all_materials()
.into_iter()
.find(|m| m.name == "Spinel")
.expect("\"Spinel\" is a built-in cubic material in GemMaterial::all_materials()")
}
#[must_use]
pub fn test_camera() -> Camera {
Camera::new(0.35, 0.28, 5.0, 18.0)
}
fn round_brilliant_planes() -> Vec<GpuFacetPlane> {
StandardGemCuts::standard_round_brilliant()
}
#[must_use]
fn all_polished_finishes(num_planes: usize) -> Vec<u32> {
encode_facet_finishes(&[], num_planes)
}
#[must_use]
pub(crate) fn bruted_girdle_finishes(num_planes: usize) -> Vec<FacetFinish> {
let mut finishes = vec![FacetFinish::Polished; num_planes];
for i in STANDARD_ROUND_BRILLIANT_GIRDLE_FACETS {
finishes[i] = FacetFinish::Frosted;
}
finishes
}
#[must_use]
pub fn zircon_material() -> GemMaterial {
GemMaterial::all_materials()
.into_iter()
.find(|m| m.name == "Zircon")
.expect("\"Zircon\" is a built-in uniaxial material in GemMaterial::all_materials()")
}
#[must_use]
pub fn quartz_material() -> GemMaterial {
GemMaterial::all_materials()
.into_iter()
.find(|m| m.name == "Quartz")
.expect("\"Quartz\" is a built-in uniaxial material in GemMaterial::all_materials()")
}
#[must_use]
pub fn tourmaline_material() -> GemMaterial {
GemMaterial::all_materials()
.into_iter()
.find(|m| m.name == "Tourmaline")
.expect("\"Tourmaline\" is a built-in uniaxial material in GemMaterial::all_materials()")
}
#[must_use]
pub fn rutile_material() -> GemMaterial {
GemMaterial::all_materials()
.into_iter()
.find(|m| m.name == "Rutile")
.expect("\"Rutile\" is a built-in uniaxial material in GemMaterial::all_materials()")
}
#[must_use]
pub fn alexandrite_material() -> GemMaterial {
GemMaterial::all_materials()
.into_iter()
.find(|m| m.name == "Alexandrite")
.expect("\"Alexandrite\" is a built-in biaxial material in GemMaterial::all_materials()")
}
#[must_use]
pub fn topaz_material() -> GemMaterial {
GemMaterial::all_materials()
.into_iter()
.find(|m| m.name == "Topaz")
.expect("\"Topaz\" is a built-in biaxial material in GemMaterial::all_materials()")
}
#[must_use]
pub fn tanzanite_material() -> GemMaterial {
GemMaterial::all_materials()
.into_iter()
.find(|m| m.name == "Tanzanite")
.expect("\"Tanzanite\" is a built-in biaxial material in GemMaterial::all_materials()")
}
struct TransportDispatch {
xyz: Vec<f32>,
radiance: Vec<f32>,
lambdas: Vec<f32>,
path_pdf: Vec<f32>,
compat: Vec<u32>,
}
#[derive(Clone, Copy)]
struct SceneBuffersForDispatch<'a> {
planes: &'a [GpuFacetPlane],
facet_finishes: &'a [u32],
hdr_env: Option<&'a crate::renderer::env_map_gpu::HdrEnvGpuData>,
}
fn dispatch_transport(
ctx: &crate::renderer::gpu::GpuContext,
camera_params: &GpuCameraParams,
params: &GpuTransportParams,
material: &GpuGemMaterial,
planes: &[GpuFacetPlane],
facet_finishes: &[u32],
total_tuples: usize,
) -> TransportDispatch {
dispatch_transport_for_class(
ctx,
camera_params,
params,
MaterialForDispatch {
encoded: material,
class: frame::material_class::GENERIC,
},
SceneBuffersForDispatch {
planes,
facet_finishes,
hdr_env: None,
},
total_tuples,
)
}
#[derive(Clone, Copy)]
struct MaterialForDispatch<'a> {
encoded: &'a GpuGemMaterial,
class: u32,
}
fn dispatch_transport_for_class(
ctx: &crate::renderer::gpu::GpuContext,
camera_params: &GpuCameraParams,
params: &GpuTransportParams,
material: MaterialForDispatch<'_>,
scene: SceneBuffersForDispatch<'_>,
total_tuples: usize,
) -> TransportDispatch {
let pipeline = compute::create_compute_pipeline_with_constants(
&ctx.device,
"transport_main",
SHADER_SRC,
"transport_main",
&[("MATERIAL_CLASS", f64::from(material.class))],
);
let material = material.encoded;
let outputs = frame::TransportOutputs::new(&ctx.device, total_tuples);
let dummy_hdr = scene
.hdr_env
.is_none()
.then(|| crate::renderer::env_map_gpu::HdrEnvGpuData::dummy(&ctx.device));
let hdr_env = scene
.hdr_env
.or(dummy_hdr.as_ref())
.expect("dummy HDR allocated when scene.hdr_env is None");
frame::encode_and_dispatch(
&frame::TransportDispatchArgs {
ctx,
pipeline: &pipeline,
camera_params,
params,
material,
planes: scene.planes,
facet_finishes: scene.facet_finishes,
outputs: &outputs,
hdr_env,
},
total_tuples,
);
TransportDispatch {
xyz: compute::readback(&ctx.device, &ctx.queue, outputs.xyz(), total_tuples * 3),
radiance: compute::readback(
&ctx.device,
&ctx.queue,
outputs.radiance(),
total_tuples * 8,
),
lambdas: compute::readback(&ctx.device, &ctx.queue, outputs.lambdas(), total_tuples * 8),
path_pdf: compute::readback(
&ctx.device,
&ctx.queue,
outputs.path_pdf(),
total_tuples * 8,
),
compat: compute::readback(&ctx.device, &ctx.queue, outputs.compat(), total_tuples * 8),
}
}
const fn camera_params_for(
camera: &Camera,
width: u32,
height: u32,
samples: u32,
) -> GpuCameraParams {
GpuCameraParams {
origin: camera.origin.to_array(),
fov_tan: camera.fov_tan,
forward: camera.forward.to_array(),
width: width as f32,
right: camera.right.to_array(),
height: height as f32,
up: camera.up.to_array(),
num_samples: samples,
}
}
#[derive(Clone, Copy)]
struct CpuScene<'a> {
camera: &'a Camera,
width: u32,
height: u32,
planes: &'a [GpuFacetPlane],
facet_finishes: &'a [FacetFinish],
material: &'a GemMaterial,
max_bounces: u32,
}
fn cpu_sample_xyz(
scene: &CpuScene<'_>,
pixel: u32,
sample_num: u32,
environment: EnvironmentSource<'_>,
) -> Vec3 {
let CpuScene {
camera,
width,
height,
planes,
facet_finishes,
material,
max_bounces,
} = *scene;
let x = pixel % width;
let y = pixel / width;
let seed = hash_u32(pixel.wrapping_mul(0x9e37_79b9) ^ sample_num.wrapping_mul(0x85eb_ca6b));
let rot_jx = low_discrepancy_base2(hash_u32(pixel ^ PIXEL_JITTER_X_ROTATION_STREAM));
let rot_jy = low_discrepancy_base2(hash_u32(pixel ^ PIXEL_JITTER_Y_ROTATION_STREAM));
let rot_hero = low_discrepancy_base2(hash_u32(pixel ^ HERO_WAVELENGTH_ROTATION_STREAM));
let jx = cranley_patterson_rotate(low_discrepancy_base2(sample_num), rot_jx) - 0.5;
let jy = cranley_patterson_rotate(radical_inverse_base(sample_num, 3), rot_jy) - 0.5;
let hero_rand = cranley_patterson_rotate(radical_inverse_base(sample_num, 5), rot_hero);
let ray = camera.generate_ray(x as f32, y as f32, width as f32, height as f32, jx, jy);
trace_spectral_ray_with_finish(
ray,
planes,
facet_finishes,
material,
max_bounces,
environment,
seed,
hero_rand,
None,
)
}
fn cpu_samples<F>(
width: u32,
height: u32,
samples_per_pixel: u32,
sample_offset: u32,
trace_one: F,
) -> Vec<Vec3>
where
F: Fn(u32, u32) -> Vec3 + Sync,
{
let num_pixels = width * height;
let num_threads = std::thread::available_parallelism()
.map_or(4, std::num::NonZero::get)
.min(16);
let mut out = vec![Vec3::ZERO; (num_pixels as usize) * (samples_per_pixel as usize)];
let rows_per_chunk = (num_pixels as usize).div_ceil(num_threads);
std::thread::scope(|s| {
let chunks: Vec<&mut [Vec3]> = out
.chunks_mut(rows_per_chunk * samples_per_pixel as usize)
.collect();
for (chunk_idx, chunk) in chunks.into_iter().enumerate() {
let start_pixel = (chunk_idx * rows_per_chunk) as u32;
let trace_one = &trace_one;
s.spawn(move || {
for (local_pixel, pixel_slot) in
chunk.chunks_mut(samples_per_pixel as usize).enumerate()
{
let pixel = start_pixel + local_pixel as u32;
if pixel >= num_pixels {
break;
}
for (local_sample, slot) in pixel_slot.iter_mut().enumerate() {
let sample_num = sample_offset + local_sample as u32;
*slot = trace_one(pixel, sample_num);
}
}
});
}
});
out
}
#[derive(Debug, Clone, Copy, Default)]
struct WelfordXyz {
x: Welford,
y: Welford,
z: Welford,
}
impl WelfordXyz {
fn update(&mut self, v: Vec3) {
self.x.update(f64::from(v.x));
self.y.update(f64::from(v.y));
self.z.update(f64::from(v.z));
}
const fn mean(&self) -> Vec3 {
Vec3::new(self.x.mean as f32, self.y.mean as f32, self.z.mean as f32)
}
}