use crate::{
MainWindow, TiltModel,
bridge::render_thread::{RenderContext, resolve_material},
};
use glam::Vec3;
use indicatrix::{
color::metrics::PROFILE_AZIMUTHS_DEG,
geometry::plane::GpuFacetPlane,
optics::{
materials::GemMaterial,
raytracer::{Camera, pixel_rotations, sample_draws, trace_spectral_ray_with_finish},
},
renderer::tonemap::tonemap_to_rgba,
};
use slint::{ComponentHandle, Rgba8Pixel, SharedPixelBuffer};
use std::{
sync::{
Arc, Mutex,
atomic::{AtomicU64, Ordering},
},
time::Duration,
};
const PREVIEW_SIZE: u32 = 96;
const PREVIEW_SPP: u32 = 2;
const HOVER_DEBOUNCE: Duration = Duration::from_millis(90);
fn camera_pose_for_hover(axis_index: usize, tilt_deg: f32) -> (f32, f32) {
let base_azimuth_deg = PROFILE_AZIMUTHS_DEG.get(axis_index).copied().unwrap_or(0.0);
let azimuth_deg = if tilt_deg < 0.0 {
base_azimuth_deg + 180.0
} else {
base_azimuth_deg
};
let pitch_deg = 90.0 - tilt_deg.abs();
(azimuth_deg.to_radians(), pitch_deg.to_radians())
}
struct HoverPreviewScene<'a> {
planes: &'a [GpuFacetPlane],
material: &'a GemMaterial,
cam_yaw: f32,
cam_pitch: f32,
distance: f32,
lighting_preset: indicatrix::optics::raytracer::LightingPreset,
exposure: f32,
light_yaw: f32,
light_pitch: f32,
max_bounces: u32,
}
fn render_hover_preview(scene: &HoverPreviewScene<'_>) -> SharedPixelBuffer<Rgba8Pixel> {
let width = PREVIEW_SIZE;
let height = PREVIEW_SIZE;
let camera = Camera::new(scene.cam_yaw, scene.cam_pitch, scene.distance, 42.0);
let environment =
scene
.lighting_preset
.studio(scene.exposure, scene.light_yaw, scene.light_pitch);
let mut accum = vec![Vec3::ZERO; (width * height) as usize];
for y in 0..height {
for (x, pixel) in accum
.iter_mut()
.skip((y * width) as usize)
.take(width as usize)
.enumerate()
{
let global_pixel_idx = y * width + x as u32;
let rot = pixel_rotations(global_pixel_idx);
let mut sample_sum = Vec3::ZERO;
for sample_num in 0..PREVIEW_SPP {
let draws = sample_draws(global_pixel_idx, sample_num, &rot);
let ray = camera.generate_ray(
x as f32,
y as f32,
width as f32,
height as f32,
draws.jitter_x,
draws.jitter_y,
);
sample_sum += trace_spectral_ray_with_finish(
ray,
scene.planes,
&[],
scene.material,
scene.max_bounces,
environment,
draws.seed,
draws.hero_rand,
None,
);
}
*pixel = sample_sum;
}
}
let rgba = tonemap_to_rgba(&accum, 1.0 / PREVIEW_SPP as f32);
let mut buffer = SharedPixelBuffer::<Rgba8Pixel>::new(width, height);
let dst = buffer.make_mut_slice();
let src: &[Rgba8Pixel] = bytemuck::cast_slice(&rgba);
dst.copy_from_slice(src);
buffer
}
pub(in crate::gui) fn setup_tilt_hover_preview_callback(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
) {
let generation = Arc::new(AtomicU64::new(0));
let ui_weak = ui.as_weak();
let render_ctx = render_ctx.clone();
ui.global::<TiltModel>().on_request_tilt_hover_preview(
move |axis_index: i32, tilt_deg: f32| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
if axis_index < 0 {
return;
}
let my_generation = generation.fetch_add(1, Ordering::SeqCst) + 1;
let generation = generation.clone();
let render_ctx = render_ctx.clone();
let ui_weak_bg = ui.as_weak();
let axis_index = axis_index as usize;
std::thread::spawn(move || {
std::thread::sleep(HOVER_DEBOUNCE);
if generation.load(Ordering::SeqCst) != my_generation {
return;
}
let (
planes,
material_name,
custom_materials,
distance,
lighting_preset,
exposure,
light_yaw,
light_pitch,
max_bounces,
) = {
let ctx = render_ctx.lock().unwrap();
(
ctx.active_planes.clone(),
ctx.material_name.clone(),
ctx.custom_materials.clone(),
ctx.distance,
ctx.lighting_preset,
ctx.exposure,
ctx.light_yaw,
ctx.light_pitch,
ctx.max_bounces,
)
};
let material = resolve_material(
&GemMaterial::all_materials(),
&custom_materials,
&material_name,
);
let (cam_yaw, cam_pitch) = camera_pose_for_hover(axis_index, tilt_deg);
let buffer = render_hover_preview(&HoverPreviewScene {
planes: &planes,
material: &material,
cam_yaw,
cam_pitch,
distance,
lighting_preset,
exposure,
light_yaw,
light_pitch,
max_bounces,
});
let _ = ui_weak_bg.upgrade_in_event_loop(move |ui| {
if generation.load(Ordering::SeqCst) != my_generation {
return;
}
ui.global::<TiltModel>()
.set_hover_preview_image(slint::Image::from_rgba8(buffer));
});
});
},
);
}
#[cfg(test)]
mod tests {
use super::camera_pose_for_hover;
use indicatrix::color::metrics::PROFILE_AZIMUTHS_DEG;
#[test]
fn positive_tilt_uses_the_axis_own_azimuth() {
let (yaw, pitch) = camera_pose_for_hover(1, 30.0);
assert!((yaw - PROFILE_AZIMUTHS_DEG[1].to_radians()).abs() < 1e-6);
assert!((pitch - (90.0 - 30.0f32).to_radians()).abs() < 1e-6);
}
#[test]
fn negative_tilt_uses_the_opposite_azimuth_and_positive_pitch() {
let (yaw, pitch) = camera_pose_for_hover(2, -25.0);
let expected_yaw = (PROFILE_AZIMUTHS_DEG[2] + 180.0).to_radians();
assert!((yaw - expected_yaw).abs() < 1e-6);
assert!((pitch - (90.0 - 25.0f32).to_radians()).abs() < 1e-6);
}
#[test]
fn zero_tilt_is_table_up_on_the_positive_azimuth() {
let (yaw, pitch) = camera_pose_for_hover(0, 0.0);
assert!((yaw - PROFILE_AZIMUTHS_DEG[0].to_radians()).abs() < 1e-6);
assert!((pitch - 90.0f32.to_radians()).abs() < 1e-6);
}
#[test]
fn extreme_tilt_is_edge_on() {
let (positive_yaw, positive_pitch) = camera_pose_for_hover(3, 90.0);
assert!((positive_yaw - PROFILE_AZIMUTHS_DEG[3].to_radians()).abs() < 1e-6);
assert!(positive_pitch.abs() < 1e-6);
let (negative_yaw, negative_pitch) = camera_pose_for_hover(3, -90.0);
assert!((negative_yaw - (PROFILE_AZIMUTHS_DEG[3] + 180.0).to_radians()).abs() < 1e-6);
assert!(negative_pitch.abs() < 1e-6);
}
#[test]
fn out_of_range_axis_index_falls_back_to_azimuth_zero() {
let (yaw, _pitch) = camera_pose_for_hover(99, 10.0);
assert!((yaw - 0.0f32.to_radians()).abs() < 1e-6);
}
}