use glam::Vec3;
use crate::{
optics::raytracer::{LightingModel, LightingPreset, sample_studio_environment_observed},
renderer::gpu::compute,
};
use super::{SHADER_SRC, UlpAccumulator, UlpCheckResult, fibonacci_sphere};
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct StudioEnvCase {
dir: [f32; 3],
lambda_nm: f32,
temp_k: f32,
spot_mult: f32,
exposure: f32,
light_yaw: f32,
light_pitch: f32,
model: f32,
_pad1: f32,
use_d65: f32,
observer: [f32; 3],
_pad2: f32,
}
const _: () = assert!(size_of::<StudioEnvCase>() == 64);
pub const STUDIO_ENV_ULP_BUDGET: u32 = 8192;
pub const STUDIO_ENV_ABS_FLOOR: f32 = 1e-4;
#[must_use]
pub fn build_studio_env_cases() -> Vec<StudioEnvCase> {
let mut cases = Vec::new();
let directions = fibonacci_sphere(256);
let poses = [(0.3f32, 0.6f32), (0.85, 0.95), (-0.5, 1.2)];
let exposures = [0.5f32, 1.0, 2.0];
let lambdas = [400.0f32, 500.0, 560.0, 650.0, 700.0];
let lit_observer = Vec3::new(0.2, 0.9, -0.3).normalize();
let no_observer = [Vec3::ZERO];
let lit_observers = [Vec3::ZERO, lit_observer];
for preset in LightingPreset::ALL {
let params = preset.params();
let use_d65 = f32::from(preset.uses_d65());
let model = preset.model().gpu_id() as f32;
let observers: &[Vec3] = if preset.model() == LightingModel::Studio {
&no_observer
} else {
&lit_observers
};
for &observer in observers {
for &(light_yaw, light_pitch) in &poses {
for &exposure in &exposures {
for &dir in &directions {
for &lambda_nm in &lambdas {
cases.push(StudioEnvCase {
dir: dir.to_array(),
lambda_nm,
temp_k: params.temp_k,
spot_mult: params.spot_mult,
exposure,
light_yaw,
light_pitch,
model,
_pad1: 0.0,
use_d65,
observer: observer.to_array(),
_pad2: 0.0,
});
}
}
}
}
}
if preset.model() != LightingModel::Studio {
push_head_shadow_cases(&mut cases, preset, lit_observer);
}
}
for &(light_yaw, light_pitch) in &poses {
let rig = crate::optics::studio_rig::StudioRig::new(light_yaw, light_pitch);
for dir in [rig.key_dir, rig.fill_dir].into_iter().chain(rig.ring_dirs) {
cases.push(StudioEnvCase {
dir: dir.to_array(),
lambda_nm: 560.0,
temp_k: preset_temp(0),
spot_mult: 1.0,
exposure: 1.0,
light_yaw,
light_pitch,
model: 0.0,
_pad1: 0.0,
use_d65: 1.0,
observer: [0.0; 3],
_pad2: 0.0,
});
}
let ring0 = rig.ring_dirs[0];
for &scale in &[0.999f32, 1.001] {
let perturbed = (ring0 * scale + Vec3::new(1e-3, 0.0, 0.0)).normalize();
cases.push(StudioEnvCase {
dir: perturbed.to_array(),
lambda_nm: 560.0,
temp_k: preset_temp(2),
spot_mult: LightingPreset::RingLights.params().spot_mult,
exposure: 1.0,
light_yaw,
light_pitch,
model: 0.0,
_pad1: 0.0,
use_d65: 0.0,
observer: [0.0; 3],
_pad2: 0.0,
});
}
}
cases
}
const fn preset_temp(index: i32) -> f32 {
LightingPreset::from_index(index).params().temp_k
}
fn push_head_shadow_cases(cases: &mut Vec<StudioEnvCase>, preset: LightingPreset, observer: Vec3) {
let params = preset.params();
let perp = observer.cross(Vec3::X).normalize();
for degrees in [0.0f32, 12.0, 16.0, 20.0] {
let (sin_a, cos_a) = degrees.to_radians().sin_cos();
let dir = observer.mul_add(Vec3::splat(cos_a), perp * sin_a);
cases.push(StudioEnvCase {
dir: dir.to_array(),
lambda_nm: 560.0,
temp_k: params.temp_k,
spot_mult: params.spot_mult,
exposure: 1.0,
light_yaw: 0.3,
light_pitch: 0.6,
model: preset.model().gpu_id() as f32,
_pad1: 0.0,
use_d65: f32::from(preset.uses_d65()),
observer: observer.to_array(),
_pad2: 0.0,
});
}
}
#[must_use]
pub fn run_studio_env(ctx: &crate::renderer::gpu::GpuContext) -> UlpCheckResult<StudioEnvCase> {
let cases = build_studio_env_cases();
let total = cases.len();
let in_buf = compute::upload(
&ctx.device,
"studio_env cases",
&cases,
wgpu::BufferUsages::STORAGE,
);
let out_buf = compute::zeroed_buffer::<f32>(
&ctx.device,
"studio_env out",
total,
wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
);
let pipeline = compute::create_compute_pipeline(
&ctx.device,
"studio_env_main",
SHADER_SRC,
"studio_env_main",
);
let bind_group = compute::bind_buffers(
&ctx.device,
"studio_env bind group",
&pipeline,
&[(4, &in_buf), (5, &out_buf)],
);
let workgroups = (total as u32).div_ceil(64);
compute::dispatch_and_wait(
&ctx.device,
&ctx.queue,
&pipeline,
&bind_group,
(workgroups, 1, 1),
);
let gpu_out: Vec<f32> = compute::readback(&ctx.device, &ctx.queue, &out_buf, total);
let mut acc = UlpAccumulator::new(
"sample_studio_environment",
STUDIO_ENV_ULP_BUDGET,
STUDIO_ENV_ABS_FLOOR,
);
for (idx, case) in cases.iter().enumerate() {
let cpu = sample_studio_environment_observed(
Vec3::from_array(case.dir),
case.lambda_nm,
preset_for_case(case),
case.exposure,
case.light_yaw,
case.light_pitch,
Vec3::from_array(case.observer),
);
acc.record(case, "radiance", cpu, gpu_out[idx]);
}
acc.finish()
}
fn preset_for_case(case: &StudioEnvCase) -> LightingPreset {
let model_id = case.model as u32;
match model_id {
1 => LightingPreset::IsoHemisphere,
2 => LightingPreset::LightTent,
3 => LightingPreset::DaylightDome,
_ => LightingPreset::from_index(preset_index_for(case.temp_k)),
}
}
fn preset_index_for(temp_k: f32) -> i32 {
LightingPreset::ALL
.iter()
.position(|p| (p.params().temp_k - temp_k).abs() < 1e-6)
.map_or(0, |i| i as i32)
}