From 8bed95f872d6276b80d5d070a348d295ba486c48 Mon Sep 17 00:00:00 2001 From: numzero Date: Sun, 16 Nov 2025 14:43:31 +0300 Subject: [PATCH 01/36] use mutable vectors, to collect into several at once --- src/lib.rs | 68 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index df375e9..926df41 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ use glam::{Mat4, UVec2, Vec3, vec3}; use crate::{ camera::OrbitalCamera, + ray::Ray, render::lines::{LookParams, Mesh, Pipeline, Vertex}, trace::{Scene, Source, Sphere}, }; @@ -189,36 +190,45 @@ 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) { - [ - Vertex { - pos: ray.base - 0.02 * ray.dir, - color: vec3(1., 1., 1.), - }, - Vertex { - pos: ray.base, - color: vec3(0., 1., 0.), - }, - ] - } else { - [ - Vertex { - pos: ray.base, - color: vec3(1., 1., 1.), - }, - Vertex { - pos: ray.base + 0.1 * ray.dir, - color: vec3(1., 0., 0.), - }, - ] - } - }) - .collect(); + let source_rays: Vec = (0..1024).map(|_| source.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); + source_ray_display.extend([ + Vertex { + pos: ray.base, + color: vec3(1., 1., 1.), + }, + Vertex { + pos: ray.base + 0.1 * ray.dir, + color: vec3(0., 1., 0.), + }, + Vertex { + pos: hit.base - 0.02 * hit.dir, + color: vec3(0., 0., 1.), + }, + Vertex { + pos: hit.base, + color: vec3(1., 1., 1.), + }, + ]); + } else { + source_ray_display.extend([ + Vertex { + pos: ray.base, + color: vec3(1., 1., 1.), + }, + Vertex { + pos: ray.base + 0.1 * ray.dir, + color: vec3(1., 0., 0.), + }, + ]); + } + } self.pipeline - .render(&mut pass, [&Mesh::new(&self.device, &rays)]); + .render(&mut pass, [&Mesh::new(&self.device, &source_ray_display)]); drop(pass); self.queue.submit(std::iter::once(encoder.finish())); From cf65929b99c52b3624b35b7e7f7c641d9c20cf0b Mon Sep 17 00:00:00 2001 From: numzero Date: Sun, 16 Nov 2025 15:15:42 +0300 Subject: [PATCH 02/36] show current values in the ui --- ui/src/main_window.cxx | 9 +++++++-- ui/src/main_window.ui | 8 ++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/ui/src/main_window.cxx b/ui/src/main_window.cxx index 913d406..bdf890f 100644 --- a/ui/src/main_window.cxx +++ b/ui/src/main_window.cxx @@ -16,7 +16,7 @@ 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()), @@ -29,7 +29,12 @@ void PhotonLight::updateView() { }, .light_radius = 0.125, .light_spread = 0.125, - }); + }; + 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->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->viewport->setView(args); } #include "moc_main_window.cpp" diff --git a/ui/src/main_window.ui b/ui/src/main_window.ui index 3974303..26cf5b6 100644 --- a/ui/src/main_window.ui +++ b/ui/src/main_window.ui @@ -47,7 +47,7 @@ - + Yaw @@ -73,7 +73,7 @@ - + Pitch @@ -108,7 +108,7 @@ - + Yaw @@ -131,7 +131,7 @@ - + Pitch From cd0c264c6ee03831ad9da62ddd439142fda7dead Mon Sep 17 00:00:00 2001 From: numzero Date: Sun, 16 Nov 2025 15:25:08 +0300 Subject: [PATCH 03/36] basic two-way tracing --- src/lib.rs | 55 ++++++++++++++++++++++++++-- ui/src/api.hxx | 2 ++ ui/src/main_window.cxx | 4 +++ ui/src/main_window.ui | 81 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 140 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 926df41..457a3f1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,8 @@ 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, @@ -33,6 +34,8 @@ pub struct RedrawArgs { pub light_position: SphericalPosition, pub light_radius: f32, pub light_spread: f32, + pub accum_sigma: f32, + pub accum_scale: f32, } pub struct Gpu { @@ -85,6 +88,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 { @@ -190,7 +209,8 @@ impl Core { }; let mut prng = rand_pcg::Pcg64::new(42, 0); - let source_rays: Vec = (0..1024).map(|_| source.make_ray(&mut prng)).collect(); + let source_rays: Vec = (0..10240).map(|_| source.make_ray(&mut prng)).collect(); + let camera_rays: Vec = (0..1024).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 { @@ -227,8 +247,39 @@ impl Core { ]); } } + let mut camera_ray_display: Vec = Vec::with_capacity(camera_rays.len()); + let sigma2 = args.accum_sigma.powi(2); + let weight = (2. * PI * sigma2).sqrt().recip() * args.accum_scale; + for ray in camera_rays { + let Some(hit) = scene.trace_ray(ray) else { + continue; + }; + let mut value = 0.0f32; + for light_hit in &hits { + let d2 = hit.base.distance_squared(light_hit.base); + let w = (-0.5 * d2 / sigma2).exp(); + value += w; + } + value *= weight; + let r = 0.01; + let color = vec3(value, value - 1., value - 2.).clamp(Vec3::splat(0.), Vec3::splat(1.)); + let vertex = |off: Vec3| Vertex { + pos: hit.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), + ]); + } self.pipeline .render(&mut pass, [&Mesh::new(&self.device, &source_ray_display)]); + 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/ui/src/api.hxx b/ui/src/api.hxx index 50b9e67..19b742c 100644 --- a/ui/src/api.hxx +++ b/ui/src/api.hxx @@ -18,6 +18,8 @@ struct RedrawArgs { SphericalPosition light_position; float light_radius; float light_spread; + float accum_sigma; + float accum_scale; }; } // namespace ffi diff --git a/ui/src/main_window.cxx b/ui/src/main_window.cxx index bdf890f..97d4302 100644 --- a/ui/src/main_window.cxx +++ b/ui/src/main_window.cxx @@ -29,11 +29,15 @@ void PhotonLight::updateView() { }, .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), }; 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->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->accumSigmaLabel->setText(tr("Sigma: %1").arg(QString::number(args.accum_sigma))); + m_ui->accumScaleLabel->setText(tr("Scale: %1").arg(QString::number(args.accum_scale))); m_ui->viewport->setView(args); } diff --git a/ui/src/main_window.ui b/ui/src/main_window.ui index 26cf5b6..aef1ac0 100644 --- a/ui/src/main_window.ui +++ b/ui/src/main_window.ui @@ -162,6 +162,55 @@ + + + + Accumulating + + + + + + Sigma + + + + + + + -100 + + + 100 + + + Qt::Horizontal + + + + + + + Scale + + + + + + + -100 + + + 100 + + + Qt::Horizontal + + + + + + @@ -253,6 +302,38 @@ + + accumSigma + valueChanged(int) + MainWindow + updateView() + + + 729 + 479 + + + 399 + 299 + + + + + accumScale + valueChanged(int) + MainWindow + updateView() + + + 729 + 535 + + + 399 + 299 + + + updateView() From aa5676865545fc4259ee8b3aeed16e2f69296290 Mon Sep 17 00:00:00 2001 From: numzero Date: Sun, 16 Nov 2025 15:40:07 +0300 Subject: [PATCH 04/36] fix dumb ui --- src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main.rs b/src/main.rs index 615a203..e92ba99 100644 --- a/src/main.rs +++ b/src/main.rs @@ -50,6 +50,8 @@ impl MainWindow { }, light_radius: 0.125, light_spread: 0.125, + accum_sigma: 0.025, + accum_scale: 0.01, }), _ => {} } From bb8373fad5b87eba90cb19c34d7e71f4787542c8 Mon Sep 17 00:00:00 2001 From: numzero Date: Sun, 16 Nov 2025 18:04:18 +0300 Subject: [PATCH 05/36] optimize summation --- src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 457a3f1..9ae7d9d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -257,6 +257,9 @@ impl Core { let mut value = 0.0f32; for light_hit in &hits { let d2 = hit.base.distance_squared(light_hit.base); + if d2 > 3. * sigma2 { + continue; + } let w = (-0.5 * d2 / sigma2).exp(); value += w; } From 198dc06d02462e65d9cda09bac7598a9232006d5 Mon Sep 17 00:00:00 2001 From: numzero Date: Sun, 16 Nov 2025 18:05:00 +0300 Subject: [PATCH 06/36] nicer hit rendering --- src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 9ae7d9d..515fb6a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -264,7 +264,8 @@ impl Core { value += w; } value *= weight; - let r = 0.01; + value = 3. * (1. - (1. + value).recip()); + let r = args.accum_sigma; let color = vec3(value, value - 1., value - 2.).clamp(Vec3::splat(0.), Vec3::splat(1.)); let vertex = |off: Vec3| Vertex { pos: hit.base + r * off, From 64925f96404c94ff5be5d4b417ee967188132773 Mon Sep 17 00:00:00 2001 From: numzero Date: Sun, 16 Nov 2025 18:05:14 +0300 Subject: [PATCH 07/36] update defaults --- src/lib.rs | 2 +- ui/src/main_window.ui | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 515fb6a..d535143 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -210,7 +210,7 @@ impl Core { let mut prng = rand_pcg::Pcg64::new(42, 0); let source_rays: Vec = (0..10240).map(|_| source.make_ray(&mut prng)).collect(); - let camera_rays: Vec = (0..1024).map(|_| camera.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 { diff --git a/ui/src/main_window.ui b/ui/src/main_window.ui index aef1ac0..fa9bb16 100644 --- a/ui/src/main_window.ui +++ b/ui/src/main_window.ui @@ -181,7 +181,10 @@ -100 - 100 + 0 + + + -40 Qt::Horizontal @@ -203,6 +206,9 @@ 100 + + -50 + Qt::Horizontal From 12bc173fb28ff1974bf2d62c785e23597589b1a4 Mon Sep 17 00:00:00 2001 From: numzero Date: Sun, 16 Nov 2025 19:39:36 +0300 Subject: [PATCH 08/36] use Lambertian scattering --- src/lib.rs | 16 +++++++++------- src/trace.rs | 33 +++++++++++++++++++++++++++------ 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d535143..7c6ce92 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,7 @@ use crate::{ camera::OrbitalCamera, ray::Ray, render::lines::{LookParams, Mesh, Pipeline, Vertex}, - trace::{Scene, Source, Sphere}, + trace::{Hit, Scene, Source, Sphere}, }; mod camera; @@ -212,7 +212,7 @@ impl Core { 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()); + 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); @@ -226,11 +226,11 @@ impl Core { color: vec3(0., 1., 0.), }, Vertex { - pos: hit.base - 0.02 * hit.dir, + pos: hit.incident.base - 0.02 * hit.incident.dir, color: vec3(0., 0., 1.), }, Vertex { - pos: hit.base, + pos: hit.incident.base, color: vec3(1., 1., 1.), }, ]); @@ -256,19 +256,21 @@ impl Core { }; let mut value = 0.0f32; for light_hit in &hits { - let d2 = hit.base.distance_squared(light_hit.base); + let d2 = hit.incident.base.distance_squared(light_hit.incident.base); if d2 > 3. * sigma2 { continue; } + assert!(hit.incident.dir.is_normalized()); + let radiance = hit.normal.dot(-hit.incident.dir); let w = (-0.5 * d2 / sigma2).exp(); - value += w; + value += w * radiance; } value *= weight; value = 3. * (1. - (1. + value).recip()); let r = args.accum_sigma; let color = vec3(value, value - 1., value - 2.).clamp(Vec3::splat(0.), Vec3::splat(1.)); let vertex = |off: Vec3| Vertex { - pos: hit.base + r * off, + pos: hit.incident.base + r * off, color, }; camera_ray_display.extend([ diff --git a/src/trace.rs b/src/trace.rs index b1945c2..5f461e6 100644 --- a/src/trace.rs +++ b/src/trace.rs @@ -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,25 @@ 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 { + let hit = self .objects .iter() .filter_map(|obj| obj.trace_ray(ray)) - .min_by(f32::total_cmp); - Some(ray.advance(dist?)) + .min_by(|a, b| f32::total_cmp(&a.dist, &b.dist))?; + Some(Hit { + incident: Ray { + base: hit.pos, + dir: ray.dir, + }, + normal: hit.normal, + }) } } From 6ba9add6eae6c26a2a24997b1e07a1060f7c05e0 Mon Sep 17 00:00:00 2001 From: numzero Date: Sun, 16 Nov 2025 20:18:05 +0300 Subject: [PATCH 09/36] a bit better ui --- src/lib.rs | 7 +------ ui/src/main_window.cxx | 4 ++-- ui/src/main_window.ui | 2 +- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 7c6ce92..420e73f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -156,12 +156,7 @@ 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, }, })], diff --git a/ui/src/main_window.cxx b/ui/src/main_window.cxx index 97d4302..511b984 100644 --- a/ui/src/main_window.cxx +++ b/ui/src/main_window.cxx @@ -36,8 +36,8 @@ void PhotonLight::updateView() { m_ui->cameraPitchLabel->setText(tr("Pitch: %1 deg").arg(QString::number(qRadiansToDegrees(args.camera_position.pitch)))); 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->accumSigmaLabel->setText(tr("Sigma: %1").arg(QString::number(args.accum_sigma))); - m_ui->accumScaleLabel->setText(tr("Scale: %1").arg(QString::number(args.accum_scale))); + m_ui->accumSigmaLabel->setText(tr("Sigma: %1").arg(QString::number(args.accum_sigma, 'f', 3))); + m_ui->accumScaleLabel->setText(tr("Scale: %1").arg(QString::number(args.accum_scale, 'f', 3))); m_ui->viewport->setView(args); } diff --git a/ui/src/main_window.ui b/ui/src/main_window.ui index fa9bb16..9688b64 100644 --- a/ui/src/main_window.ui +++ b/ui/src/main_window.ui @@ -204,7 +204,7 @@ -100 - 100 + 0 -50 From 2e6589e01bd69abcfb551d233a88908e81b42c82 Mon Sep 17 00:00:00 2001 From: numzero Date: Tue, 18 Nov 2025 19:45:56 +0300 Subject: [PATCH 10/36] fix threshold --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 420e73f..09f5e5a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -252,7 +252,7 @@ impl Core { let mut value = 0.0f32; for light_hit in &hits { let d2 = hit.incident.base.distance_squared(light_hit.incident.base); - if d2 > 3. * sigma2 { + if d2 > 9. * sigma2 { continue; } assert!(hit.incident.dir.is_normalized()); From cbe0839cd6166981e3835cde6651e4cb46ba5c10 Mon Sep 17 00:00:00 2001 From: numzero Date: Tue, 18 Nov 2025 19:46:08 +0300 Subject: [PATCH 11/36] sort out units --- src/lib.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 09f5e5a..5195e03 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -244,26 +244,27 @@ impl Core { } let mut camera_ray_display: Vec = Vec::with_capacity(camera_rays.len()); let sigma2 = args.accum_sigma.powi(2); - let weight = (2. * PI * sigma2).sqrt().recip() * args.accum_scale; + let accum_normalizator = (2. * PI * sigma2).sqrt().recip(); for ray in camera_rays { let Some(hit) = scene.trace_ray(ray) else { continue; }; - let mut value = 0.0f32; + 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.incident.dir.is_normalized()); - let radiance = hit.normal.dot(-hit.incident.dir); - let w = (-0.5 * d2 / sigma2).exp(); - value += w * radiance; + let in_lm = 1.0; + let out_cd = in_lm * hit.normal.dot(-hit.incident.dir) / PI; + let weight = accum_normalizator * (-0.5 * d2 / sigma2).exp(); + total_cd += weight * out_cd; } - value *= weight; - value = 3. * (1. - (1. + value).recip()); + let brightness = 3. * (1. - (1. + total_cd * args.accum_scale).recip()); let r = args.accum_sigma; - let color = vec3(value, value - 1., value - 2.).clamp(Vec3::splat(0.), Vec3::splat(1.)); + 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, From 713fd25c9c88f03e576fb9a54bf7473bd56c0677 Mon Sep 17 00:00:00 2001 From: numzero Date: Thu, 20 Nov 2025 13:05:02 +0300 Subject: [PATCH 12/36] add generic reflector --- src/trace.rs | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/trace.rs b/src/trace.rs index 5f461e6..baa2633 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}; @@ -134,3 +134,27 @@ impl Scene { }) } } + +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 + } +} From 91473f08aef45434f3d52f8ff09f6868d87f38ac Mon Sep 17 00:00:00 2001 From: numzero Date: Thu, 20 Nov 2025 13:10:21 +0300 Subject: [PATCH 13/36] use generic reflector for final computation --- src/lib.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5195e03..5d2fc5d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,7 @@ use crate::{ camera::OrbitalCamera, ray::Ray, render::lines::{LookParams, Mesh, Pipeline, Vertex}, - trace::{Hit, Scene, Source, Sphere}, + trace::{Hit, Lambertian, Reflector, Scene, Source, Sphere}, }; mod camera; @@ -255,9 +255,13 @@ impl Core { 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) / PI; + 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; } From 93c8652116586c10daaa8d61d3d1af047058eb25 Mon Sep 17 00:00:00 2001 From: numzero Date: Thu, 20 Nov 2025 13:23:04 +0300 Subject: [PATCH 14/36] =?UTF-8?q?ensure=20rays=20don=E2=80=99t=20propagate?= =?UTF-8?q?=20backwards?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/trace.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/trace.rs b/src/trace.rs index baa2633..984f9d9 100644 --- a/src/trace.rs +++ b/src/trace.rs @@ -120,10 +120,12 @@ pub struct Hit { impl Scene { 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)) + .filter(|h| h.dist >= EPS) .min_by(|a, b| f32::total_cmp(&a.dist, &b.dist))?; Some(Hit { incident: Ray { From a7746deec53a9c4ec597289b7475abf5a13e1512 Mon Sep 17 00:00:00 2001 From: numzero Date: Thu, 20 Nov 2025 13:23:19 +0300 Subject: [PATCH 15/36] add basic interreflections --- src/lib.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 5d2fc5d..fa70501 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -242,6 +242,27 @@ impl Core { ]); } } + let mut hits2: Vec = Vec::with_capacity(hits.len()); + for hit in &hits { + 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); + 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); let mut camera_ray_display: Vec = Vec::with_capacity(camera_rays.len()); let sigma2 = args.accum_sigma.powi(2); let accum_normalizator = (2. * PI * sigma2).sqrt().recip(); From 61f19c85f4aa28c43b56c58559aac991e373e471 Mon Sep 17 00:00:00 2001 From: numzero Date: Thu, 20 Nov 2025 13:55:49 +0300 Subject: [PATCH 16/36] add sane-ish defaults to avoid ub --- ui/src/api.hxx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ui/src/api.hxx b/ui/src/api.hxx index 19b742c..c1f3b2a 100644 --- a/ui/src/api.hxx +++ b/ui/src/api.hxx @@ -8,18 +8,18 @@ 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 accum_sigma; - float accum_scale; + float light_radius = 1.; + float light_spread = 0.; + float accum_sigma = 1.; + float accum_scale = 1.; }; } // namespace ffi From 5a69a3a1cb8f9b7d13074ceb1e1ca87ad2365fcf Mon Sep 17 00:00:00 2001 From: numzero Date: Thu, 20 Nov 2025 14:12:31 +0300 Subject: [PATCH 17/36] add custom display options --- src/lib.rs | 199 +++++++++++++++++------------- src/main.rs | 7 ++ ui/src/api.hxx | 7 ++ ui/src/main_window.cxx | 17 ++- ui/src/main_window.ui | 273 ++++++++++++++++++++++++++++++++++++++--- 5 files changed, 395 insertions(+), 108 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index fa70501..ba7dd7e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,6 +36,13 @@ pub struct RedrawArgs { pub light_spread: f32, pub accum_sigma: f32, pub accum_scale: f32, + 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 { @@ -163,7 +170,9 @@ impl Core { 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, @@ -173,14 +182,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); @@ -211,35 +222,43 @@ impl Core { for ray in source_rays { if let Some(hit) = scene.trace_ray(ray) { hits.push(hit); - source_ray_display.extend([ - Vertex { - pos: ray.base, - color: vec3(1., 1., 1.), - }, - Vertex { - pos: ray.base + 0.1 * ray.dir, - color: vec3(0., 1., 0.), - }, - 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.), - }, - ]); + if args.show_hit_emission { + source_ray_display.extend([ + Vertex { + pos: ray.base, + color: vec3(1., 1., 1.), + }, + Vertex { + pos: ray.base + 0.1 * ray.dir, + color: vec3(0., 1., 0.), + }, + ]); + } + 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 { - source_ray_display.extend([ - Vertex { - pos: ray.base, - color: vec3(1., 1., 1.), - }, - Vertex { - pos: ray.base + 0.1 * ray.dir, - color: vec3(1., 0., 0.), - }, - ]); + if args.show_miss_emission { + source_ray_display.extend([ + Vertex { + pos: ray.base, + color: vec3(1., 1., 1.), + }, + Vertex { + pos: ray.base + 0.1 * ray.dir, + color: vec3(1., 0., 0.), + }, + ]); + } } } let mut hits2: Vec = Vec::with_capacity(hits.len()); @@ -251,62 +270,70 @@ impl Core { continue; }; hits2.push(hit2); - 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.), - }, - ]); + 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); let mut camera_ray_display: Vec = Vec::with_capacity(camera_rays.len()); - 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 { + 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; } - 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), + ]); } - 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), - ]); } - self.pipeline - .render(&mut pass, [&Mesh::new(&self.device, &source_ray_display)]); - self.pipeline - .render(&mut pass, [&Mesh::new(&self.device, &camera_ray_display)]); + 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 e92ba99..7f8a128 100644 --- a/src/main.rs +++ b/src/main.rs @@ -52,6 +52,13 @@ impl MainWindow { light_spread: 0.125, accum_sigma: 0.025, accum_scale: 0.01, + 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/ui/src/api.hxx b/ui/src/api.hxx index c1f3b2a..ff7038e 100644 --- a/ui/src/api.hxx +++ b/ui/src/api.hxx @@ -20,6 +20,13 @@ struct RedrawArgs { float light_spread = 0.; float accum_sigma = 1.; float accum_scale = 1.; + 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 511b984..033645f 100644 --- a/ui/src/main_window.cxx +++ b/ui/src/main_window.cxx @@ -20,24 +20,33 @@ void PhotonLight::updateView() { .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), + .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->accumSigmaLabel->setText(tr("Sigma: %1").arg(QString::number(args.accum_sigma, 'f', 3))); - m_ui->accumScaleLabel->setText(tr("Scale: %1").arg(QString::number(args.accum_scale, 'f', 3))); + 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); } diff --git a/ui/src/main_window.ui b/ui/src/main_window.ui index 9688b64..44b57c5 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 @@ -98,6 +98,26 @@ + + + + Distance + + + + + + + 50 + + + 30 + + + Qt::Horizontal + + + @@ -159,19 +179,39 @@ + + + + Distance + + + + + + + 50 + + + 10 + + + Qt::Horizontal + + + - Accumulating + Lighting - Sigma + Averaging radius @@ -194,7 +234,7 @@ - Scale + Brightness @@ -217,6 +257,75 @@ + + + + Show + + + + + + Axes + + + true + + + + + + + Shapes + + + true + + + + + + + Emitted rays + + + false + + + + + + + Direct incident rays + + + false + + + + + + + Indirect incident rays + + + false + + + + + + + Average light + + + true + + + + + + @@ -251,8 +360,8 @@ updateView() - 688 - 159 + 1585 + 169 403 @@ -267,8 +376,8 @@ updateView() - 688 - 215 + 1585 + 225 403 @@ -283,8 +392,8 @@ updateView() - 688 - 319 + 1585 + 385 403 @@ -299,8 +408,8 @@ updateView() - 688 - 375 + 1585 + 441 403 @@ -315,8 +424,8 @@ updateView() - 729 - 479 + 1585 + 601 399 @@ -331,8 +440,8 @@ updateView() - 729 - 535 + 1585 + 657 399 @@ -340,6 +449,134 @@ + + 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 + + + updateView() From e80e1d09fe142cb9508067a7c8f2b92485aa4a56 Mon Sep 17 00:00:00 2001 From: numzero Date: Thu, 20 Nov 2025 14:19:58 +0300 Subject: [PATCH 18/36] add interreflections --- src/lib.rs | 49 ++++++++++++++++++++++++------------------ src/main.rs | 1 + ui/src/api.hxx | 1 + ui/src/main_window.cxx | 1 + ui/src/main_window.ui | 33 ++++++++++++++++++++++++++++ 5 files changed, 64 insertions(+), 21 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index ba7dd7e..8b7384d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,6 +36,7 @@ pub struct RedrawArgs { 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, @@ -261,29 +262,35 @@ impl Core { } } } - let mut hits2: Vec = Vec::with_capacity(hits.len()); - for hit in &hits { - 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.), - }, - ]); + 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; } } - hits.extend(hits2); let mut camera_ray_display: Vec = Vec::with_capacity(camera_rays.len()); if args.show_light { let sigma2 = args.accum_sigma.powi(2); diff --git a/src/main.rs b/src/main.rs index 7f8a128..f08bf16 100644 --- a/src/main.rs +++ b/src/main.rs @@ -52,6 +52,7 @@ impl MainWindow { light_spread: 0.125, accum_sigma: 0.025, accum_scale: 0.01, + reflections: 2, show_axes: true, show_shapes: true, show_hit_emission: false, diff --git a/ui/src/api.hxx b/ui/src/api.hxx index ff7038e..caf32cd 100644 --- a/ui/src/api.hxx +++ b/ui/src/api.hxx @@ -20,6 +20,7 @@ struct RedrawArgs { 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; diff --git a/ui/src/main_window.cxx b/ui/src/main_window.cxx index 033645f..dff2f7d 100644 --- a/ui/src/main_window.cxx +++ b/ui/src/main_window.cxx @@ -31,6 +31,7 @@ void PhotonLight::updateView() { .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(), diff --git a/ui/src/main_window.ui b/ui/src/main_window.ui index 44b57c5..23bf13b 100644 --- a/ui/src/main_window.ui +++ b/ui/src/main_window.ui @@ -254,6 +254,23 @@ + + + + Reflections + + + + + + + 20 + + + 2 + + + @@ -577,6 +594,22 @@ + + reflections + valueChanged(int) + MainWindow + updateView() + + + 1489 + 712 + + + 799 + 599 + + + updateView() From 96e5c78a59d421bb6630fa9788718b64c2c39fde Mon Sep 17 00:00:00 2001 From: numzero Date: Thu, 20 Nov 2025 14:23:29 +0300 Subject: [PATCH 19/36] ui hints --- ui/src/main_window.ui | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/ui/src/main_window.ui b/ui/src/main_window.ui index 23bf13b..697c566 100644 --- a/ui/src/main_window.ui +++ b/ui/src/main_window.ui @@ -51,6 +51,9 @@ Yaw + + cameraYaw + @@ -77,6 +80,9 @@ Pitch + + cameraPitch + @@ -103,6 +109,9 @@ Distance + + cameraDistance + @@ -132,6 +141,9 @@ Yaw + + lightYaw + @@ -155,6 +167,9 @@ Pitch + + lightPitch + @@ -184,6 +199,9 @@ Distance + + lightDistance + @@ -213,6 +231,9 @@ Averaging radius + + accumSigma + @@ -236,6 +257,9 @@ Brightness + + accumScale + @@ -259,6 +283,9 @@ Reflections + + reflections + From 707bb6f66ed8077fb98e79fd7fc4de657bdd90e1 Mon Sep 17 00:00:00 2001 From: numzero Date: Sun, 23 Nov 2025 13:29:24 +0300 Subject: [PATCH 20/36] embed the simple shader --- src/render/lines.rs | 3 +-- src/render/mod.rs | 2 ++ shaders/line.wgsl => src/render/simple.wgsl | 0 3 files changed, 3 insertions(+), 2 deletions(-) rename shaders/line.wgsl => src/render/simple.wgsl (100%) diff --git a/src/render/lines.rs b/src/render/lines.rs index 1b5f3fc..493023e 100644 --- a/src/render/lines.rs +++ b/src/render/lines.rs @@ -57,10 +57,9 @@ impl Pipeline { mapped_at_creation: false, }); - let shader = std::fs::read_to_string("shaders/line.wgsl").unwrap(); let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: None, - source: wgpu::ShaderSource::Wgsl(shader.into()), + source: wgpu::ShaderSource::Wgsl(super::SIMPLE_SHADER.into()), }); let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: None, diff --git a/src/render/mod.rs b/src/render/mod.rs index ef14730..249b8bb 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -1 +1,3 @@ pub mod lines; + +static SIMPLE_SHADER: &str = include_str!("simple.wgsl"); diff --git a/shaders/line.wgsl b/src/render/simple.wgsl similarity index 100% rename from shaders/line.wgsl rename to src/render/simple.wgsl From fb8ea92ce869865617fee0ca863b5510f2aa8ad8 Mon Sep 17 00:00:00 2001 From: numzero Date: Sun, 23 Nov 2025 21:02:38 +0300 Subject: [PATCH 21/36] fix double-accounting for surface slope --- src/lib.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 8b7384d..a6a1101 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -309,9 +309,7 @@ impl Core { 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 out_cd = in_lm * reflector.brdf(hit.normal, hit.incident.dir, -ray.dir); let weight = accum_normalizator * (-0.5 * d2 / sigma2).exp(); total_cd += weight * out_cd; } From a51f46a038762a949c5a4ef7f12cff9f879f8dc2 Mon Sep 17 00:00:00 2001 From: numzero Date: Mon, 24 Nov 2025 00:19:28 +0300 Subject: [PATCH 22/36] fix a bug it's invisible with Lambertian model though --- src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index a6a1101..f2f9c39 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -309,7 +309,8 @@ impl Core { assert!(hit.incident.dir.is_normalized()); let reflector = Lambertian; let in_lm = 1.0; - let out_cd = in_lm * reflector.brdf(hit.normal, hit.incident.dir, -ray.dir); + let out_cd = + in_lm * reflector.brdf(light_hit.normal, light_hit.incident.dir, -ray.dir); let weight = accum_normalizator * (-0.5 * d2 / sigma2).exp(); total_cd += weight * out_cd; } From 4b138deb34ef04df546dd1ebb49d3f2c98709fa1 Mon Sep 17 00:00:00 2001 From: numzero Date: Mon, 24 Nov 2025 00:21:06 +0300 Subject: [PATCH 23/36] allow choosing which normals to use --- src/lib.rs | 19 +++++++++++--- src/main.rs | 3 ++- ui/src/api.hxx | 9 ++++++- ui/src/main_window.cxx | 13 +++++++++- ui/src/main_window.hxx | 1 + ui/src/main_window.ui | 57 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 95 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f2f9c39..18937dd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,6 +27,13 @@ pub struct SphericalPosition { pub distance: f32, } +#[derive(Debug, Clone, Copy)] +#[repr(u8)] +pub enum UseNormal { + Light, + Camera, +} + #[derive(Debug, Clone, Copy)] #[repr(C)] pub struct RedrawArgs { @@ -36,7 +43,8 @@ pub struct RedrawArgs { pub light_spread: f32, pub accum_sigma: f32, pub accum_scale: f32, - pub reflections: u32, + pub reflections: u8, + pub use_normal: UseNormal, pub show_axes: bool, pub show_shapes: bool, pub show_hit_emission: bool, @@ -305,12 +313,15 @@ impl Core { if d2 > 9. * sigma2 { continue; } - assert!(hit.normal.is_normalized()); + let normal = match args.use_normal { + UseNormal::Light => light_hit.normal, + UseNormal::Camera => hit.normal, + }; + assert!(normal.is_normalized()); assert!(hit.incident.dir.is_normalized()); let reflector = Lambertian; let in_lm = 1.0; - let out_cd = - in_lm * reflector.brdf(light_hit.normal, light_hit.incident.dir, -ray.dir); + let out_cd = in_lm * reflector.brdf(normal, light_hit.incident.dir, -ray.dir); let weight = accum_normalizator * (-0.5 * d2 / sigma2).exp(); total_cd += weight * out_cd; } diff --git a/src/main.rs b/src/main.rs index f08bf16..cb11049 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use std::{f32::consts::PI, sync::Arc}; use glam::uvec2; -use photon_light::{Core, RedrawArgs, SphericalPosition, init_gpu_inner}; +use photon_light::{Core, RedrawArgs, SphericalPosition, UseNormal, init_gpu_inner}; use winit::{ application::ApplicationHandler, event::WindowEvent, @@ -53,6 +53,7 @@ impl MainWindow { accum_sigma: 0.025, accum_scale: 0.01, reflections: 2, + use_normal: UseNormal::Light, show_axes: true, show_shapes: true, show_hit_emission: false, diff --git a/ui/src/api.hxx b/ui/src/api.hxx index caf32cd..bc96bc8 100644 --- a/ui/src/api.hxx +++ b/ui/src/api.hxx @@ -13,6 +13,11 @@ struct SphericalPosition { float distance = 1.; }; +enum class UseNormal: std::uint8_t { + Light, + Camera, +}; + struct RedrawArgs { SphericalPosition camera_position; SphericalPosition light_position; @@ -20,7 +25,8 @@ struct RedrawArgs { float light_spread = 0.; float accum_sigma = 1.; float accum_scale = 1.; - std::uint32_t reflections = 0; + std::uint8_t reflections = 0; + UseNormal use_normal = UseNormal::Light; bool show_axes = true; bool show_shapes = true; bool show_hit_emission = true; @@ -31,6 +37,7 @@ struct RedrawArgs { }; } // namespace ffi +using ffi::UseNormal; using ffi::RedrawArgs; using ffi::SphericalPosition; diff --git a/ui/src/main_window.cxx b/ui/src/main_window.cxx index dff2f7d..11ae0f2 100644 --- a/ui/src/main_window.cxx +++ b/ui/src/main_window.cxx @@ -16,6 +16,11 @@ float deg_to_rad(float val) { } void PhotonLight::updateView() { + UseNormal use_normal = {}; + if (m_ui->normalFromLight->isChecked()) + use_normal = UseNormal::Light; + else if (m_ui->normalFromCamera->isChecked()) + use_normal = UseNormal::Camera; RedrawArgs args{ .camera_position = SphericalPosition{ .yaw = deg_to_rad(m_ui->cameraYaw->value()), @@ -31,7 +36,8 @@ void PhotonLight::updateView() { .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()), + .reflections = std::uint8_t(m_ui->reflections->value()), + .use_normal = use_normal, .show_axes = m_ui->displayAxes->isChecked(), .show_shapes = m_ui->displayShapes->isChecked(), .show_hit_emission = m_ui->displayEmitted->isChecked(), @@ -51,4 +57,9 @@ void PhotonLight::updateView() { m_ui->viewport->setView(args); } +void PhotonLight::updateViewIf(bool update) { + if (update) + updateView(); +} + #include "moc_main_window.cpp" diff --git a/ui/src/main_window.hxx b/ui/src/main_window.hxx index 1ca73cc..27b63ff 100644 --- a/ui/src/main_window.hxx +++ b/ui/src/main_window.hxx @@ -16,6 +16,7 @@ public: public slots: void updateView(); + void updateViewIf(bool update); // for radio buttons private: const std::unique_ptr m_ui; diff --git a/ui/src/main_window.ui b/ui/src/main_window.ui index 697c566..5802a1d 100644 --- a/ui/src/main_window.ui +++ b/ui/src/main_window.ui @@ -298,6 +298,30 @@ + + + + Use normal at + + + + + + + Hit position + + + true + + + + + + + Show position + + + @@ -637,8 +661,41 @@ + + normalFromLight + toggled(bool) + MainWindow + updateViewIf(bool) + + + 1429 + 787 + + + 1381 + 783 + + + + + normalFromCamera + toggled(bool) + MainWindow + updateViewIf(bool) + + + 1932 + 816 + + + 1600 + 874 + + + updateView() + updateViewIf(bool) From 77cab94a6e50e8247c4e1a9c050facfeaadbb59c Mon Sep 17 00:00:00 2001 From: numzero Date: Mon, 24 Nov 2025 17:10:12 +0300 Subject: [PATCH 24/36] add basic shape generation --- src/lib.rs | 1 + src/shape.rs | 140 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 src/shape.rs diff --git a/src/lib.rs b/src/lib.rs index 18937dd..ce55aa2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,7 @@ use crate::{ mod camera; mod ray; mod render; +mod shape; mod trace; const OUTPUT_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Bgra8UnormSrgb; diff --git a/src/shape.rs b/src/shape.rs new file mode 100644 index 0000000..e1b5865 --- /dev/null +++ b/src/shape.rs @@ -0,0 +1,140 @@ +use std::f32::consts::PI; + +use glam::{Vec3, vec3}; + +pub type Face = [u16; 3]; + +#[derive(Debug, Clone)] +pub struct Mesh { + pub vertices: Vec, + pub indices: Vec, +} + +pub fn sphere(subdiv: usize) -> Mesh { + assert!(subdiv >= 2); + assert!(2 * subdiv * (subdiv - 1) + 2 <= 1 << 16); + + let mut vertices = Vec::new(); + vertices.push(Vec3::Z); + for j in 1..subdiv { + let v = j as f32 / subdiv as f32; + let theta = PI * v; + let (xy, z) = theta.sin_cos(); + for i in 0..2 * subdiv { + let u = i as f32 / (2 * subdiv) as f32; + let phi = 2. * PI * u; + let (y, x) = phi.sin_cos(); + vertices.push(vec3(xy * x, xy * y, z)); + } + } + vertices.push(-Vec3::Z); + assert_eq!(vertices.len(), 2 * subdiv * (subdiv - 1) + 2); + + let mut indices = Vec::new(); + let subdiv = subdiv as u16; + for i in 0..2 * subdiv - 1 { + indices.push([0, i + 1, i + 2]); + } + indices.push([0, 2 * subdiv, 1]); + for j in 0..subdiv - 2 { + let k1 = j * 2 * subdiv + 1; + let k2 = k1 + 2 * subdiv; + for i in 0..2 * subdiv - 1 { + let a = k1 + i; + let b = k2 + i; + indices.push([a, b, b + 1]); + indices.push([a, b + 1, a + 1]); + } + let a = k1 + 2 * subdiv - 1; + let b = k2 + 2 * subdiv - 1; + indices.push([a, b, k2]); + indices.push([a, k2, k1]); + } + let k1 = 2 * subdiv * (subdiv - 2) + 1; + let k2 = 2 * subdiv * (subdiv - 1) + 1; + for i in 0..2 * subdiv - 1 { + indices.push([k1 + i, k2, k1 + i + 1]); + } + indices.push([k1 + 2 * subdiv - 1, k2, k1]); + + Mesh { vertices, indices } +} + +#[cfg(test)] +mod tests { + use approx::abs_diff_eq; + + use super::*; + + #[test] + fn test_sphere_2_topology() { + assert_eq!( + sphere(2).indices, + vec![ + [0, 1, 2], + [0, 2, 3], + [0, 3, 4], + [0, 4, 1], + [1, 5, 2], + [2, 5, 3], + [3, 5, 4], + [4, 5, 1], + ] + ); + } + + fn are_equal_eps(left: &[Vec3], right: &[Vec3], eps: f32) -> bool { + if left.len() != right.len() { + return false; + } + left.iter() + .zip(right.iter()) + .all(|(a, b)| abs_diff_eq!(a, b, epsilon = eps)) + } + + #[test] + fn test_sphere_2_geometry() { + let left = sphere(2).vertices; + let right = [Vec3::Z, Vec3::X, Vec3::Y, -Vec3::X, -Vec3::Y, -Vec3::Z]; + assert!( + are_equal_eps(&left, &right, 1e-6), + "assertion `left == right` failed\n left: {left:?}\n right: {right:?}" + ); + } + + #[test] + fn test_sphere_3_topology() { + assert_eq!( + sphere(3).indices, + vec![ + // top + [0, 1, 2], + [0, 2, 3], + [0, 3, 4], + [0, 4, 5], + [0, 5, 6], + [0, 6, 1], + // belt + [1, 7, 8], + [1, 8, 2], + [2, 8, 9], + [2, 9, 3], + [3, 9, 10], + [3, 10, 4], + [4, 10, 11], + [4, 11, 5], + [5, 11, 12], + [5, 12, 6], + [6, 12, 7], + [6, 7, 1], + // bottom + [7, 13, 8], + [8, 13, 9], + [9, 13, 10], + [10, 13, 11], + [11, 13, 12], + [12, 13, 7], + ] + ); + } +} From 5a6b10965de4b6fb530df846ec76a92b0722b8c4 Mon Sep 17 00:00:00 2001 From: numzero Date: Mon, 24 Nov 2025 17:29:08 +0300 Subject: [PATCH 25/36] show spheres --- src/lib.rs | 29 ++++++++ src/render/faces.rs | 161 ++++++++++++++++++++++++++++++++++++++++++++ src/render/mod.rs | 1 + 3 files changed, 191 insertions(+) create mode 100644 src/render/faces.rs diff --git a/src/lib.rs b/src/lib.rs index ce55aa2..f2122d3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,6 +67,7 @@ pub struct Core { surface: wgpu::Surface<'static>, pipeline: Pipeline, + mesh_pipe: render::faces::Pipeline, tripod: Mesh, } @@ -130,6 +131,7 @@ impl Core { } = gpu; let pipeline = Pipeline::new(&device, OUTPUT_FORMAT); + let mesh_pipe = render::faces::Pipeline::new(&device, OUTPUT_FORMAT); let tripod = new_tripod(&device); queue.submit([]); // flush buffer updates @@ -139,6 +141,7 @@ impl Core { surface, pipeline, tripod, + mesh_pipe, } } @@ -161,6 +164,12 @@ impl Core { m: perspective * camera.transform(), }, ); + self.mesh_pipe.set_look( + &self.queue, + render::faces::LookParams { + m: perspective * camera.transform(), + }, + ); self.queue.submit([]); // flush buffer updates let view = output.create_view(&wgpu::TextureViewDescriptor::default()); @@ -223,6 +232,26 @@ impl Core { sphere(vec3(0.1, 0.3, 0.1)), ], }; + if args.show_shapes { + let mut meshes = Vec::new(); + for obj in &scene.objects { + let mesh = shape::sphere((obj.radius * 15.) as usize + 7); + let obj_mesh = render::faces::Mesh::new( + &self.device, + &mesh + .vertices + .iter() + .map(|v| render::faces::Vertex { + pos: obj.position + obj.radius * v, + color: 0.5 * (v + 1.), + }) + .collect::>(), + bytemuck::cast_slice(&mesh.indices), + ); + meshes.push(obj_mesh); + } + self.mesh_pipe.render(&mut pass, &meshes); + } let mut prng = rand_pcg::Pcg64::new(42, 0); let source_rays: Vec = (0..10240).map(|_| source.make_ray(&mut prng)).collect(); diff --git a/src/render/faces.rs b/src/render/faces.rs new file mode 100644 index 0000000..1776a39 --- /dev/null +++ b/src/render/faces.rs @@ -0,0 +1,161 @@ +use std::mem::offset_of; + +use bytemuck::{Pod, Zeroable, bytes_of, cast_slice}; +use glam::{Mat4, Vec3}; +use wgpu::util::DeviceExt as _; + +#[derive(Debug, Clone, Copy, Pod, Zeroable)] +#[repr(C)] +pub struct LookParams { + pub m: Mat4, +} + +#[derive(Debug, Clone, Copy, Pod, Zeroable)] +#[repr(C)] +pub struct Vertex { + pub pos: Vec3, + pub color: Vec3, +} + +impl Vertex { + pub fn new(pos: Vec3, color: Vec3) -> Self { + Self { pos, color } + } +} + +pub struct Mesh { + vertex_buffer: wgpu::Buffer, + index_buffer: wgpu::Buffer, + index_count: u32, +} + +impl Mesh { + pub fn new(device: &wgpu::Device, vertices: &[Vertex], indices: &[u16]) -> Self { + if vertices.len() >= 1 << 16 { + panic!("too many vertices"); + } + for index in indices { + if *index as usize >= vertices.len() { + panic!( + "vertex index out of bounds: {index} out of {}", + vertices.len() + ); + } + } + let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: None, + usage: wgpu::BufferUsages::VERTEX, + contents: cast_slice(vertices), + }); + let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: None, + usage: wgpu::BufferUsages::INDEX, + contents: cast_slice(indices), + }); + Self { + vertex_buffer, + index_buffer, + index_count: indices.len().try_into().expect("too many indices"), + } + } +} + +pub struct Pipeline { + look_buf: wgpu::Buffer, + bindings: wgpu::BindGroup, + pipeline: wgpu::RenderPipeline, +} + +impl Pipeline { + pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self { + let look_buf = device.create_buffer(&wgpu::BufferDescriptor { + label: None, + size: size_of::() as u64, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: None, + source: wgpu::ShaderSource::Wgsl(super::SIMPLE_SHADER.into()), + }); + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: None, + layout: None, + vertex: wgpu::VertexState { + module: &shader, + entry_point: None, + compilation_options: wgpu::PipelineCompilationOptions::default(), + buffers: &[wgpu::VertexBufferLayout { + array_stride: size_of::() as u64, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &[ + wgpu::VertexAttribute { + shader_location: 0, + offset: offset_of!(Vertex, pos) as u64, + format: wgpu::VertexFormat::Float32x3, + }, + wgpu::VertexAttribute { + shader_location: 1, + offset: offset_of!(Vertex, color) as u64, + format: wgpu::VertexFormat::Float32x3, + }, + ], + }], + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: None, + compilation_options: wgpu::PipelineCompilationOptions::default(), + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + multiview: None, + cache: None, + }); + let bindings = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: None, + layout: &pipeline.get_bind_group_layout(0), + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: look_buf.as_entire_binding(), + }], + }); + Self { + look_buf, + bindings, + pipeline, + } + } + + pub fn set_look(&self, queue: &wgpu::Queue, look: LookParams) { + queue.write_buffer(&self.look_buf, 0, bytes_of(&look)); + } + + pub fn render<'a>( + &self, + pass: &mut wgpu::RenderPass, + meshes: impl IntoIterator, + ) { + pass.set_pipeline(&self.pipeline); + pass.set_bind_group(0, &self.bindings, &[]); + for mesh in meshes { + pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..)); + pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint16); + pass.draw_indexed(0..mesh.index_count, 0, 0..1); + } + } +} diff --git a/src/render/mod.rs b/src/render/mod.rs index 249b8bb..291e015 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -1,3 +1,4 @@ +pub mod faces; pub mod lines; static SIMPLE_SHADER: &str = include_str!("simple.wgsl"); From c40ff3e324b5cd55767acbd191f232c6a8aa8277 Mon Sep 17 00:00:00 2001 From: numzero Date: Mon, 24 Nov 2025 17:54:42 +0300 Subject: [PATCH 26/36] fix output format --- src/lib.rs | 11 ++++++----- src/render/faces.rs | 6 ++++-- src/render/lines.rs | 6 ++++-- src/render/mod.rs | 2 ++ 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f2122d3..79e027d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,10 @@ use rand_distr::Distribution as _; use crate::{ camera::OrbitalCamera, ray::Ray, - render::lines::{LookParams, Mesh, Pipeline, Vertex}, + render::{ + OUTPUT_FORMAT, + lines::{LookParams, Mesh, Pipeline, Vertex}, + }, trace::{Hit, Lambertian, Reflector, Scene, Source, Sphere}, }; @@ -18,8 +21,6 @@ mod render; mod shape; mod trace; -const OUTPUT_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Bgra8UnormSrgb; - #[derive(Debug, Clone, Copy, PartialEq)] #[repr(C)] pub struct SphericalPosition { @@ -130,8 +131,8 @@ impl Core { surface, } = gpu; - let pipeline = Pipeline::new(&device, OUTPUT_FORMAT); - let mesh_pipe = render::faces::Pipeline::new(&device, OUTPUT_FORMAT); + let pipeline = Pipeline::new(&device); + let mesh_pipe = render::faces::Pipeline::new(&device); let tripod = new_tripod(&device); queue.submit([]); // flush buffer updates diff --git a/src/render/faces.rs b/src/render/faces.rs index 1776a39..e52e3e7 100644 --- a/src/render/faces.rs +++ b/src/render/faces.rs @@ -4,6 +4,8 @@ use bytemuck::{Pod, Zeroable, bytes_of, cast_slice}; use glam::{Mat4, Vec3}; use wgpu::util::DeviceExt as _; +use crate::render::OUTPUT_FORMAT; + #[derive(Debug, Clone, Copy, Pod, Zeroable)] #[repr(C)] pub struct LookParams { @@ -67,7 +69,7 @@ pub struct Pipeline { } impl Pipeline { - pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self { + pub fn new(device: &wgpu::Device) -> Self { let look_buf = device.create_buffer(&wgpu::BufferDescriptor { label: None, size: size_of::() as u64, @@ -118,7 +120,7 @@ impl Pipeline { entry_point: None, compilation_options: wgpu::PipelineCompilationOptions::default(), targets: &[Some(wgpu::ColorTargetState { - format, + format: OUTPUT_FORMAT, blend: Some(wgpu::BlendState::REPLACE), write_mask: wgpu::ColorWrites::ALL, })], diff --git a/src/render/lines.rs b/src/render/lines.rs index 493023e..8a7ef94 100644 --- a/src/render/lines.rs +++ b/src/render/lines.rs @@ -4,6 +4,8 @@ use bytemuck::{Pod, Zeroable, bytes_of, cast_slice}; use glam::{Mat4, Vec3}; use wgpu::util::DeviceExt as _; +use crate::render::OUTPUT_FORMAT; + #[derive(Debug, Clone, Copy, Pod, Zeroable)] #[repr(C)] pub struct LookParams { @@ -49,7 +51,7 @@ pub struct Pipeline { } impl Pipeline { - pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self { + pub fn new(device: &wgpu::Device) -> Self { let look_buf = device.create_buffer(&wgpu::BufferDescriptor { label: None, size: size_of::() as u64, @@ -100,7 +102,7 @@ impl Pipeline { entry_point: None, compilation_options: wgpu::PipelineCompilationOptions::default(), targets: &[Some(wgpu::ColorTargetState { - format, + format: OUTPUT_FORMAT, blend: Some(wgpu::BlendState::REPLACE), write_mask: wgpu::ColorWrites::ALL, })], diff --git a/src/render/mod.rs b/src/render/mod.rs index 291e015..435dc0d 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -2,3 +2,5 @@ pub mod faces; pub mod lines; static SIMPLE_SHADER: &str = include_str!("simple.wgsl"); + +pub const OUTPUT_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Bgra8UnormSrgb; From a8f72096d8805fb25e46920af9600b5c74412cfb Mon Sep 17 00:00:00 2001 From: numzero Date: Mon, 24 Nov 2025 18:08:03 +0300 Subject: [PATCH 27/36] pass size on creation --- src/lib.rs | 8 +++++--- src/main.rs | 3 +-- ui/src/api.cxx | 6 +++--- ui/src/api.hxx | 2 +- ui/src/lib.rs | 9 ++++++--- ui/src/viewport.cxx | 4 ++-- 6 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 79e027d..87c1922 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -124,7 +124,7 @@ impl OrbitalCamera { } impl Core { - pub fn new(gpu: Gpu) -> Self { + pub fn new(gpu: Gpu, pixel_size: UVec2) -> Self { let Gpu { device, queue, @@ -136,14 +136,16 @@ impl Core { let tripod = new_tripod(&device); queue.submit([]); // flush buffer updates - Self { + let mut this = Self { device, queue, surface, pipeline, tripod, mesh_pipe, - } + }; + this.configure(pixel_size); + this } fn render(&self, output: &wgpu::Texture, args: &RedrawArgs) { diff --git a/src/main.rs b/src/main.rs index cb11049..adce210 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,8 +26,7 @@ impl MainWindow { instance.create_surface(Arc::clone(&window)) })) .unwrap(); - let mut core = Core::new(gpu); - core.configure(uvec2(1, 1)); + let core = Core::new(gpu, uvec2(1, 1)); Self { window, core } } diff --git a/ui/src/api.cxx b/ui/src/api.cxx index d65a690..aaa2b64 100644 --- a/ui/src/api.cxx +++ b/ui/src/api.cxx @@ -4,7 +4,7 @@ #include namespace ffi { -extern "C" Core* rt4_viewport_create(xcb_connection_t* connection, std::uint32_t window); +extern "C" Core* rt4_viewport_create(xcb_connection_t* connection, std::uint32_t window, std::uint32_t width, std::uint32_t height); extern "C" void rt4_viewport_destroy(Core* viewport); extern "C" void rt4_viewport_configure(Core* viewport, std::uint32_t width, std::uint32_t height); extern "C" void rt4_viewport_redraw(Core* viewport, const RedrawArgs* args); @@ -26,13 +26,13 @@ BoxCore& BoxCore::operator=(BoxCore&& b) { return *this; } -BoxCore BoxCore::from_xcb(xcb_connection_t* connection, std::uint32_t window) { +BoxCore BoxCore::from_xcb(xcb_connection_t* connection, std::uint32_t window, std::uint32_t width, std::uint32_t height) { if (!connection) throw std::logic_error("attempt to use a null connection"); if (!window) throw std::logic_error("attempt to use a null window"); BoxCore out; - out.ptr.ptr = ffi::rt4_viewport_create(connection, window); + out.ptr.ptr = ffi::rt4_viewport_create(connection, window, width, height); return out; } diff --git a/ui/src/api.hxx b/ui/src/api.hxx index bc96bc8..d46272e 100644 --- a/ui/src/api.hxx +++ b/ui/src/api.hxx @@ -69,7 +69,7 @@ public: void reset(); - static BoxCore from_xcb(xcb_connection_t* connection, std::uint32_t window); + static BoxCore from_xcb(xcb_connection_t* connection, std::uint32_t window, std::uint32_t width, std::uint32_t height); private: MutCore ptr; diff --git a/ui/src/lib.rs b/ui/src/lib.rs index 9ee96ef..c7a85f8 100644 --- a/ui/src/lib.rs +++ b/ui/src/lib.rs @@ -1,12 +1,13 @@ use std::{ffi::c_void, num::NonZero, ptr::NonNull}; -use glam::uvec2; +use glam::{UVec2, uvec2}; use photon_light::{Core, RedrawArgs, init_gpu_inner}; use raw_window_handle::{RawDisplayHandle, RawWindowHandle, XcbDisplayHandle, XcbWindowHandle}; unsafe fn create_viewport( display: impl Into, window: impl Into, + size: UVec2, ) -> Box { let target = wgpu::SurfaceTargetUnsafe::RawHandle { raw_display_handle: display.into(), @@ -16,17 +17,19 @@ unsafe fn create_viewport( instance.create_surface_unsafe(target) })) .unwrap(); - Box::new(Core::new(gpu)) + Box::new(Core::new(gpu, size)) } #[unsafe(no_mangle)] unsafe extern "C" fn rt4_viewport_create( connection: NonNull, window: NonZero, + width: u32, + height: u32, ) -> Box { let display = XcbDisplayHandle::new(Some(connection), 0); let window = XcbWindowHandle::new(window); - unsafe { create_viewport(display, window) } + unsafe { create_viewport(display, window, uvec2(width, height)) } } #[unsafe(no_mangle)] diff --git a/ui/src/viewport.cxx b/ui/src/viewport.cxx index 483ba07..e681bd9 100644 --- a/ui/src/viewport.cxx +++ b/ui/src/viewport.cxx @@ -61,9 +61,9 @@ void Viewport::recreate() try { fprintf(stderr, "connection %p, window %#08x\n", xcb_connection, x11_window); + const QSize device_size = size() * devicePixelRatio(); core.reset(); - core = BoxCore::from_xcb(xcb_connection, x11_window); - updateSize(); + core = BoxCore::from_xcb(xcb_connection, x11_window, device_size.width(), device_size.height()); } catch (const std::exception& e) { fprintf(stderr, "failed to recreate the viewport: %s", e.what()); } From 295037edc6a22774ae76cb3a9b26d97766e0db5d Mon Sep 17 00:00:00 2001 From: numzero Date: Mon, 24 Nov 2025 18:15:48 +0300 Subject: [PATCH 28/36] add depth buffer --- src/lib.rs | 56 +++++++++++++++++++++++++++++++++++++-------- src/render/faces.rs | 10 ++++++-- src/render/lines.rs | 14 ++++++++++-- src/render/mod.rs | 1 + 4 files changed, 67 insertions(+), 14 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 87c1922..0beb0aa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,7 @@ use crate::{ camera::OrbitalCamera, ray::Ray, render::{ - OUTPUT_FORMAT, + DEPTH_FORMAT, OUTPUT_FORMAT, lines::{LookParams, Mesh, Pipeline, Vertex}, }, trace::{Hit, Lambertian, Reflector, Scene, Source, Sphere}, @@ -67,6 +67,8 @@ pub struct Core { queue: wgpu::Queue, surface: wgpu::Surface<'static>, + depth: wgpu::Texture, + pipeline: Pipeline, mesh_pipe: render::faces::Pipeline, tripod: Mesh, @@ -134,18 +136,20 @@ impl Core { let pipeline = Pipeline::new(&device); let mesh_pipe = render::faces::Pipeline::new(&device); let tripod = new_tripod(&device); + let depth = Self::create_depth_buffer(&device, pixel_size); queue.submit([]); // flush buffer updates - let mut this = Self { + Self::configure_surface(&surface, &device, pixel_size); + + Self { device, queue, surface, + depth, pipeline, tripod, mesh_pipe, - }; - this.configure(pixel_size); - this + } } fn render(&self, output: &wgpu::Texture, args: &RedrawArgs) { @@ -176,6 +180,9 @@ impl Core { self.queue.submit([]); // flush buffer updates let view = output.create_view(&wgpu::TextureViewDescriptor::default()); + let depth_view = self + .depth + .create_view(&wgpu::TextureViewDescriptor::default()); let mut encoder = self .device .create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); @@ -189,7 +196,14 @@ impl Core { store: wgpu::StoreOp::Store, }, })], - depth_stencil_attachment: None, + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &depth_view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Clear(1.), + store: wgpu::StoreOp::Discard, + }), + stencil_ops: None, + }), ..Default::default() }); if args.show_axes { @@ -389,10 +403,26 @@ impl Core { self.queue.submit(std::iter::once(encoder.finish())); } - /// Configures the renderer for a given target size. - pub fn configure(&mut self, pixel_size: UVec2) { - self.surface.configure( - &self.device, + fn create_depth_buffer(device: &wgpu::Device, pixel_size: UVec2) -> wgpu::Texture { + device.create_texture(&wgpu::TextureDescriptor { + label: Some("depth buffer"), + size: wgpu::Extent3d { + width: pixel_size.x, + height: pixel_size.y, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: DEPTH_FORMAT, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }) + } + + fn configure_surface(surface: &wgpu::Surface, device: &wgpu::Device, pixel_size: UVec2) { + surface.configure( + device, &wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST, format: OUTPUT_FORMAT, @@ -406,6 +436,12 @@ impl Core { ); } + /// Configures the renderer for a given target size. + pub fn configure(&mut self, pixel_size: UVec2) { + Self::configure_surface(&self.surface, &self.device, pixel_size); + self.depth = Self::create_depth_buffer(&self.device, pixel_size); + } + /// Redraws the entire surface. /// /// [`Self::configure`] must be called at least once before this. diff --git a/src/render/faces.rs b/src/render/faces.rs index e52e3e7..b6a2a20 100644 --- a/src/render/faces.rs +++ b/src/render/faces.rs @@ -4,7 +4,7 @@ use bytemuck::{Pod, Zeroable, bytes_of, cast_slice}; use glam::{Mat4, Vec3}; use wgpu::util::DeviceExt as _; -use crate::render::OUTPUT_FORMAT; +use crate::render::{DEPTH_FORMAT, OUTPUT_FORMAT}; #[derive(Debug, Clone, Copy, Pod, Zeroable)] #[repr(C)] @@ -109,7 +109,13 @@ impl Pipeline { topology: wgpu::PrimitiveTopology::TriangleList, ..Default::default() }, - depth_stencil: None, + depth_stencil: Some(wgpu::DepthStencilState { + format: DEPTH_FORMAT, + depth_write_enabled: true, + depth_compare: wgpu::CompareFunction::LessEqual, + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), + }), multisample: wgpu::MultisampleState { count: 1, mask: !0, diff --git a/src/render/lines.rs b/src/render/lines.rs index 8a7ef94..b68fb86 100644 --- a/src/render/lines.rs +++ b/src/render/lines.rs @@ -4,7 +4,7 @@ use bytemuck::{Pod, Zeroable, bytes_of, cast_slice}; use glam::{Mat4, Vec3}; use wgpu::util::DeviceExt as _; -use crate::render::OUTPUT_FORMAT; +use crate::render::{DEPTH_FORMAT, OUTPUT_FORMAT}; #[derive(Debug, Clone, Copy, Pod, Zeroable)] #[repr(C)] @@ -91,7 +91,17 @@ impl Pipeline { topology: wgpu::PrimitiveTopology::LineList, ..Default::default() }, - depth_stencil: None, + depth_stencil: Some(wgpu::DepthStencilState { + format: DEPTH_FORMAT, + depth_write_enabled: true, + depth_compare: wgpu::CompareFunction::LessEqual, + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState { + constant: -2, + slope_scale: 0., + clamp: 0., + }, + }), multisample: wgpu::MultisampleState { count: 1, mask: !0, diff --git a/src/render/mod.rs b/src/render/mod.rs index 435dc0d..2f629ee 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -4,3 +4,4 @@ pub mod lines; static SIMPLE_SHADER: &str = include_str!("simple.wgsl"); pub const OUTPUT_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Bgra8UnormSrgb; +pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth24Plus; From 722e5d4b6eee1ad5907af5e3a3ce85cf5985b6b7 Mon Sep 17 00:00:00 2001 From: numzero Date: Mon, 24 Nov 2025 18:21:31 +0300 Subject: [PATCH 29/36] split light calculations and camera rays --- src/lib.rs | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0beb0aa..7376d38 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -346,16 +346,13 @@ impl Core { hits1 = hits2; } } - let mut camera_ray_display: Vec = Vec::with_capacity(camera_rays.len()); - if args.show_light { + let light_at = { 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 hits = &hits; + move |hit: Hit| -> f32 { let mut total_cd = 0.0f32; - for light_hit in &hits { + for light_hit in hits { let d2 = hit.incident.base.distance_squared(light_hit.incident.base); if d2 > 9. * sigma2 { continue; @@ -368,11 +365,22 @@ impl Core { assert!(hit.incident.dir.is_normalized()); let reflector = Lambertian; let in_lm = 1.0; - let out_cd = in_lm * reflector.brdf(normal, light_hit.incident.dir, -ray.dir); + let out_cd = + in_lm * reflector.brdf(normal, light_hit.incident.dir, -hit.incident.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()); + total_cd * args.accum_scale + } + }; + let mut camera_ray_display: Vec = Vec::with_capacity(camera_rays.len()); + if args.show_light { + for ray in camera_rays { + let Some(hit) = scene.trace_ray(ray) else { + continue; + }; + let light = light_at(hit); + let brightness = 3. * (1. - (1. + light).recip()); let r = args.accum_sigma; let color = vec3(brightness, brightness - 1., brightness - 2.) .clamp(Vec3::splat(0.), Vec3::splat(1.)); From 41579bbb5d44f6a455fa1b45876c2ee962a48db8 Mon Sep 17 00:00:00 2001 From: numzero Date: Mon, 24 Nov 2025 18:23:35 +0300 Subject: [PATCH 30/36] split colormap --- src/lib.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 7376d38..d3713d7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -373,17 +373,20 @@ impl Core { total_cd * args.accum_scale } }; + let colormap = |light: f32| { + let brightness = 3. * (1. - (1. + light).recip()); + vec3(brightness, brightness - 1., brightness - 2.) + .clamp(Vec3::splat(0.), Vec3::splat(1.)) + }; let mut camera_ray_display: Vec = Vec::with_capacity(camera_rays.len()); if args.show_light { + let r = args.accum_sigma; for ray in camera_rays { let Some(hit) = scene.trace_ray(ray) else { continue; }; let light = light_at(hit); - let brightness = 3. * (1. - (1. + light).recip()); - let r = args.accum_sigma; - let color = vec3(brightness, brightness - 1., brightness - 2.) - .clamp(Vec3::splat(0.), Vec3::splat(1.)); + let color = colormap(light); let vertex = |off: Vec3| Vertex { pos: hit.incident.base + r * off, color, From 59bc5b640f515318c0af780921bcf9eeb2dac0e8 Mon Sep 17 00:00:00 2001 From: numzero Date: Mon, 24 Nov 2025 18:31:23 +0300 Subject: [PATCH 31/36] per-vertex lighting! --- src/lib.rs | 47 +++++++++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d3713d7..4393e0e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -249,26 +249,6 @@ impl Core { sphere(vec3(0.1, 0.3, 0.1)), ], }; - if args.show_shapes { - let mut meshes = Vec::new(); - for obj in &scene.objects { - let mesh = shape::sphere((obj.radius * 15.) as usize + 7); - let obj_mesh = render::faces::Mesh::new( - &self.device, - &mesh - .vertices - .iter() - .map(|v| render::faces::Vertex { - pos: obj.position + obj.radius * v, - color: 0.5 * (v + 1.), - }) - .collect::>(), - bytemuck::cast_slice(&mesh.indices), - ); - meshes.push(obj_mesh); - } - self.mesh_pipe.render(&mut pass, &meshes); - } let mut prng = rand_pcg::Pcg64::new(42, 0); let source_rays: Vec = (0..10240).map(|_| source.make_ray(&mut prng)).collect(); @@ -401,6 +381,33 @@ impl Core { ]); } } + if args.show_shapes { + let mut meshes = Vec::new(); + for obj in &scene.objects { + let mesh = shape::sphere((obj.radius * 45.) as usize + 7); + let obj_mesh = render::faces::Mesh::new( + &self.device, + &mesh + .vertices + .iter() + .map(|&v| { + let pos = obj.position + obj.radius * v; + let normal = v; + let dir = (pos - camera.position()).normalize(); + let light = light_at(Hit { + incident: Ray::new(pos, dir), + normal, + }); + let color = colormap(light); + render::faces::Vertex { pos, color } + }) + .collect::>(), + bytemuck::cast_slice(&mesh.indices), + ); + meshes.push(obj_mesh); + } + self.mesh_pipe.render(&mut pass, &meshes); + } if !source_ray_display.is_empty() { self.pipeline .render(&mut pass, [&Mesh::new(&self.device, &source_ray_display)]); From 98f3c44ab3e944c331791675f8afe31dbe933868 Mon Sep 17 00:00:00 2001 From: numzero Date: Tue, 25 Nov 2025 12:40:26 +0300 Subject: [PATCH 32/36] add super-duper complicated sphere mesh generator --- src/lib.rs | 2 +- src/shape.rs | 208 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 209 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 4393e0e..ddd026c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -384,7 +384,7 @@ impl Core { if args.show_shapes { let mut meshes = Vec::new(); for obj in &scene.objects { - let mesh = shape::sphere((obj.radius * 45.) as usize + 7); + let mesh = shape::octo_sphere((obj.radius * 45.) as usize + 7); let obj_mesh = render::faces::Mesh::new( &self.device, &mesh diff --git a/src/shape.rs b/src/shape.rs index e1b5865..311076d 100644 --- a/src/shape.rs +++ b/src/shape.rs @@ -60,6 +60,114 @@ pub fn sphere(subdiv: usize) -> Mesh { Mesh { vertices, indices } } +pub fn octo_sphere(subdiv: usize) -> Mesh { + assert!(subdiv >= 2); + + let mut vertices = Vec::new(); + let theta_step = PI / subdiv as f32; + let v_belt = |j: usize, n: usize| { + let phi_step = 2. * PI / n as f32; + let theta = j as f32 * theta_step; + let (xy, z) = theta.sin_cos(); + (0..n).map(move |i| { + let phi = i as f32 * phi_step; + let (y, x) = phi.sin_cos(); + vec3(xy * x, xy * y, z) + }) + }; + vertices.push(Vec3::Z); + for j in 1..subdiv / 2 { + vertices.extend(v_belt(j, 4 * j)); + } + { + let j = subdiv / 2; + vertices.extend(v_belt(j, 4 * j)); + } + for j in (subdiv + 2) / 2..subdiv { + vertices.extend(v_belt(j, 4 * (subdiv - j))); + } + vertices.push(-Vec3::Z); + + fn i_cap(top: usize, start: usize) -> impl Iterator { + gen move { + yield [top, start, start + 1]; + yield [top, start + 1, start + 2]; + yield [top, start + 2, start + 3]; + yield [top, start + 3, start]; + } + .map(face) + } + fn i_skew_belt( + short_start: usize, + long_start: usize, + seg_short_face: usize, + ) -> impl Iterator { + gen move { + let seg_long_face = seg_short_face + 1; + let n_short_total = 4 * seg_short_face; + let n_long_total = 4 * seg_long_face; + for face in 0..4 { + let short = move |k| short_start + (face * seg_short_face + k) % n_short_total; + let long = move |k| long_start + (face * seg_long_face + k) % n_long_total; + yield [short(0), long(0), long(1)]; + for k in 0..seg_short_face { + yield [short(k), long(k + 1), short(k + 1)]; + yield [short(k + 1), long(k + 1), long(k + 2)]; + } + } + } + .map(face) + } + fn i_belt(start1: usize, start2: usize, seg_per_face: usize) -> impl Iterator { + gen move { + let n = 4 * seg_per_face; + for k in 0..n { + let k2 = (k + 1) % n; + let a = start1 + k; + let b = start2 + k; + let c = start1 + k2; + let d = start2 + k2; + yield [a, b, c]; + yield [c, b, d]; + } + } + .map(face) + } + fn face(f: [usize; 3]) -> Face { + f.map(|i| i as u16) + } + fn flip(f: Face) -> Face { + [f[1], f[0], f[2]] + } + + let mut indices = Vec::new(); + indices.extend(i_cap(0, 1)); + let mut k = 1; + for j in 1..subdiv - 1 { + let j2 = (subdiv - 1) - j; + match j.cmp(&j2) { + std::cmp::Ordering::Less => { + let n = 4 * j; + indices.extend(i_skew_belt(k, k + n, j)); + k += n; + } + std::cmp::Ordering::Equal => { + let n = 4 * j; + indices.extend(i_belt(k, k + n, j)); + k += n; + } + std::cmp::Ordering::Greater => { + let n = 4 * (j2 + 1); + indices.extend(i_skew_belt(k + n, k, j2).map(flip)); + k += n; + } + } + } + indices.extend(i_cap(k + 4, k).map(flip)); + + Mesh { vertices, indices } +} + #[cfg(test)] mod tests { use approx::abs_diff_eq; @@ -137,4 +245,104 @@ mod tests { ] ); } + + #[test] + fn test_octo_sphere_2_topology() { + assert_eq!( + octo_sphere(2).indices, + vec![ + [0, 1, 2], + [0, 2, 3], + [0, 3, 4], + [0, 4, 1], + [1, 5, 2], + [2, 5, 3], + [3, 5, 4], + [4, 5, 1], + ] + ); + } + + #[test] + fn test_octo_sphere_2_geometry() { + let left = octo_sphere(2).vertices; + let right = [Vec3::Z, Vec3::X, Vec3::Y, -Vec3::X, -Vec3::Y, -Vec3::Z]; + assert!( + are_equal_eps(&left, &right, 1e-6), + "assertion `left == right` failed\n left: {left:?}\n right: {right:?}" + ); + } + + #[test] + fn test_octo_sphere_3_topology() { + assert_eq!( + octo_sphere(3).indices, + vec![ + // top + [0, 1, 2], + [0, 2, 3], + [0, 3, 4], + [0, 4, 1], + // belt + [1, 5, 2], + [2, 5, 6], + [2, 6, 3], + [3, 6, 7], + [3, 7, 4], + [4, 7, 8], + [4, 8, 1], + [1, 8, 5], + // bottom + [5, 9, 6], + [6, 9, 7], + [7, 9, 8], + [8, 9, 5], + ] + ); + } + + #[test] + fn test_octo_sphere_4_topology() { + assert_eq!( + octo_sphere(4).indices, + vec![ + // top + [0, 1, 2], + [0, 2, 3], + [0, 3, 4], + [0, 4, 1], + // belt 1 + [1, 5, 6], + [1, 6, 2], + [2, 6, 7], + [2, 7, 8], + [2, 8, 3], + [3, 8, 9], + [3, 9, 10], + [3, 10, 4], + [4, 10, 11], + [4, 11, 12], + [4, 12, 1], + [1, 12, 5], + // belt 2 + [5, 13, 6], + [6, 13, 14], + [6, 14, 7], + [7, 14, 8], + [8, 14, 15], + [8, 15, 9], + [9, 15, 10], + [10, 15, 16], + [10, 16, 11], + [11, 16, 12], + [12, 16, 13], + [12, 13, 5], + // bottom + [13, 17, 14], + [14, 17, 15], + [15, 17, 16], + [16, 17, 13], + ] + ); + } } From 68b4041c886be4a2b530330a87a80e8a2c15e091 Mon Sep 17 00:00:00 2001 From: numzero Date: Tue, 25 Nov 2025 12:59:28 +0300 Subject: [PATCH 33/36] fix normalization --- src/lib.rs | 2 +- ui/src/main_window.cxx | 4 ++-- ui/src/main_window.ui | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index ddd026c..432b3b7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -328,7 +328,7 @@ impl Core { } let light_at = { let sigma2 = args.accum_sigma.powi(2); - let accum_normalizator = (2. * PI * sigma2).sqrt().recip(); + let accum_normalizator = (2. * PI * sigma2).recip(); let hits = &hits; move |hit: Hit| -> f32 { let mut total_cd = 0.0f32; diff --git a/ui/src/main_window.cxx b/ui/src/main_window.cxx index 11ae0f2..75861e6 100644 --- a/ui/src/main_window.cxx +++ b/ui/src/main_window.cxx @@ -52,8 +52,8 @@ void PhotonLight::updateView() { 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->accumSigmaLabel->setText(tr("Averaging radius: %1").arg(QString::number(args.accum_sigma, 'f', 5))); + m_ui->accumScaleLabel->setText(tr("Brightness: %1").arg(QString::number(args.accum_scale, 'f', 5))); m_ui->viewport->setView(args); } diff --git a/ui/src/main_window.ui b/ui/src/main_window.ui index 5802a1d..150d5e0 100644 --- a/ui/src/main_window.ui +++ b/ui/src/main_window.ui @@ -245,7 +245,7 @@ 0 - -40 + -45 Qt::Horizontal @@ -271,7 +271,7 @@ 0 - -50 + -85 Qt::Horizontal From 653788d456bdf929f8f275b4946de235cdc1c96c Mon Sep 17 00:00:00 2001 From: numzero Date: Tue, 25 Nov 2025 13:00:26 +0300 Subject: [PATCH 34/36] change default view --- ui/src/main_window.ui | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/main_window.ui b/ui/src/main_window.ui index 150d5e0..9c4f169 100644 --- a/ui/src/main_window.ui +++ b/ui/src/main_window.ui @@ -337,7 +337,7 @@ Axes - true + false @@ -387,7 +387,7 @@ Average light - true + false From 0065a45e7df429a27d8a6d23f30827f3a925de95 Mon Sep 17 00:00:00 2001 From: numzero Date: Tue, 25 Nov 2025 14:40:53 +0300 Subject: [PATCH 35/36] avoid crashes on dir=0 --- src/camera.rs | 6 ++++++ src/lib.rs | 5 ++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/camera.rs b/src/camera.rs index a91e1a7..06542e2 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -20,6 +20,12 @@ impl OrbitalCamera { self.distance * vec3(xy * x, xy * y, z) } + pub fn direction(&self) -> Vec3 { + let (y, x) = self.position_yaw.sin_cos(); + let (z, xy) = self.position_pitch.sin_cos(); + -vec3(xy * x, xy * y, z) + } + pub fn transform(&self) -> Mat4 { // for yaw=0, pitch=0: // X -> -Z diff --git a/src/lib.rs b/src/lib.rs index 432b3b7..746879b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -393,7 +393,10 @@ impl Core { .map(|&v| { let pos = obj.position + obj.radius * v; let normal = v; - let dir = (pos - camera.position()).normalize(); + let mut dir = (pos - camera.position()).normalize(); + if !dir.is_finite() { + dir = camera.direction(); + } let light = light_at(Hit { incident: Ray::new(pos, dir), normal, From 059e98c3e352065a42f8330746ee829660004798 Mon Sep 17 00:00:00 2001 From: numzero Date: Tue, 25 Nov 2025 14:41:10 +0300 Subject: [PATCH 36/36] do color mapping per-pixel --- src/lib.rs | 2 +- src/render/colormap.wgsl | 32 ++++++++++++++++++++++++++++++++ src/render/faces.rs | 2 +- src/render/mod.rs | 1 + 4 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 src/render/colormap.wgsl diff --git a/src/lib.rs b/src/lib.rs index 746879b..048a7cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -401,7 +401,7 @@ impl Core { incident: Ray::new(pos, dir), normal, }); - let color = colormap(light); + let color = Vec3::splat(light); render::faces::Vertex { pos, color } }) .collect::>(), diff --git a/src/render/colormap.wgsl b/src/render/colormap.wgsl new file mode 100644 index 0000000..e2af29d --- /dev/null +++ b/src/render/colormap.wgsl @@ -0,0 +1,32 @@ +struct LookParams { + m: mat4x4f, +} + +struct Vertex { + @location(0) pos: vec3f, + @location(1) color: vec3f, +} + +struct Varying { + @builtin(position) screen: vec4f, + @location(0) color: vec4f, +} + +@group(0) @binding(0) var look: LookParams; + +fn colormap(light: f32) -> vec3f { + let brightness = 3. * (1. - 1. / (1. + light)); + return vec3(brightness, brightness - 1., brightness - 2.); +} + +@vertex +fn on_vertex(in: Vertex) -> Varying { + let pos = look.m * vec4f(in.pos, 1.0); + let color = vec4f(in.color, 1.0); + return Varying(pos, color); +} + +@fragment +fn on_fragment(in: Varying) -> @location(0) vec4f { + return vec4f(colormap(in.color.x), 1.0); +} diff --git a/src/render/faces.rs b/src/render/faces.rs index b6a2a20..7879c30 100644 --- a/src/render/faces.rs +++ b/src/render/faces.rs @@ -79,7 +79,7 @@ impl Pipeline { let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: None, - source: wgpu::ShaderSource::Wgsl(super::SIMPLE_SHADER.into()), + source: wgpu::ShaderSource::Wgsl(super::COLORMAP_SHADER.into()), }); let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: None, diff --git a/src/render/mod.rs b/src/render/mod.rs index 2f629ee..3601ec0 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -2,6 +2,7 @@ pub mod faces; pub mod lines; static SIMPLE_SHADER: &str = include_str!("simple.wgsl"); +static COLORMAP_SHADER: &str = include_str!("colormap.wgsl"); pub const OUTPUT_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Bgra8UnormSrgb; pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth24Plus;