diff --git a/src/camera.rs b/src/camera.rs index 72560ea..88badff 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 4c79b00..0873c0b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,21 +2,25 @@ 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, - render::lines::{LookParams, Mesh, Pipeline, Vertex}, - trace::{Scene, Source, Sphere}, + ray::Ray, + render::{ + DEPTH_FORMAT, OUTPUT_FORMAT, + lines::{LookParams, Mesh, Pipeline, Vertex}, + }, + trace::{Hit, Lambertian, Reflector, Scene, Source, Sphere}, }; mod camera; mod ray; mod render; +mod shape; mod trace; -const OUTPUT_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Bgra8UnormSrgb; - #[derive(Debug, Clone, Copy, PartialEq)] #[repr(C)] pub struct SphericalPosition { @@ -25,6 +29,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 { @@ -32,6 +43,17 @@ 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: u8, + pub use_normal: UseNormal, + 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, } impl Default for RedrawArgs { @@ -49,6 +71,17 @@ impl Default for RedrawArgs { }, light_radius: 0.125, light_spread: 0.125, + accum_sigma: 0.025, + accum_scale: 0.01, + reflections: 2, + use_normal: UseNormal::Light, + 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, } } } @@ -64,7 +97,10 @@ pub struct Core { queue: wgpu::Queue, surface: wgpu::Surface<'static>, + depth: wgpu::Texture, + pipeline: Pipeline, + mesh_pipe: render::faces::Pipeline, tripod: Mesh, } @@ -103,6 +139,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, pixel_size: UVec2) -> Self { let Gpu { @@ -111,8 +163,10 @@ impl Core { surface, } = gpu; - let pipeline = Pipeline::new(&device, OUTPUT_FORMAT); + 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 Self::configure_surface(&surface, &device, pixel_size); @@ -121,8 +175,10 @@ impl Core { device, queue, surface, + depth, pipeline, tripod, + mesh_pipe, } } @@ -145,9 +201,18 @@ 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()); + let depth_view = self + .depth + .create_view(&wgpu::TextureViewDescriptor::default()); let mut encoder = self .device .create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); @@ -157,19 +222,23 @@ 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, + 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() }); - 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, @@ -179,14 +248,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); @@ -210,22 +281,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.), @@ -234,17 +323,154 @@ 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 light_at = { + let sigma2 = args.accum_sigma.powi(2); + let accum_normalizator = (2. * PI * sigma2).recip(); + let hits = &hits; + move |hit: Hit| -> f32 { + 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; + } + 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(normal, light_hit.incident.dir, -hit.incident.dir); + let weight = accum_normalizator * (-0.5 * d2 / sigma2).exp(); + total_cd += weight * out_cd; + } + 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 color = colormap(light); + 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 args.show_shapes { + let mut meshes = Vec::new(); + for obj in &scene.objects { + let mesh = shape::octo_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 mut dir = (pos - camera.position()).normalize(); + if !dir.is_finite() { + dir = camera.direction(); + } + let light = light_at(Hit { + incident: Ray::new(pos, dir), + normal, + }); + let color = Vec3::splat(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)]); + } + 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())); } + 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, @@ -264,6 +490,7 @@ 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. 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 new file mode 100644 index 0000000..7879c30 --- /dev/null +++ b/src/render/faces.rs @@ -0,0 +1,169 @@ +use std::mem::offset_of; + +use bytemuck::{Pod, Zeroable, bytes_of, cast_slice}; +use glam::{Mat4, Vec3}; +use wgpu::util::DeviceExt as _; + +use crate::render::{DEPTH_FORMAT, OUTPUT_FORMAT}; + +#[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) -> 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::COLORMAP_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: 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, + alpha_to_coverage_enabled: false, + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: None, + compilation_options: wgpu::PipelineCompilationOptions::default(), + targets: &[Some(wgpu::ColorTargetState { + format: OUTPUT_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/lines.rs b/src/render/lines.rs index 1b5f3fc..b68fb86 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::{DEPTH_FORMAT, 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, @@ -57,10 +59,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, @@ -90,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, @@ -101,7 +112,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 ef14730..3601ec0 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -1 +1,8 @@ +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; 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 diff --git a/src/shape.rs b/src/shape.rs new file mode 100644 index 0000000..311076d --- /dev/null +++ b/src/shape.rs @@ -0,0 +1,348 @@ +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 } +} + +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; + + 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], + ] + ); + } + + #[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], + ] + ); + } +} 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 ff0383f..d46272e 100644 --- a/ui/src/api.hxx +++ b/ui/src/api.hxx @@ -8,19 +8,36 @@ namespace ffi { struct Core; struct SphericalPosition { - float yaw; - float pitch; - float distance; + float yaw = 0.; + float pitch = 0.; + float distance = 1.; +}; + +enum class UseNormal: std::uint8_t { + Light, + Camera, }; 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::uint8_t reflections = 0; + UseNormal use_normal = UseNormal::Light; + 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 +using ffi::UseNormal; using ffi::RedrawArgs; using ffi::SphericalPosition; diff --git a/ui/src/main_window.cxx b/ui/src/main_window.cxx index 913d406..75861e6 100644 --- a/ui/src/main_window.cxx +++ b/ui/src/main_window.cxx @@ -16,20 +16,50 @@ float deg_to_rad(float val) { } void PhotonLight::updateView() { - m_ui->viewport->setView(RedrawArgs{ + 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()), .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::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(), + .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', 5))); + m_ui->accumScaleLabel->setText(tr("Brightness: %1").arg(QString::number(args.accum_scale, 'f', 5))); + 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 3974303..9c4f169 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,203 @@ + + + + Distance + + + lightDistance + + + + + + + 50 + + + 10 + + + Qt::Horizontal + + + + + + + + + + Lighting + + + + + + Averaging radius + + + accumSigma + + + + + + + -100 + + + 0 + + + -45 + + + Qt::Horizontal + + + + + + + Brightness + + + accumScale + + + + + + + -100 + + + 0 + + + -85 + + + Qt::Horizontal + + + + + + + Reflections + + + reflections + + + + + + + 20 + + + 2 + + + + + + + Use normal at + + + + + + + Hit position + + + true + + + + + + + Show position + + + + + + + + + + Show + + + + + + Axes + + + false + + + + + + + Shapes + + + true + + + + + + + Emitted rays + + + false + + + + + + + Direct incident rays + + + false + + + + + + + Indirect incident rays + + + false + + + + + + + Average light + + + false + + + @@ -196,8 +428,8 @@ updateView() - 688 - 159 + 1585 + 169 403 @@ -212,8 +444,8 @@ updateView() - 688 - 215 + 1585 + 225 403 @@ -228,8 +460,8 @@ updateView() - 688 - 319 + 1585 + 385 403 @@ -244,8 +476,8 @@ updateView() - 688 - 375 + 1585 + 441 403 @@ -253,8 +485,217 @@ + + 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 + + + + + normalFromLight + toggled(bool) + MainWindow + updateViewIf(bool) + + + 1429 + 787 + + + 1381 + 783 + + + + + normalFromCamera + toggled(bool) + MainWindow + updateViewIf(bool) + + + 1932 + 816 + + + 1600 + 874 + + + updateView() + updateViewIf(bool)