diff --git a/src/lib.rs b/src/lib.rs index df375e9..8b7384d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,12 +2,14 @@ use std::{convert::identity, error::Error, f32::consts::PI}; -use glam::{Mat4, UVec2, Vec3, vec3}; +use glam::{Mat4, UVec2, Vec2, Vec3, vec3}; +use rand_distr::Distribution as _; use crate::{ camera::OrbitalCamera, + ray::Ray, render::lines::{LookParams, Mesh, Pipeline, Vertex}, - trace::{Scene, Source, Sphere}, + trace::{Hit, Lambertian, Reflector, Scene, Source, Sphere}, }; mod camera; @@ -32,6 +34,16 @@ pub struct RedrawArgs { pub light_position: SphericalPosition, pub light_radius: f32, pub light_spread: f32, + pub accum_sigma: f32, + pub accum_scale: f32, + pub reflections: u32, + pub show_axes: bool, + pub show_shapes: bool, + pub show_hit_emission: bool, + pub show_miss_emission: bool, + pub show_direct_hit: bool, + pub show_indirect_hit: bool, + pub show_light: bool, } pub struct Gpu { @@ -84,6 +96,22 @@ fn loop_list_ex( } } +impl OrbitalCamera { + fn make_ray(&self, rng: &mut impl rand::Rng) -> Ray { + let off: f32 = rand_distr::StandardUniform.sample(rng); + let side: Vec2 = rand_distr::UnitCircle.sample(rng).into(); + + let m = self.transform().inverse(); + let fwd = 1. - 0.1 * off; + let side_scale = (1. - fwd.powi(2)).sqrt(); + let dir = Vec3::from((side_scale * side, fwd)); + Ray { + base: self.position(), + dir: m.transform_vector3(dir), + } + } +} + impl Core { pub fn new(gpu: Gpu) -> Self { let Gpu { @@ -136,19 +164,16 @@ impl Core { depth_slice: None, resolve_target: None, ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color { - r: 0.1, - g: 0.2, - b: 0.8, - a: 1.0, - }), + load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: wgpu::StoreOp::Store, }, })], depth_stencil_attachment: None, ..Default::default() }); - self.pipeline.render(&mut pass, [&self.tripod]); + if args.show_axes { + self.pipeline.render(&mut pass, [&self.tripod]); + } let source = Source { position_yaw: args.light_position.yaw, @@ -158,14 +183,16 @@ impl Core { spread: args.light_spread, }; - let contour: Vec = loop_list(source.contour(17)) - .map(|pos| Vertex { - pos, - color: vec3(1., 1., 1.), - }) - .collect(); - self.pipeline - .render(&mut pass, [&Mesh::new(&self.device, &contour)]); + if args.show_shapes { + let contour: Vec = loop_list(source.contour(17)) + .map(|pos| Vertex { + pos, + color: vec3(1., 1., 1.), + }) + .collect(); + self.pipeline + .render(&mut pass, [&Mesh::new(&self.device, &contour)]); + } const BASE_R: f32 = 2.; const BASE_POS: Vec3 = vec3(0., 0., -BASE_R); @@ -189,22 +216,40 @@ impl Core { }; let mut prng = rand_pcg::Pcg64::new(42, 0); - let rays: Vec = (0..10000) - .flat_map(|_| { - let ray = source.make_ray(&mut prng); - if let Some(ray) = scene.trace_ray(ray) { - [ + let source_rays: Vec = (0..10240).map(|_| source.make_ray(&mut prng)).collect(); + let camera_rays: Vec = (0..10240).map(|_| camera.make_ray(&mut prng)).collect(); + let mut source_ray_display: Vec = Vec::with_capacity(source_rays.len()); + let mut hits: Vec = Vec::with_capacity(source_rays.len()); + for ray in source_rays { + if let Some(hit) = scene.trace_ray(ray) { + hits.push(hit); + if args.show_hit_emission { + source_ray_display.extend([ Vertex { - pos: ray.base - 0.02 * ray.dir, + pos: ray.base, color: vec3(1., 1., 1.), }, Vertex { - pos: ray.base, + pos: ray.base + 0.1 * ray.dir, color: vec3(0., 1., 0.), }, - ] - } else { - [ + ]); + } + if args.show_direct_hit { + source_ray_display.extend([ + Vertex { + pos: hit.incident.base - 0.02 * hit.incident.dir, + color: vec3(0., 0., 1.), + }, + Vertex { + pos: hit.incident.base, + color: vec3(1., 1., 1.), + }, + ]); + } + } else { + if args.show_miss_emission { + source_ray_display.extend([ Vertex { pos: ray.base, color: vec3(1., 1., 1.), @@ -213,12 +258,89 @@ impl Core { pos: ray.base + 0.1 * ray.dir, color: vec3(1., 0., 0.), }, - ] + ]); } - }) - .collect(); - self.pipeline - .render(&mut pass, [&Mesh::new(&self.device, &rays)]); + } + } + if args.reflections > 0 { + let mut hits1 = hits.clone(); + for _ in 0..args.reflections { + let mut hits2: Vec = Vec::with_capacity(hits1.len()); + for hit in &hits1 { + let reflector = Lambertian; + let reflected = reflector.reflect(&mut prng, hit.normal, hit.incident.dir); + let ray = Ray::new(hit.incident.base, reflected); + let Some(hit2) = scene.trace_ray(ray) else { + continue; + }; + hits2.push(hit2); + if args.show_indirect_hit { + source_ray_display.extend([ + Vertex { + pos: hit2.incident.base - 0.02 * hit2.incident.dir, + color: vec3(1., 0., 1.), + }, + Vertex { + pos: hit2.incident.base, + color: vec3(1., 1., 1.), + }, + ]); + } + } + hits.extend(&hits2); + hits1 = hits2; + } + } + let mut camera_ray_display: Vec = Vec::with_capacity(camera_rays.len()); + if args.show_light { + let sigma2 = args.accum_sigma.powi(2); + let accum_normalizator = (2. * PI * sigma2).sqrt().recip(); + for ray in camera_rays { + let Some(hit) = scene.trace_ray(ray) else { + continue; + }; + let mut total_cd = 0.0f32; + for light_hit in &hits { + let d2 = hit.incident.base.distance_squared(light_hit.incident.base); + if d2 > 9. * sigma2 { + continue; + } + assert!(hit.normal.is_normalized()); + assert!(hit.incident.dir.is_normalized()); + let reflector = Lambertian; + let in_lm = 1.0; + let out_cd = in_lm + * hit.normal.dot(-hit.incident.dir) + * reflector.brdf(hit.normal, hit.incident.dir, -ray.dir); + let weight = accum_normalizator * (-0.5 * d2 / sigma2).exp(); + total_cd += weight * out_cd; + } + let brightness = 3. * (1. - (1. + total_cd * args.accum_scale).recip()); + let r = args.accum_sigma; + let color = vec3(brightness, brightness - 1., brightness - 2.) + .clamp(Vec3::splat(0.), Vec3::splat(1.)); + let vertex = |off: Vec3| Vertex { + pos: hit.incident.base + r * off, + color, + }; + camera_ray_display.extend([ + vertex(-Vec3::X), + vertex(Vec3::X), + vertex(-Vec3::Y), + vertex(Vec3::Y), + vertex(-Vec3::Z), + vertex(Vec3::Z), + ]); + } + } + if !source_ray_display.is_empty() { + self.pipeline + .render(&mut pass, [&Mesh::new(&self.device, &source_ray_display)]); + } + if !camera_ray_display.is_empty() { + self.pipeline + .render(&mut pass, [&Mesh::new(&self.device, &camera_ray_display)]); + } drop(pass); self.queue.submit(std::iter::once(encoder.finish())); diff --git a/src/main.rs b/src/main.rs index 615a203..f08bf16 100644 --- a/src/main.rs +++ b/src/main.rs @@ -50,6 +50,16 @@ impl MainWindow { }, light_radius: 0.125, light_spread: 0.125, + accum_sigma: 0.025, + accum_scale: 0.01, + reflections: 2, + show_axes: true, + show_shapes: true, + show_hit_emission: false, + show_miss_emission: false, + show_direct_hit: false, + show_indirect_hit: false, + show_light: true, }), _ => {} } diff --git a/src/trace.rs b/src/trace.rs index b1945c2..984f9d9 100644 --- a/src/trace.rs +++ b/src/trace.rs @@ -1,7 +1,7 @@ use std::f32::consts::PI; use glam::{Mat4, Vec2, Vec3, vec3}; -use rand_distr::Distribution; +use rand_distr::{Distribution, UnitSphere}; use crate::{camera::OrbitalCamera, ray::Ray}; @@ -78,8 +78,14 @@ pub struct Sphere { pub radius: f32, } +struct Hit1 { + pos: Vec3, + dist: f32, + normal: Vec3, +} + impl Sphere { - fn trace_ray(&self, ray: Ray) -> Option { + fn trace_ray(&self, ray: Ray) -> Option { // let t: f32; // let hit = ray.base + t * ray.dir; // (hit - self.position).length() == self.radius; @@ -94,7 +100,10 @@ impl Sphere { if d4 < 0. { return None; } - Some((-b2 - d4.sqrt()) / a) + let dist = (-b2 - d4.sqrt()) / a; + let pos = ray.advance(dist).base; + let normal = (pos - self.position).normalize(); + Some(Hit1 { pos, dist, normal }) } } @@ -103,13 +112,51 @@ pub struct Scene { pub objects: Vec, } +#[derive(Debug, Clone, Copy)] +pub struct Hit { + pub incident: Ray, + pub normal: Vec3, +} + impl Scene { - pub fn trace_ray(&self, ray: Ray) -> Option { - let dist = self + pub fn trace_ray(&self, ray: Ray) -> Option { + const EPS: f32 = -1e-3; + let hit = self .objects .iter() .filter_map(|obj| obj.trace_ray(ray)) - .min_by(f32::total_cmp); - Some(ray.advance(dist?)) + .filter(|h| h.dist >= EPS) + .min_by(|a, b| f32::total_cmp(&a.dist, &b.dist))?; + Some(Hit { + incident: Ray { + base: hit.pos, + dir: ray.dir, + }, + normal: hit.normal, + }) + } +} + +pub trait Reflector { + fn brdf(&self, normal: Vec3, incident: Vec3, reflected: Vec3) -> f32 /* 1/sr */; + fn reflect(&self, rgen: &mut impl rand::Rng, normal: Vec3, incident: Vec3) -> Vec3; +} + +pub struct Lambertian; + +impl Reflector for Lambertian { + fn brdf(&self, _normal: Vec3, _incident: Vec3, _reflected: Vec3) -> f32 { + 1. / PI + } + + fn reflect(&self, rgen: &mut impl rand::Rng, normal: Vec3, _incident: Vec3) -> Vec3 { + let sphere: Vec3 = UnitSphere.sample(rgen).into(); + let sphere_n = normal.dot(sphere); // uniform on [-1, 1]! + let sphere_t = sphere - sphere_n * normal; + + let out_n_len2 = sphere_n.abs(); + let out_t = (1. + out_n_len2).recip().sqrt() * sphere_t; + let out_n = out_n_len2.sqrt() * normal; + out_t + out_n } } diff --git a/ui/src/api.hxx b/ui/src/api.hxx index 50b9e67..caf32cd 100644 --- a/ui/src/api.hxx +++ b/ui/src/api.hxx @@ -8,16 +8,26 @@ namespace ffi { struct Core; struct SphericalPosition { - float yaw; - float pitch; - float distance; + float yaw = 0.; + float pitch = 0.; + float distance = 1.; }; struct RedrawArgs { SphericalPosition camera_position; SphericalPosition light_position; - float light_radius; - float light_spread; + float light_radius = 1.; + float light_spread = 0.; + float accum_sigma = 1.; + float accum_scale = 1.; + std::uint32_t reflections = 0; + bool show_axes = true; + bool show_shapes = true; + bool show_hit_emission = true; + bool show_miss_emission = true; + bool show_direct_hit = true; + bool show_indirect_hit = true; + bool show_light = true; }; } // namespace ffi diff --git a/ui/src/main_window.cxx b/ui/src/main_window.cxx index 913d406..dff2f7d 100644 --- a/ui/src/main_window.cxx +++ b/ui/src/main_window.cxx @@ -16,20 +16,39 @@ float deg_to_rad(float val) { } void PhotonLight::updateView() { - m_ui->viewport->setView(RedrawArgs{ + RedrawArgs args{ .camera_position = SphericalPosition{ .yaw = deg_to_rad(m_ui->cameraYaw->value()), .pitch = deg_to_rad(m_ui->cameraPitch->value()), - .distance = 3.0, + .distance = m_ui->cameraDistance->value() / 10.0f, }, .light_position = SphericalPosition{ .yaw = deg_to_rad(m_ui->lightYaw->value()), .pitch = deg_to_rad(m_ui->lightPitch->value()), - .distance = 1.0, + .distance = m_ui->lightDistance->value() / 10.0f, }, .light_radius = 0.125, .light_spread = 0.125, - }); + .accum_sigma = exp10f(m_ui->accumSigma->value() / 25.0), + .accum_scale = exp10f(m_ui->accumScale->value() / 25.0), + .reflections = std::uint32_t(m_ui->reflections->value()), + .show_axes = m_ui->displayAxes->isChecked(), + .show_shapes = m_ui->displayShapes->isChecked(), + .show_hit_emission = m_ui->displayEmitted->isChecked(), + .show_miss_emission = m_ui->displayEmitted->isChecked(), + .show_direct_hit = m_ui->displayDirectHits->isChecked(), + .show_indirect_hit = m_ui->displayIndirectHits->isChecked(), + .show_light = m_ui->displayResult->isChecked(), + }; + m_ui->cameraYawLabel->setText(tr("Yaw: %1 deg").arg(QString::number(qRadiansToDegrees(args.camera_position.yaw)))); + m_ui->cameraPitchLabel->setText(tr("Pitch: %1 deg").arg(QString::number(qRadiansToDegrees(args.camera_position.pitch)))); + m_ui->cameraDistanceLabel->setText(tr("Distance: %1").arg(QString::number(args.camera_position.distance))); + m_ui->lightYawLabel->setText(tr("Yaw: %1 deg").arg(QString::number(qRadiansToDegrees(args.light_position.yaw)))); + m_ui->lightPitchLabel->setText(tr("Pitch: %1 deg").arg(QString::number(qRadiansToDegrees(args.light_position.pitch)))); + m_ui->lightDistanceLabel->setText(tr("Distance: %1").arg(QString::number(args.light_position.distance))); + m_ui->accumSigmaLabel->setText(tr("Averaging radius: %1").arg(QString::number(args.accum_sigma, 'f', 3))); + m_ui->accumScaleLabel->setText(tr("Brightness: %1").arg(QString::number(args.accum_scale, 'f', 3))); + m_ui->viewport->setView(args); } #include "moc_main_window.cpp" diff --git a/ui/src/main_window.ui b/ui/src/main_window.ui index 3974303..697c566 100644 --- a/ui/src/main_window.ui +++ b/ui/src/main_window.ui @@ -6,8 +6,8 @@ 0 0 - 800 - 600 + 1600 + 1200 @@ -25,7 +25,7 @@ 0 0 - 800 + 1600 38 @@ -47,10 +47,13 @@ - + Yaw + + cameraYaw + @@ -73,10 +76,13 @@ - + Pitch + + cameraPitch + @@ -98,6 +104,29 @@ + + + + Distance + + + cameraDistance + + + + + + + 50 + + + 30 + + + Qt::Horizontal + + + @@ -108,10 +137,13 @@ - + Yaw + + lightYaw + @@ -131,10 +163,13 @@ - + Pitch + + lightPitch + @@ -159,6 +194,179 @@ + + + + Distance + + + lightDistance + + + + + + + 50 + + + 10 + + + Qt::Horizontal + + + + + + + + + + Lighting + + + + + + Averaging radius + + + accumSigma + + + + + + + -100 + + + 0 + + + -40 + + + Qt::Horizontal + + + + + + + Brightness + + + accumScale + + + + + + + -100 + + + 0 + + + -50 + + + Qt::Horizontal + + + + + + + Reflections + + + reflections + + + + + + + 20 + + + 2 + + + + + + + + + + Show + + + + + + Axes + + + true + + + + + + + Shapes + + + true + + + + + + + Emitted rays + + + false + + + + + + + Direct incident rays + + + false + + + + + + + Indirect incident rays + + + false + + + + + + + Average light + + + true + + + @@ -196,8 +404,8 @@ updateView() - 688 - 159 + 1585 + 169 403 @@ -212,8 +420,8 @@ updateView() - 688 - 215 + 1585 + 225 403 @@ -228,8 +436,8 @@ updateView() - 688 - 319 + 1585 + 385 403 @@ -244,8 +452,8 @@ updateView() - 688 - 375 + 1585 + 441 403 @@ -253,6 +461,182 @@ + + accumSigma + valueChanged(int) + MainWindow + updateView() + + + 1585 + 601 + + + 399 + 299 + + + + + accumScale + valueChanged(int) + MainWindow + updateView() + + + 1585 + 657 + + + 399 + 299 + + + + + cameraDistance + valueChanged(int) + MainWindow + updateView() + + + 1489 + 271 + + + 799 + 599 + + + + + lightDistance + valueChanged(int) + MainWindow + updateView() + + + 1489 + 487 + + + 799 + 599 + + + + + displayAxes + stateChanged(int) + MainWindow + updateView() + + + 1489 + 799 + + + 799 + 599 + + + + + displayDirectHits + stateChanged(int) + MainWindow + updateView() + + + 1489 + 901 + + + 799 + 599 + + + + + displayEmitted + stateChanged(int) + MainWindow + updateView() + + + 1489 + 867 + + + 799 + 599 + + + + + displayIndirectHits + stateChanged(int) + MainWindow + updateView() + + + 1489 + 935 + + + 799 + 599 + + + + + displayResult + stateChanged(int) + MainWindow + updateView() + + + 1489 + 969 + + + 799 + 599 + + + + + displayShapes + stateChanged(int) + MainWindow + updateView() + + + 1489 + 833 + + + 799 + 599 + + + + + reflections + valueChanged(int) + MainWindow + updateView() + + + 1489 + 712 + + + 799 + 599 + + + updateView()