From caa4ea3e44ff5ae05c11d80af2bb8ed28cf45390 Mon Sep 17 00:00:00 2001 From: Nathan Vegdahl Date: Thu, 27 Dec 2018 22:57:44 -0800 Subject: [PATCH] Replaced SurfaceClosure trait with a SurfaceClosure enum. Also moved surface closures to using Color internally for color specification. --- src/color.rs | 23 + src/light/rectangle_light.rs | 8 +- src/light/sphere_light.rs | 7 +- src/parse/psy_surface_shader.rs | 127 ++---- src/renderer.rs | 41 +- src/scene/assembly.rs | 2 +- src/shading/mod.rs | 55 +-- src/shading/surface_closure.rs | 773 ++++++++++---------------------- src/surface/mod.rs | 4 +- 9 files changed, 346 insertions(+), 694 deletions(-) diff --git a/src/color.rs b/src/color.rs index 3ac3eae..81698a3 100644 --- a/src/color.rs +++ b/src/color.rs @@ -99,6 +99,29 @@ impl Color { } } +impl Mul for Color { + type Output = Self; + + fn mul(self, rhs: f32) -> Self { + match self { + Color::XYZ(x, y, z) => Color::XYZ(x * rhs, y * rhs, z * rhs), + Color::Blackbody { + temperature, + factor, + } => Color::Blackbody { + temperature: temperature, + factor: factor * rhs, + }, + } + } +} + +impl MulAssign for Color { + fn mul_assign(&mut self, rhs: f32) { + *self = *self * rhs; + } +} + impl Lerp for Color { /// Note that this isn't a proper lerp in spectral space. However, /// for our purposes that should be fine: all we care about is that diff --git a/src/light/rectangle_light.rs b/src/light/rectangle_light.rs index d9818bd..5985bb8 100644 --- a/src/light/rectangle_light.rs +++ b/src/light/rectangle_light.rs @@ -11,7 +11,7 @@ use crate::{ spherical_triangle_solid_angle, triangle_surface_area, uniform_sample_spherical_triangle, uniform_sample_triangle, }, - shading::surface_closure::{EmitClosure, SurfaceClosureUnion}, + shading::surface_closure::SurfaceClosure, shading::SurfaceShader, surface::{triangle, Surface, SurfaceIntersection, SurfaceIntersectionData}, }; @@ -312,10 +312,8 @@ impl<'a> Surface for RectangleLight<'a> { let closure = { let inv_surface_area = (1.0 / (dim.0 as f64 * dim.1 as f64)) as f32; - let color = lerp_slice(self.colors, r.time) - .to_spectral_sample(wr.wavelength) - * inv_surface_area; - SurfaceClosureUnion::EmitClosure(EmitClosure::new(color)) + let color = lerp_slice(self.colors, r.time) * inv_surface_area; + SurfaceClosure::Emit(color) }; // Fill in intersection diff --git a/src/light/sphere_light.rs b/src/light/sphere_light.rs index 9132ec8..d8e8208 100644 --- a/src/light/sphere_light.rs +++ b/src/light/sphere_light.rs @@ -10,7 +10,7 @@ use crate::{ math::{coordinate_system_from_vector, dot, Matrix4x4, Normal, Point, Vector}, ray::{AccelRay, Ray}, sampling::{uniform_sample_cone, uniform_sample_cone_pdf, uniform_sample_sphere}, - shading::surface_closure::{EmitClosure, SurfaceClosureUnion}, + shading::surface_closure::SurfaceClosure, shading::SurfaceShader, surface::{Surface, SurfaceIntersection, SurfaceIntersectionData}, }; @@ -322,9 +322,8 @@ impl<'a> Surface for SphereLight<'a> { let closure = { let inv_surface_area = (1.0 / (4.0 * PI_64 * radius as f64 * radius as f64)) as f32; - let color = lerp_slice(self.colors, r.time).to_spectral_sample(wr.wavelength) - * inv_surface_area; - SurfaceClosureUnion::EmitClosure(EmitClosure::new(color)) + let color = lerp_slice(self.colors, r.time) * inv_surface_area; + SurfaceClosure::Emit(color) }; // Fill in intersection diff --git a/src/parse/psy_surface_shader.rs b/src/parse/psy_surface_shader.rs index bf176f9..27996f8 100644 --- a/src/parse/psy_surface_shader.rs +++ b/src/parse/psy_surface_shader.rs @@ -34,30 +34,6 @@ pub fn parse_surface_shader<'a>( }; let shader = match type_name { - "Emit" => { - let color = if let Some((_, contents, byte_offset)) = - tree.iter_leaf_children_with_type("Color").nth(0) - { - if let IResult::Done(_, color) = - closure!(tuple!(ws_f32, ws_f32, ws_f32))(contents.as_bytes()) - { - // TODO: handle color space conversions properly. - // Probably will need a special color type with its - // own parser...? - Color::new_xyz(rec709_e_to_xyz(color)) - } else { - // Found color, but its contents is not in the right format - return Err(PsyParseError::UnknownError(byte_offset)); - } - } else { - return Err(PsyParseError::MissingNode( - tree.byte_offset(), - "Expected a Color field in Emit SurfaceShader.", - )); - }; - - arena.alloc(SimpleSurfaceShader::Emit { color: color }) - } "Lambert" => { let color = if let Some((_, contents, byte_offset)) = tree.iter_leaf_children_with_type("Color").nth(0) @@ -82,84 +58,6 @@ pub fn parse_surface_shader<'a>( arena.alloc(SimpleSurfaceShader::Lambert { color: color }) } - "GTR" => { - // Color - let color = if let Some((_, contents, byte_offset)) = - tree.iter_leaf_children_with_type("Color").nth(0) - { - if let IResult::Done(_, color) = - closure!(tuple!(ws_f32, ws_f32, ws_f32))(contents.as_bytes()) - { - // TODO: handle color space conversions properly. - // Probably will need a special color type with its - // own parser...? - Color::new_xyz(rec709_e_to_xyz(color)) - } else { - // Found color, but its contents is not in the right format - return Err(PsyParseError::UnknownError(byte_offset)); - } - } else { - return Err(PsyParseError::MissingNode( - tree.byte_offset(), - "Expected a Color field in GTR SurfaceShader.", - )); - }; - - // Roughness - let roughness = if let Some((_, contents, byte_offset)) = - tree.iter_leaf_children_with_type("Roughness").nth(0) - { - if let IResult::Done(_, roughness) = ws_f32(contents.as_bytes()) { - roughness - } else { - return Err(PsyParseError::UnknownError(byte_offset)); - } - } else { - return Err(PsyParseError::MissingNode( - tree.byte_offset(), - "Expected a Roughness field in GTR SurfaceShader.", - )); - }; - - // TailShape - let tail_shape = if let Some((_, contents, byte_offset)) = - tree.iter_leaf_children_with_type("TailShape").nth(0) - { - if let IResult::Done(_, tail_shape) = ws_f32(contents.as_bytes()) { - tail_shape - } else { - return Err(PsyParseError::UnknownError(byte_offset)); - } - } else { - return Err(PsyParseError::MissingNode( - tree.byte_offset(), - "Expected a TailShape field in GTR SurfaceShader.", - )); - }; - - // Fresnel - let fresnel = if let Some((_, contents, byte_offset)) = - tree.iter_leaf_children_with_type("Fresnel").nth(0) - { - if let IResult::Done(_, fresnel) = ws_f32(contents.as_bytes()) { - fresnel - } else { - return Err(PsyParseError::UnknownError(byte_offset)); - } - } else { - return Err(PsyParseError::MissingNode( - tree.byte_offset(), - "Expected a Fresnel field in GTR SurfaceShader.", - )); - }; - - arena.alloc(SimpleSurfaceShader::GTR { - color: color, - roughness: roughness, - tail_shape: tail_shape, - fresnel: fresnel, - }) - } "GGX" => { // Color @@ -223,6 +121,31 @@ pub fn parse_surface_shader<'a>( }) } + "Emit" => { + let color = if let Some((_, contents, byte_offset)) = + tree.iter_leaf_children_with_type("Color").nth(0) + { + if let IResult::Done(_, color) = + closure!(tuple!(ws_f32, ws_f32, ws_f32))(contents.as_bytes()) + { + // TODO: handle color space conversions properly. + // Probably will need a special color type with its + // own parser...? + Color::new_xyz(rec709_e_to_xyz(color)) + } else { + // Found color, but its contents is not in the right format + return Err(PsyParseError::UnknownError(byte_offset)); + } + } else { + return Err(PsyParseError::MissingNode( + tree.byte_offset(), + "Expected a Color field in Emit SurfaceShader.", + )); + }; + + arena.alloc(SimpleSurfaceShader::Emit { color: color }) + } + _ => unimplemented!(), }; diff --git a/src/renderer.rs b/src/renderer.rs index c1cc68c..8f1471f 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -14,7 +14,7 @@ use float4::Float4; use crate::{ accel::{ACCEL_NODE_RAY_TESTS, ACCEL_TRAV_TIME}, algorithm::partition_pair, - color::{map_0_1_to_wavelength, Color, SpectralSample, XYZ}, + color::{map_0_1_to_wavelength, SpectralSample, XYZ}, fp_utils::robust_ray_origin, hash::hash_u32, hilbert, @@ -447,14 +447,15 @@ impl LightPath { // If it's an emission closure, handle specially: // - Collect light from the emission. // - Terminate the path. - use crate::shading::surface_closure::SurfaceClosureUnion; - if let SurfaceClosureUnion::EmitClosure(ref clsr) = *closure { + use crate::shading::surface_closure::SurfaceClosure; + if let SurfaceClosure::Emit(color) = *closure { + let color = color.to_spectral_sample(self.wavelength).e; if let LightPathEvent::CameraRay = self.event { - self.color += clsr.emitted_color().e; + self.color += color; } else { let mis_pdf = power_heuristic(self.closure_sample_pdf, idata.sample_pdf); - self.color += clsr.emitted_color().e * self.light_attenuation / mis_pdf; + self.color += color * self.light_attenuation / mis_pdf; }; return false; @@ -487,7 +488,6 @@ impl LightPath { } else { let light_pdf = light_info.pdf(); let light_sel_pdf = light_info.selection_pdf(); - let material = closure.as_surface_closure(); // Calculate the shadow ray and surface closure stuff let (attenuation, closure_pdf, shadow_ray) = match light_info { @@ -495,8 +495,13 @@ impl LightPath { // Distant light SceneLightSample::Distant { direction, .. } => { - let (attenuation, closure_pdf) = - material.evaluate(ray.dir, direction, idata.nor, idata.nor_g); + let (attenuation, closure_pdf) = closure.evaluate( + ray.dir, + direction, + idata.nor, + idata.nor_g, + self.wavelength, + ); let mut shadow_ray = { // Calculate the shadow ray for testing if the light is // in shadow or not. @@ -521,8 +526,13 @@ impl LightPath { // Surface light SceneLightSample::Surface { sample_geo, .. } => { let dir = sample_geo.0 - idata.pos; - let (attenuation, closure_pdf) = - material.evaluate(ray.dir, dir, idata.nor, idata.nor_g); + let (attenuation, closure_pdf) = closure.evaluate( + ray.dir, + dir, + idata.nor, + idata.nor_g, + self.wavelength, + ); let shadow_ray = { // Calculate the shadow ray for testing if the light is // in shadow or not. @@ -572,12 +582,17 @@ impl LightPath { let do_bounce = if self.bounce_count < 2 { self.bounce_count += 1; - // Sample material + // Sample closure let (dir, filter, pdf) = { - let material = closure.as_surface_closure(); let u = self.next_lds_samp(); let v = self.next_lds_samp(); - material.sample(idata.incoming, idata.nor, idata.nor_g, (u, v)) + closure.sample( + idata.incoming, + idata.nor, + idata.nor_g, + (u, v), + self.wavelength, + ) }; // Check if pdf is zero, to avoid NaN's. diff --git a/src/scene/assembly.rs b/src/scene/assembly.rs index 0e52361..3d66288 100644 --- a/src/scene/assembly.rs +++ b/src/scene/assembly.rs @@ -67,7 +67,7 @@ impl<'a> Assembly<'a> { idata.pos * sel_xform, idata.nor * sel_xform, idata.nor_g * sel_xform, - closure.as_surface_closure(), + &closure, time, n, ) { diff --git a/src/shading/mod.rs b/src/shading/mod.rs index a94046e..c82c98f 100644 --- a/src/shading/mod.rs +++ b/src/shading/mod.rs @@ -4,20 +4,13 @@ use std::fmt::Debug; use crate::{color::Color, surface::SurfaceIntersectionData}; -use self::surface_closure::{ - EmitClosure, GGXClosure, GTRClosure, LambertClosure, SurfaceClosureUnion, -}; +use self::surface_closure::SurfaceClosure; /// Trait for surface shaders. pub trait SurfaceShader: Debug + Sync { /// Takes the result of a surface intersection and returns the surface /// closure to be evaluated at that intersection point. - fn shade( - &self, - data: &SurfaceIntersectionData, - time: f32, - wavelength: f32, - ) -> SurfaceClosureUnion; + fn shade(&self, data: &SurfaceIntersectionData, time: f32, wavelength: f32) -> SurfaceClosure; } /// Clearly we must eat this brownie before the world ends, lest it @@ -39,12 +32,6 @@ pub enum SimpleSurfaceShader { Lambert { color: Color, }, - GTR { - color: Color, - roughness: f32, - tail_shape: f32, - fresnel: f32, - }, GGX { color: Color, roughness: f32, @@ -53,41 +40,23 @@ pub enum SimpleSurfaceShader { } impl SurfaceShader for SimpleSurfaceShader { - fn shade( - &self, - data: &SurfaceIntersectionData, - time: f32, - wavelength: f32, - ) -> SurfaceClosureUnion { + fn shade(&self, data: &SurfaceIntersectionData, time: f32, wavelength: f32) -> SurfaceClosure { let _ = (data, time); // Silence "unused" compiler warning match *self { - SimpleSurfaceShader::Emit { color } => SurfaceClosureUnion::EmitClosure( - EmitClosure::new(color.to_spectral_sample(wavelength)), - ), - SimpleSurfaceShader::Lambert { color } => SurfaceClosureUnion::LambertClosure( - LambertClosure::new(color.to_spectral_sample(wavelength)), - ), - SimpleSurfaceShader::GTR { - color, - roughness, - tail_shape, - fresnel, - } => SurfaceClosureUnion::GTRClosure(GTRClosure::new( - color.to_spectral_sample(wavelength), - roughness, - tail_shape, - fresnel, - )), + SimpleSurfaceShader::Emit { color } => SurfaceClosure::Emit(color), + + SimpleSurfaceShader::Lambert { color } => SurfaceClosure::Lambert(color), + SimpleSurfaceShader::GGX { color, roughness, fresnel, - } => SurfaceClosureUnion::GGXClosure(GGXClosure::new( - color.to_spectral_sample(wavelength), - roughness, - fresnel, - )), + } => SurfaceClosure::GGX { + color: color, + roughness: roughness, + fresnel: fresnel, + }, } } } diff --git a/src/shading/surface_closure.rs b/src/shading/surface_closure.rs index 314d802..6699a66 100644 --- a/src/shading/surface_closure.rs +++ b/src/shading/surface_closure.rs @@ -2,8 +2,10 @@ use std::f32::consts::PI as PI_32; +use float4::Float4; + use crate::{ - color::SpectralSample, + color::{Color, SpectralSample}, lerp::lerp, math::{clamp, dot, zup_to_vec, Normal, Vector}, sampling::cosine_sample_hemisphere, @@ -12,71 +14,97 @@ use crate::{ const INV_PI: f32 = 1.0 / PI_32; const H_PI: f32 = PI_32 / 2.0; +/// A surface closure, specifying a BSDF for a point on a surface. #[derive(Debug, Copy, Clone)] -pub enum SurfaceClosureUnion { - EmitClosure(EmitClosure), - LambertClosure(LambertClosure), - GTRClosure(GTRClosure), - GGXClosure(GGXClosure), +pub enum SurfaceClosure { + // Normal surface closures. + Lambert(Color), + GGX { + color: Color, + roughness: f32, + fresnel: f32, // [0.0, 1.0] determines how much fresnel reflection comes into play + }, + + // Special closures that need special handling by the renderer. + Emit(Color), } -impl SurfaceClosureUnion { - pub fn as_surface_closure(&self) -> &SurfaceClosure { +use self::SurfaceClosure::*; + +/// Note when implementing new BSDFs: both the the color filter and pdf returned from +/// `sample()` and `evaluate()` should be identical for the same parameters and outgoing +/// light direction. +impl SurfaceClosure { + /// Returns whether the closure has a delta distribution or not. + pub fn is_delta(&self) -> bool { match *self { - SurfaceClosureUnion::EmitClosure(ref closure) => closure as &SurfaceClosure, - SurfaceClosureUnion::LambertClosure(ref closure) => closure as &SurfaceClosure, - SurfaceClosureUnion::GTRClosure(ref closure) => closure as &SurfaceClosure, - SurfaceClosureUnion::GGXClosure(ref closure) => closure as &SurfaceClosure, + Lambert(_) => false, + GGX { roughness, .. } => roughness == 0.0, + Emit(_) => false, } } -} - -/// Trait for surface closures. -/// -/// Note: each surface closure is assumed to be bound to a particular hero -/// wavelength. This is implicit in the `sample`, `evaluate`, and `sample_pdf` -/// functions below. -/// -/// Also important is that _both_ the color filter and pdf returned from -/// `sample()` and `evaluate()` should be identical for the same parameters -/// and outgoing light direction. -pub trait SurfaceClosure { - /// Returns whether the closure has a delta distribution or not. - fn is_delta(&self) -> bool; /// Given an incoming ray and sample values, generates an outgoing ray and /// color filter. /// - /// inc: Incoming light direction. - /// nor: The shading surface normal at the surface point. - /// nor_g: The geometric surface normal at the surface point. - /// uv: The sampling values. + /// inc: Incoming light direction. + /// nor: The shading surface normal at the surface point. + /// nor_g: The geometric surface normal at the surface point. + /// uv: The sampling values. + /// wavelength: Hero wavelength to generate the color filter for. /// /// Returns a tuple with the generated outgoing light direction, color filter, and pdf. - fn sample( + pub fn sample( &self, inc: Vector, nor: Normal, nor_g: Normal, uv: (f32, f32), - ) -> (Vector, SpectralSample, f32); + wavelength: f32, + ) -> (Vector, SpectralSample, f32) { + match *self { + Lambert(color) => lambert_closure::sample(color, inc, nor, nor_g, uv, wavelength), + + GGX { + color, + roughness, + fresnel, + } => ggx_closure::sample(color, roughness, fresnel, inc, nor, nor_g, uv, wavelength), + + Emit(color) => emit_closure::sample(color, inc, nor, nor_g, uv, wavelength), + } + } /// Evaluates the closure for the given incoming and outgoing rays. /// - /// inc: The incoming light direction. - /// out: The outgoing light direction. - /// nor: The shading surface normal at the surface point. - /// nor_g: The geometric surface normal at the surface point. + /// inc: The incoming light direction. + /// out: The outgoing light direction. + /// nor: The shading surface normal at the surface point. + /// nor_g: The geometric surface normal at the surface point. + /// wavelength: Hero wavelength to generate the color filter for. /// /// Returns the resulting filter color and pdf of if this had been generated /// by `sample()`. - fn evaluate( + pub fn evaluate( &self, inc: Vector, out: Vector, nor: Normal, nor_g: Normal, - ) -> (SpectralSample, f32); + wavelength: f32, + ) -> (SpectralSample, f32) { + match *self { + Lambert(color) => lambert_closure::evaluate(color, inc, out, nor, nor_g, wavelength), + + GGX { + color, + roughness, + fresnel, + } => ggx_closure::evaluate(color, roughness, fresnel, inc, out, nor, nor_g, wavelength), + + Emit(color) => emit_closure::evaluate(color, inc, out, nor, nor_g, wavelength), + } + } /// Returns an estimate of the sum total energy that evaluate() would return /// when integrated over a spherical light source with a center at relative @@ -84,134 +112,7 @@ pub trait SurfaceClosure { /// This is used for importance sampling, so does not need to be exact, /// but it does need to be non-zero anywhere that an exact solution would /// be non-zero. - fn estimate_eval_over_sphere_light( - &self, - inc: Vector, - to_light_center: Vector, - light_radius_squared: f32, - nor: Normal, - nor_g: Normal, - ) -> f32; -} - -/// Utility function that calculates the fresnel reflection factor of a given -/// incoming ray against a surface with the given ior outside/inside ratio. -/// -/// `ior_ratio`: The ratio of the outside material ior (probably 1.0 for air) -/// over the inside ior. -/// `c`: The cosine of the angle between the incoming light and the -/// surface's normal. Probably calculated e.g. with a normalized -/// dot product. -#[allow(dead_code)] -fn dielectric_fresnel(ior_ratio: f32, c: f32) -> f32 { - let g = (ior_ratio - 1.0 + (c * c)).sqrt(); - - let f1 = g - c; - let f2 = g + c; - let f3 = (f1 * f1) / (f2 * f2); - - let f4 = (c * f2) - 1.0; - let f5 = (c * f1) + 1.0; - let f6 = 1.0 + ((f4 * f4) / (f5 * f5)); - - 0.5 * f3 * f6 -} - -/// Schlick's approximation of the fresnel reflection factor. -/// -/// Same interface as `dielectric_fresnel()`, above. -#[allow(dead_code)] -fn schlick_fresnel(ior_ratio: f32, c: f32) -> f32 { - let f1 = (1.0 - ior_ratio) / (1.0 + ior_ratio); - let f2 = f1 * f1; - let c1 = 1.0 - c; - let c2 = c1 * c1; - - f2 + ((1.0 - f2) * c1 * c2 * c2) -} - -/// Utility function that calculates the fresnel reflection factor of a given -/// incoming ray against a surface with the given normal-reflectance factor. -/// -/// `frensel_fac`: The ratio of light reflected back if the ray were to -/// hit the surface head-on (perpendicular to the surface). -/// `c`: The cosine of the angle between the incoming light and the -/// surface's normal. Probably calculated e.g. with a normalized -/// dot product. -#[allow(dead_code)] -fn dielectric_fresnel_from_fac(fresnel_fac: f32, c: f32) -> f32 { - let tmp1 = fresnel_fac.sqrt() - 1.0; - - // Protect against divide by zero. - if tmp1.abs() < 0.000_001 { - return 1.0; - } - - // Find the ior ratio - let tmp2 = (-2.0 / tmp1) - 1.0; - let ior_ratio = tmp2 * tmp2; - - // Calculate fresnel factor - dielectric_fresnel(ior_ratio, c) -} - -/// Schlick's approximation version of `dielectric_fresnel_from_fac()` above. -#[allow(dead_code)] -fn schlick_fresnel_from_fac(frensel_fac: f32, c: f32) -> f32 { - let c1 = 1.0 - c; - let c2 = c1 * c1; - frensel_fac + ((1.0 - frensel_fac) * c1 * c2 * c2) -} - -/// Emit closure. -/// -/// NOTE: this needs to be handled specially by the integrator! It does not -/// behave like a standard closure! -#[derive(Debug, Copy, Clone)] -pub struct EmitClosure { - col: SpectralSample, -} - -impl EmitClosure { - pub fn new(color: SpectralSample) -> EmitClosure { - EmitClosure { col: color } - } - - pub fn emitted_color(&self) -> SpectralSample { - self.col - } -} - -impl SurfaceClosure for EmitClosure { - fn is_delta(&self) -> bool { - false - } - - fn sample( - &self, - inc: Vector, - nor: Normal, - nor_g: Normal, - uv: (f32, f32), - ) -> (Vector, SpectralSample, f32) { - let _ = (inc, nor, nor_g, uv); // Not using these, silence warning - - (Vector::new(0.0, 0.0, 0.0), self.col, 1.0) - } - - fn evaluate( - &self, - inc: Vector, - out: Vector, - nor: Normal, - nor_g: Normal, - ) -> (SpectralSample, f32) { - let _ = (inc, out, nor, nor_g); // Not using these, silence warning - - (self.col, 1.0) - } - - fn estimate_eval_over_sphere_light( + pub fn estimate_eval_over_sphere_light( &self, inc: Vector, to_light_center: Vector, @@ -219,37 +120,52 @@ impl SurfaceClosure for EmitClosure { nor: Normal, nor_g: Normal, ) -> f32 { - // Not using these, silence warning - let _ = (inc, to_light_center, light_radius_squared, nor, nor_g); - - // TODO: what to do here? - unimplemented!() + match *self { + Lambert(color) => lambert_closure::estimate_eval_over_sphere_light( + color, + inc, + to_light_center, + light_radius_squared, + nor, + nor_g, + ), + GGX { + color, + roughness, + fresnel, + } => ggx_closure::estimate_eval_over_sphere_light( + color, + roughness, + fresnel, + inc, + to_light_center, + light_radius_squared, + nor, + nor_g, + ), + Emit(color) => emit_closure::estimate_eval_over_sphere_light( + color, + inc, + to_light_center, + light_radius_squared, + nor, + nor_g, + ), + } } } -/// Lambertian surface closure -#[derive(Debug, Copy, Clone)] -pub struct LambertClosure { - col: SpectralSample, -} +/// Lambert closure code. +mod lambert_closure { + use super::*; -impl LambertClosure { - pub fn new(col: SpectralSample) -> LambertClosure { - LambertClosure { col: col } - } -} - -impl SurfaceClosure for LambertClosure { - fn is_delta(&self) -> bool { - false - } - - fn sample( - &self, + pub fn sample( + color: Color, inc: Vector, nor: Normal, nor_g: Normal, uv: (f32, f32), + wavelength: f32, ) -> (Vector, SpectralSample, f32) { let (nn, flipped_nor_g) = if dot(nor_g.into_vector(), inc) <= 0.0 { (nor.normalized().into_vector(), nor_g.into_vector()) @@ -265,18 +181,19 @@ impl SurfaceClosure for LambertClosure { // Make sure it's not on the wrong side of the geometric normal. if dot(flipped_nor_g, out) >= 0.0 { - (out, self.col * pdf, pdf) + (out, color.to_spectral_sample(wavelength) * pdf, pdf) } else { (out, SpectralSample::new(0.0), 0.0) } } - fn evaluate( - &self, + pub fn evaluate( + color: Color, inc: Vector, out: Vector, nor: Normal, nor_g: Normal, + wavelength: f32, ) -> (SpectralSample, f32) { let (nn, flipped_nor_g) = if dot(nor_g.into_vector(), inc) <= 0.0 { (nor.normalized().into_vector(), nor_g.into_vector()) @@ -286,14 +203,14 @@ impl SurfaceClosure for LambertClosure { if dot(flipped_nor_g, out) >= 0.0 { let fac = dot(nn, out.normalized()).max(0.0) * INV_PI; - (self.col * fac, fac) + (color.to_spectral_sample(wavelength) * fac, fac) } else { (SpectralSample::new(0.0), 0.0) } } - fn estimate_eval_over_sphere_light( - &self, + pub fn estimate_eval_over_sphere_light( + color: Color, inc: Vector, to_light_center: Vector, light_radius_squared: f32, @@ -369,113 +286,24 @@ impl SurfaceClosure for LambertClosure { } } -/// The GTR microfacet BRDF from the Disney Principled BRDF paper. -#[derive(Debug, Copy, Clone)] -pub struct GTRClosure { - col: SpectralSample, - roughness: f32, - tail_shape: f32, - fresnel: f32, // [0.0, 1.0] determines how much fresnel reflection comes into play - normalization_factor: f32, -} - -impl GTRClosure { - pub fn new(col: SpectralSample, roughness: f32, tail_shape: f32, fresnel: f32) -> GTRClosure { - let mut closure = GTRClosure { - col: col, - roughness: roughness, - tail_shape: tail_shape, - fresnel: fresnel, - normalization_factor: GTRClosure::normalization(roughness, tail_shape), - }; - - closure.validate(); - - closure - } - - // Returns the normalization factor for the distribution function - // of the BRDF. - fn normalization(r: f32, t: f32) -> f32 { - let r2 = r * r; - let top = (t - 1.0) * (r2 - 1.0); - let bottom = PI_32 * (1.0 - r2.powf(1.0 - t)); - top / bottom - } +mod ggx_closure { + use super::*; // Makes sure values are in a valid range - fn validate(&mut self) { - debug_assert!(self.fresnel >= 0.0 && self.fresnel <= 1.0); - - // Clamp values to valid ranges - self.roughness = clamp(self.roughness, 0.0, 0.9999); - self.tail_shape = (0.0001f32).max(self.tail_shape); - - // When roughness is too small, but not zero, there are floating point accuracy issues - if self.roughness < 0.000_244_140_625 { - // (2^-12) - self.roughness = 0.0; - } - - // If tail_shape is too near 1.0, push it away a tiny bit. - // This avoids having to have a special form of various equations - // due to a singularity at tail_shape = 1.0 - // That in turn avoids some branches in the code, and the effect of - // tail_shape is sufficiently subtle that there is no visible - // difference in renders. - const TAIL_EPSILON: f32 = 0.0001; - if (self.tail_shape - 1.0).abs() < TAIL_EPSILON { - self.tail_shape = 1.0 + TAIL_EPSILON; - } - - // Precalculate normalization factor - self.normalization_factor = GTRClosure::normalization(self.roughness, self.tail_shape); + pub fn validate(roughness: f32, fresnel: f32) { + debug_assert!(fresnel >= 0.0 && fresnel <= 1.0); + debug_assert!(roughness >= 0.0 && roughness <= 1.0); } - // Returns the cosine of the half-angle that should be sampled, given - // a random variable in [0,1] - fn half_theta_sample(&self, u: f32) -> f32 { - let roughness2 = self.roughness * self.roughness; - - // Calculate top half of equation - let top = 1.0 - - ((roughness2.powf(1.0 - self.tail_shape) * (1.0 - u)) + u) - .powf(1.0 / (1.0 - self.tail_shape)); - - // Calculate bottom half of equation - let bottom = 1.0 - roughness2; - - (top / bottom).sqrt() - } - - /// Microfacet distribution function. - /// - /// nh: cosine of the angle between the surface normal and the microfacet normal. - fn dist(&self, nh: f32, rough: f32) -> f32 { - // Other useful numbers - let roughness2 = rough * rough; - - // Calculate D - Distribution - if nh <= 0.0 { - 0.0 - } else { - let nh2 = nh * nh; - self.normalization_factor / (1.0 + ((roughness2 - 1.0) * nh2)).powf(self.tail_shape) - } - } -} - -impl SurfaceClosure for GTRClosure { - fn is_delta(&self) -> bool { - self.roughness == 0.0 - } - - fn sample( - &self, + pub fn sample( + col: Color, + roughness: f32, + fresnel: f32, inc: Vector, nor: Normal, nor_g: Normal, uv: (f32, f32), + wavelength: f32, ) -> (Vector, SpectralSample, f32) { // Get normalized surface normal let (nn, flipped_nor_g) = if dot(nor_g.into_vector(), inc) <= 0.0 { @@ -486,7 +314,7 @@ impl SurfaceClosure for GTRClosure { // Generate a random ray direction in the hemisphere // of the surface. - let theta_cos = self.half_theta_sample(uv.0); + let theta_cos = half_theta_sample(uv.0, roughness); let theta_sin = (1.0 - (theta_cos * theta_cos)).sqrt(); let angle = uv.1 * PI_32 * 2.0; let mut half_dir = Vector::new(angle.cos() * theta_sin, angle.sin() * theta_sin, theta_cos); @@ -496,19 +324,22 @@ impl SurfaceClosure for GTRClosure { // Make sure it's not on the wrong side of the geometric normal. if dot(flipped_nor_g, out) >= 0.0 { - let (filter, pdf) = self.evaluate(inc, out, nor, nor_g); + let (filter, pdf) = evaluate(col, roughness, fresnel, inc, out, nor, nor_g, wavelength); (out, filter, pdf) } else { (out, SpectralSample::new(0.0), 0.0) } } - fn evaluate( - &self, + pub fn evaluate( + col: Color, + roughness: f32, + fresnel: f32, inc: Vector, out: Vector, nor: Normal, nor_g: Normal, + wavelength: f32, ) -> (SpectralSample, f32) { // Calculate needed vectors, normalized let aa = -inc.normalized(); // Vector pointing to where "in" came from @@ -534,77 +365,55 @@ impl SurfaceClosure for GTRClosure { let hb = clamp(dot(hh, bb), -1.0, 1.0); let nh = clamp(dot(nn, hh), -1.0, 1.0); - // Other useful numbers - let roughness2 = self.roughness * self.roughness; - // Calculate F - Fresnel let col_f = { - let rev_fresnel = 1.0 - self.fresnel; + let spectrum_sample = col.to_spectral_sample(wavelength); + let rev_fresnel = 1.0 - fresnel; let c0 = lerp( - schlick_fresnel_from_fac(self.col.e.get_0(), hb), - self.col.e.get_0(), + schlick_fresnel_from_fac(spectrum_sample.e.get_0(), hb), + spectrum_sample.e.get_0(), rev_fresnel, ); let c1 = lerp( - schlick_fresnel_from_fac(self.col.e.get_1(), hb), - self.col.e.get_1(), + schlick_fresnel_from_fac(spectrum_sample.e.get_1(), hb), + spectrum_sample.e.get_1(), rev_fresnel, ); let c2 = lerp( - schlick_fresnel_from_fac(self.col.e.get_2(), hb), - self.col.e.get_2(), + schlick_fresnel_from_fac(spectrum_sample.e.get_2(), hb), + spectrum_sample.e.get_2(), rev_fresnel, ); let c3 = lerp( - schlick_fresnel_from_fac(self.col.e.get_3(), hb), - self.col.e.get_3(), + schlick_fresnel_from_fac(spectrum_sample.e.get_3(), hb), + spectrum_sample.e.get_3(), rev_fresnel, ); - let mut col_f = self.col; - col_f.e.set_0(c0); - col_f.e.set_1(c1); - col_f.e.set_2(c2); - col_f.e.set_3(c3); - - col_f + SpectralSample::from_parts(Float4::new(c0, c1, c2, c3), wavelength) }; // Calculate everything else - if self.roughness == 0.0 { + if roughness == 0.0 { // If sharp mirror, just return col * fresnel factor return (col_f, 0.0); } else { // Calculate D - Distribution - let dist = self.dist(nh, self.roughness) / na; + let dist = ggx_d(nh, roughness) / na; - // Calculate G1 - Geometric microfacet shadowing - let g1 = { - let na2 = na * na; - let tan_na = ((1.0 - na2) / na2).sqrt(); - let g1_pos_char = if (ha * na) > 0.0 { 1.0 } else { 0.0 }; - let g1_a = roughness2 * tan_na; - let g1_b = ((1.0 + (g1_a * g1_a)).sqrt() - 1.0) * 0.5; - g1_pos_char / (1.0 + g1_b) - }; - - // Calculate G2 - Geometric microfacet shadowing - let g2 = { - let nb2 = nb * nb; - let tan_nb = ((1.0 - nb2) / nb2).sqrt(); - let g2_pos_char = if (hb * nb) > 0.0 { 1.0 } else { 0.0 }; - let g2_a = roughness2 * tan_nb; - let g2_b = ((1.0 + (g2_a * g2_a)).sqrt() - 1.0) * 0.5; - g2_pos_char / (1.0 + g2_b) - }; + // Calculate G1 and G2- Geometric microfacet shadowing + let g1 = ggx_g(ha, na, roughness); + let g2 = ggx_g(hb, nb, roughness); // Final result (col_f * (dist * g1 * g2) * INV_PI, dist * INV_PI) } } - fn estimate_eval_over_sphere_light( - &self, + pub fn estimate_eval_over_sphere_light( + col: Color, + roughness: f32, + fresnel: f32, inc: Vector, to_light_center: Vector, light_radius_squared: f32, @@ -645,7 +454,7 @@ impl SurfaceClosure for GTRClosure { // samp = zup_to_vec(samp, bb).normalized(); // if dot(nn, samp) > 0.0 { // let hh = (aa+samp).normalized(); - // fac += self.dist(dot(nn, hh), roughness); + // fac += ggx_d(dot(nn, hh), roughness); // } //} //fac /= N * N; @@ -654,41 +463,12 @@ impl SurfaceClosure for GTRClosure { let theta = cos_theta_max.acos(); let hh = (aa + bb).normalized(); let nh = clamp(dot(nn, hh), -1.0, 1.0); - let fac = self.dist( - nh, - (1.0f32).min(self.roughness.sqrt() + (2.0 * theta / PI_32)), - ); + let fac = ggx_d(nh, (1.0f32).min(roughness.sqrt() + (2.0 * theta / PI_32))); fac * (1.0f32).min(1.0 - cos_theta_max) * INV_PI } -} -/// The GGX microfacet BRDF. -#[derive(Debug, Copy, Clone)] -pub struct GGXClosure { - col: SpectralSample, - roughness: f32, - fresnel: f32, // [0.0, 1.0] determines how much fresnel reflection comes into play -} - -impl GGXClosure { - pub fn new(col: SpectralSample, roughness: f32, fresnel: f32) -> GGXClosure { - let mut closure = GGXClosure { - col: col, - roughness: roughness, - fresnel: fresnel, - }; - - closure.validate(); - - closure - } - - // Makes sure values are in a valid range - fn validate(&mut self) { - debug_assert!(self.fresnel >= 0.0 && self.fresnel <= 1.0); - debug_assert!(self.roughness >= 0.0 && self.roughness <= 1.0); - } + //---------------------------------------------------- // Returns the cosine of the half-angle that should be sampled, given // a random variable in [0,1] @@ -730,181 +510,126 @@ impl GGXClosure { } } -impl SurfaceClosure for GGXClosure { - fn is_delta(&self) -> bool { - self.roughness == 0.0 - } +/// Emit closure code. +/// +/// NOTE: this needs to be handled specially by the integrator! It does not +/// behave like a standard closure! +mod emit_closure { + use super::*; - fn sample( - &self, + pub fn sample( + color: Color, inc: Vector, nor: Normal, nor_g: Normal, uv: (f32, f32), + wavelength: f32, ) -> (Vector, SpectralSample, f32) { - // Get normalized surface normal - let (nn, flipped_nor_g) = if dot(nor_g.into_vector(), inc) <= 0.0 { - (nor.normalized().into_vector(), nor_g.into_vector()) - } else { - (-nor.normalized().into_vector(), -nor_g.into_vector()) - }; + let _ = (inc, nor, nor_g, uv); // Not using these, silence warning - // Generate a random ray direction in the hemisphere - // of the surface. - let theta_cos = Self::half_theta_sample(uv.0, self.roughness); - let theta_sin = (1.0 - (theta_cos * theta_cos)).sqrt(); - let angle = uv.1 * PI_32 * 2.0; - let mut half_dir = Vector::new(angle.cos() * theta_sin, angle.sin() * theta_sin, theta_cos); - half_dir = zup_to_vec(half_dir, nn).normalized(); - - let out = inc - (half_dir * 2.0 * dot(inc, half_dir)); - - // Make sure it's not on the wrong side of the geometric normal. - if dot(flipped_nor_g, out) >= 0.0 { - let (filter, pdf) = self.evaluate(inc, out, nor, nor_g); - (out, filter, pdf) - } else { - (out, SpectralSample::new(0.0), 0.0) - } + ( + Vector::new(0.0, 0.0, 0.0), + color.to_spectral_sample(wavelength), + 1.0, + ) } - fn evaluate( - &self, + pub fn evaluate( + color: Color, inc: Vector, out: Vector, nor: Normal, nor_g: Normal, + wavelength: f32, ) -> (SpectralSample, f32) { - // Calculate needed vectors, normalized - let aa = -inc.normalized(); // Vector pointing to where "in" came from - let bb = out.normalized(); // Out - let hh = (aa + bb).normalized(); // Half-way between aa and bb + let _ = (inc, out, nor, nor_g); // Not using these, silence warning - // Surface normal - let (nn, flipped_nor_g) = if dot(nor_g.into_vector(), inc) <= 0.0 { - (nor.normalized().into_vector(), nor_g.into_vector()) - } else { - (-nor.normalized().into_vector(), -nor_g.into_vector()) - }; - - // Make sure everything's on the correct side of the surface - if dot(nn, aa) < 0.0 || dot(nn, bb) < 0.0 || dot(flipped_nor_g, bb) < 0.0 { - return (SpectralSample::new(0.0), 0.0); - } - - // Calculate needed dot products - let na = clamp(dot(nn, aa), -1.0, 1.0); - let nb = clamp(dot(nn, bb), -1.0, 1.0); - let ha = clamp(dot(hh, aa), -1.0, 1.0); - let hb = clamp(dot(hh, bb), -1.0, 1.0); - let nh = clamp(dot(nn, hh), -1.0, 1.0); - - // Calculate F - Fresnel - let col_f = { - let rev_fresnel = 1.0 - self.fresnel; - let c0 = lerp( - schlick_fresnel_from_fac(self.col.e.get_0(), hb), - self.col.e.get_0(), - rev_fresnel, - ); - let c1 = lerp( - schlick_fresnel_from_fac(self.col.e.get_1(), hb), - self.col.e.get_1(), - rev_fresnel, - ); - let c2 = lerp( - schlick_fresnel_from_fac(self.col.e.get_2(), hb), - self.col.e.get_2(), - rev_fresnel, - ); - let c3 = lerp( - schlick_fresnel_from_fac(self.col.e.get_3(), hb), - self.col.e.get_3(), - rev_fresnel, - ); - - let mut col_f = self.col; - col_f.e.set_0(c0); - col_f.e.set_1(c1); - col_f.e.set_2(c2); - col_f.e.set_3(c3); - - col_f - }; - - // Calculate everything else - if self.roughness == 0.0 { - // If sharp mirror, just return col * fresnel factor - return (col_f, 0.0); - } else { - // Calculate D - Distribution - let dist = Self::ggx_d(nh, self.roughness) / na; - - // Calculate G1 and G2- Geometric microfacet shadowing - let g1 = Self::ggx_g(ha, na, self.roughness); - let g2 = Self::ggx_g(hb, nb, self.roughness); - - // Final result - (col_f * (dist * g1 * g2) * INV_PI, dist * INV_PI) - } + (color.to_spectral_sample(wavelength), 1.0) } - fn estimate_eval_over_sphere_light( - &self, + pub fn estimate_eval_over_sphere_light( + color: Color, inc: Vector, to_light_center: Vector, light_radius_squared: f32, nor: Normal, nor_g: Normal, ) -> f32 { - // TODO: all of the stuff in this function is horribly hacky. - // Find a proper way to approximate the light contribution from a - // solid angle. + // Not using these, silence warning + let _ = (inc, to_light_center, light_radius_squared, nor, nor_g); - let _ = nor_g; // Not using this, silence warning - - let dist2 = to_light_center.length2(); - let sin_theta_max2 = (light_radius_squared / dist2).min(1.0); - let cos_theta_max = (1.0 - sin_theta_max2).sqrt(); - - assert!(cos_theta_max >= -1.0); - assert!(cos_theta_max <= 1.0); - - // Surface normal - let nn = if dot(nor.into_vector(), inc) < 0.0 { - nor.normalized() - } else { - -nor.normalized() // If back-facing, flip normal - } - .into_vector(); - - let aa = -inc.normalized(); // Vector pointing to where "in" came from - let bb = to_light_center.normalized(); // Out - - // Brute-force method - //let mut fac = 0.0; - //const N: usize = 256; - //for i in 0..N { - // let uu = Halton::sample(0, i); - // let vv = Halton::sample(1, i); - // let mut samp = uniform_sample_cone(uu, vv, cos_theta_max); - // samp = zup_to_vec(samp, bb).normalized(); - // if dot(nn, samp) > 0.0 { - // let hh = (aa+samp).normalized(); - // fac += self.dist(dot(nn, hh), roughness); - // } - //} - //fac /= N * N; - - // Approximate method - let theta = cos_theta_max.acos(); - let hh = (aa + bb).normalized(); - let nh = clamp(dot(nn, hh), -1.0, 1.0); - let fac = Self::ggx_d( - nh, - (1.0f32).min(self.roughness.sqrt() + (2.0 * theta / PI_32)), - ); - - fac * (1.0f32).min(1.0 - cos_theta_max) * INV_PI + // TODO: what to do here? + unimplemented!() } } + +//============================================================================= + +/// Utility function that calculates the fresnel reflection factor of a given +/// incoming ray against a surface with the given normal-reflectance factor. +/// +/// `frensel_fac`: The ratio of light reflected back if the ray were to +/// hit the surface head-on (perpendicular to the surface). +/// `c`: The cosine of the angle between the incoming light and the +/// surface's normal. Probably calculated e.g. with a normalized +/// dot product. +#[allow(dead_code)] +fn dielectric_fresnel_from_fac(fresnel_fac: f32, c: f32) -> f32 { + let tmp1 = fresnel_fac.sqrt() - 1.0; + + // Protect against divide by zero. + if tmp1.abs() < 0.000_001 { + return 1.0; + } + + // Find the ior ratio + let tmp2 = (-2.0 / tmp1) - 1.0; + let ior_ratio = tmp2 * tmp2; + + // Calculate fresnel factor + dielectric_fresnel(ior_ratio, c) +} + +/// Schlick's approximation version of `dielectric_fresnel_from_fac()` above. +#[allow(dead_code)] +fn schlick_fresnel_from_fac(frensel_fac: f32, c: f32) -> f32 { + let c1 = 1.0 - c; + let c2 = c1 * c1; + frensel_fac + ((1.0 - frensel_fac) * c1 * c2 * c2) +} + +/// Utility function that calculates the fresnel reflection factor of a given +/// incoming ray against a surface with the given ior outside/inside ratio. +/// +/// `ior_ratio`: The ratio of the outside material ior (probably 1.0 for air) +/// over the inside ior. +/// `c`: The cosine of the angle between the incoming light and the +/// surface's normal. Probably calculated e.g. with a normalized +/// dot product. +#[allow(dead_code)] +fn dielectric_fresnel(ior_ratio: f32, c: f32) -> f32 { + let g = (ior_ratio - 1.0 + (c * c)).sqrt(); + + let f1 = g - c; + let f2 = g + c; + let f3 = (f1 * f1) / (f2 * f2); + + let f4 = (c * f2) - 1.0; + let f5 = (c * f1) + 1.0; + let f6 = 1.0 + ((f4 * f4) / (f5 * f5)); + + 0.5 * f3 * f6 +} + +/// Schlick's approximation of the fresnel reflection factor. +/// +/// Same interface as `dielectric_fresnel()`, above. +#[allow(dead_code)] +fn schlick_fresnel(ior_ratio: f32, c: f32) -> f32 { + let f1 = (1.0 - ior_ratio) / (1.0 + ior_ratio); + let f2 = f1 * f1; + let c1 = 1.0 - c; + let c2 = c1 * c1; + + f2 + ((1.0 - f2) * c1 * c2 * c2) +} diff --git a/src/surface/mod.rs b/src/surface/mod.rs index 5c746c6..0b3cfcd 100644 --- a/src/surface/mod.rs +++ b/src/surface/mod.rs @@ -9,7 +9,7 @@ use crate::{ boundable::Boundable, math::{Matrix4x4, Normal, Point, Vector}, ray::{AccelRay, Ray}, - shading::surface_closure::SurfaceClosureUnion, + shading::surface_closure::SurfaceClosure, shading::SurfaceShader, }; @@ -31,7 +31,7 @@ pub enum SurfaceIntersection { Occlude, Hit { intersection_data: SurfaceIntersectionData, - closure: SurfaceClosureUnion, + closure: SurfaceClosure, }, }