├── .gitignore ├── renders ├── dragon.png ├── bathroom.png ├── staircase.png └── testball.png ├── scenes └── material-testball │ ├── textures │ └── envmap.hdr │ ├── LICENSE.txt │ └── scene.json ├── src ├── math │ ├── ray.rs │ ├── mod.rs │ ├── frame.rs │ ├── aabb.rs │ ├── vec3.rs │ └── mat4.rs ├── geometry.rs ├── warp.rs ├── camera.rs ├── distribution.rs ├── integrator.rs ├── light.rs ├── scene.rs ├── mesh.rs ├── texture.rs ├── obj.rs ├── primitive.rs ├── bvh.rs ├── lib.rs ├── bin │ └── render_tungsten.rs └── material.rs ├── Cargo.toml ├── LICENSE ├── README.md └── examples └── cornell.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /renders/dragon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yblein/tracing/HEAD/renders/dragon.png -------------------------------------------------------------------------------- /renders/bathroom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yblein/tracing/HEAD/renders/bathroom.png -------------------------------------------------------------------------------- /renders/staircase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yblein/tracing/HEAD/renders/staircase.png -------------------------------------------------------------------------------- /renders/testball.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yblein/tracing/HEAD/renders/testball.png -------------------------------------------------------------------------------- /scenes/material-testball/textures/envmap.hdr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yblein/tracing/HEAD/scenes/material-testball/textures/envmap.hdr -------------------------------------------------------------------------------- /src/math/ray.rs: -------------------------------------------------------------------------------- 1 | use math::Vec3; 2 | 3 | #[derive(Copy, Clone, Debug)] 4 | pub struct Ray { 5 | pub origin: Vec3, 6 | pub direction: Vec3, 7 | } 8 | 9 | impl Ray { 10 | pub fn point_at(&self, t: f32) -> Vec3 { 11 | self.origin + self.direction * t 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/geometry.rs: -------------------------------------------------------------------------------- 1 | use math::*; 2 | 3 | #[derive(Copy, Clone)] 4 | pub struct Intersection { 5 | pub distance: f32, 6 | pub normal: Vec3, 7 | pub uv: (f32, f32), 8 | } 9 | 10 | pub trait Surface { 11 | fn intersect(&self, ray: Ray) -> Option; 12 | fn aabb(&self) -> AABB; 13 | } 14 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tracing" 3 | version = "0.2.0" 4 | authors = ["Yoann Blein "] 5 | 6 | [features] 7 | default = ["gui"] 8 | gui = ["sdl2"] 9 | 10 | [dependencies] 11 | rand = "^0.4" 12 | rayon = "^1" 13 | time = "^0" 14 | image = "^0" 15 | serde = "^1" 16 | serde_derive = "^1" 17 | bincode = "^1" 18 | 19 | sdl2 = { version = "^0", optional = true } 20 | 21 | serde_json = "^1" 22 | 23 | [profile.release] 24 | #lto = true 25 | #debug = true 26 | 27 | [profile.dev] 28 | opt-level = 2 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Yoann Blein 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | -------------------------------------------------------------------------------- /src/math/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod vec3; 2 | pub mod mat4; 3 | pub mod frame; 4 | pub mod ray; 5 | pub mod aabb; 6 | 7 | pub use vec3::Vec3; 8 | pub use mat4::Mat4; 9 | pub use frame::Frame; 10 | pub use ray::Ray; 11 | pub use aabb::AABB; 12 | pub use std::f32::{INFINITY, NEG_INFINITY}; 13 | pub use std::f32::consts::*; 14 | 15 | pub const EPSILON: f32 = 1e-5; 16 | pub const INV_PI: f32 = FRAC_1_PI; 17 | pub const INV_2_PI: f32 = 0.5 * FRAC_1_PI; 18 | pub const INV_4_PI: f32 = 0.25 * FRAC_1_PI; 19 | 20 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 21 | pub enum Axis { X, Y, Z } 22 | 23 | pub fn lerp(a: Vec3, b: Vec3, t: f32) -> Vec3 { 24 | (1.0 - t) * a + t * b 25 | } 26 | 27 | pub fn bilerp(x00: Vec3, x01: Vec3, x10: Vec3, x11: Vec3, (u, v): (f32, f32)) -> Vec3 { 28 | lerp(lerp(x00, x01, u), lerp(x10, x11, u), v) 29 | } 30 | -------------------------------------------------------------------------------- /src/math/frame.rs: -------------------------------------------------------------------------------- 1 | use math::Vec3; 2 | 3 | /// Right-handed orthonormal basis -- Y is up 4 | pub struct Frame(Vec3, Vec3, Vec3); 5 | 6 | impl Frame { 7 | pub fn from_up(normal: Vec3) -> Frame { 8 | let tangent = if normal.x.abs() > normal.y.abs() { 9 | Vec3::new(normal.z, 0.0, -normal.x) / (normal.x * normal.x + normal.z * normal.z).sqrt() 10 | } else { 11 | Vec3::new(0.0, -normal.z, normal.y) / (normal.y * normal.y + normal.z * normal.z).sqrt() 12 | }; 13 | let bitangent = Vec3::cross(normal, tangent); 14 | Frame(tangent, normal, bitangent) 15 | } 16 | 17 | #[inline(always)] 18 | pub fn to_world(&self, v: Vec3) -> Vec3 { 19 | self.0 * v.x + self.1 * v.y + self.2 * v.z 20 | } 21 | 22 | #[inline(always)] 23 | pub fn to_local(&self, v: Vec3) -> Vec3 { 24 | Vec3::new(Vec3::dot(v, self.0), Vec3::dot(v, self.1), Vec3::dot(v, self.2)) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /scenes/material-testball/LICENSE.txt: -------------------------------------------------------------------------------- 1 | This is the Tungsten version of 'Material Test Ball' by Benedikt Bitterli, downloaded from https://benedikt-bitterli.me/resources/ 2 | 3 | The original file may be obtained here: https://benedikt-bitterli.me/resources 4 | 5 | This scene was released under a CC0 license and is in the public domain. 6 | It may be copied, modified and used commercially, without permission or 7 | attribution. However, crediting the artist is encouraged. 8 | 9 | For more information about the license, please see 10 | https://creativecommons.org/publicdomain/zero/1.0/ 11 | 12 | 13 | This scene contains an environment map by Bernhard Vogl, available here: 14 | http://dativ.at/lightprobes/ 15 | The environment map is NOT covered by this CC0 license, and is instead 16 | licensed under attribution/non-commercial terms. If you wish to use the 17 | environment map, you must follow the terms of its license. 18 | -------------------------------------------------------------------------------- /src/warp.rs: -------------------------------------------------------------------------------- 1 | use math::*; 2 | 3 | /// Warp a sample from [0:1[² on the unit hemisphere around the Y-axis uniformly 4 | pub fn uniform_hemisphere((u, v): (f32, f32)) -> Vec3 { 5 | let r = (1.0 - u * u).sqrt(); 6 | let phi = 2.0 * PI * v; 7 | let x = r * phi.cos(); 8 | let z = r * phi.sin(); 9 | Vec3::new(x, u, z) 10 | } 11 | 12 | pub fn uniform_hemisphere_pdf(_: Vec3) -> f32 { 13 | INV_2_PI 14 | } 15 | 16 | /// Warp a sample from [0:1[² on the unit hemisphere around the Y-axis with a cosine-weight 17 | pub fn cosine_hemisphere((u, v): (f32, f32)) -> Vec3 { 18 | let (x, z) = uniform_disk((u, v)); 19 | let y = (1.0 - u).sqrt(); 20 | Vec3::new(x, y, z) 21 | } 22 | 23 | pub fn cosine_hemisphere_pdf(v: Vec3) -> f32 { 24 | v.y * INV_PI 25 | } 26 | 27 | /// Warp a sample from [0:1[² on the unit sphere 28 | pub fn uniform_sphere((u, v): (f32, f32)) -> Vec3 { 29 | let y = 1.0 - 2.0 * u; 30 | let r = (1.0 - y * y).sqrt(); 31 | let phi = 2.0 * PI * v; 32 | Vec3::new(r * phi.cos(), y, r * phi.sin()) 33 | } 34 | 35 | pub fn uniform_sphere_pdf(_: Vec3) -> f32 { 36 | INV_4_PI 37 | } 38 | 39 | /// Warp a sample from [0:1[² on the unit disk 40 | pub fn uniform_disk((u, v): (f32, f32)) -> (f32, f32) { 41 | let r = u.sqrt(); 42 | let theta = 2.0 * PI * v; 43 | (r * theta.cos(), r * theta.sin()) 44 | } 45 | 46 | /* 47 | pub fn beckmann(u: f32, v: f32, alpha: f32) -> Vec3 { 48 | let phi = u * 2.0 * PI; 49 | let p = v * phi / (2.0 * PI); 50 | let lg = (1.0-(2.0*PI*p/phi)).log10(); 51 | let denom = (1.0 - (alpha*alpha*lg)).sqrt(); 52 | let theta = (1.0/denom).acos(); 53 | 54 | let phi = 2.0 * PI * v; 55 | let r = theta.sin(); 56 | 57 | let x = r * phi.cos(); 58 | let y = theta.cos(); 59 | let z = r * phi.sin(); 60 | 61 | Vec3::new(x, y, z).normalized() 62 | } 63 | */ 64 | 65 | /// Warp a sample from [0;1[ to a tent over [-1;1[ 66 | pub fn tent1d(u: f32) -> f32 { 67 | let x = 2.0 * u; 68 | if x < 1.0 { 69 | x.sqrt() - 1.0 70 | } else { 71 | 1.0 - (2.0 - x).sqrt() 72 | } 73 | } 74 | 75 | /// Warp a sample from [0;1[² to a tent over [-1;1[² 76 | pub fn tent((u, v): (f32, f32)) -> (f32, f32) { 77 | (tent1d(u), tent1d(v)) 78 | } 79 | -------------------------------------------------------------------------------- /src/camera.rs: -------------------------------------------------------------------------------- 1 | use math::*; 2 | use warp; 3 | 4 | #[derive(Clone)] 5 | pub struct Camera { 6 | pos: Vec3, 7 | transform: Mat4, 8 | 9 | resolution: (usize, usize), 10 | ratio: f32, 11 | pixel_size: (f32, f32), 12 | 13 | fov_rad: f32, 14 | plane_dist: f32, 15 | 16 | aperture_radius: f32, 17 | focus_dist: f32, 18 | 19 | pub tonemap: Tonemap, 20 | } 21 | 22 | impl Camera { 23 | pub fn new(transform: &Mat4, resolution: (usize, usize), fov: f32, tonemap: Tonemap, aperture_radius: Option, focus_dist: Option) -> Camera { 24 | let fov_rad = fov * PI / 180.0; 25 | let plane_dist = 1.0 / (fov_rad * 0.5).tan(); 26 | 27 | Camera { 28 | pos: transform.transform_point(Vec3::zero()), 29 | transform: transform.clone(), 30 | resolution, 31 | ratio: resolution.1 as f32 / resolution.0 as f32, 32 | pixel_size: (1.0 / resolution.0 as f32, 1.0 / resolution.1 as f32), 33 | fov_rad, 34 | plane_dist, 35 | aperture_radius: aperture_radius.unwrap_or(0.0), 36 | focus_dist: focus_dist.unwrap_or(plane_dist), 37 | tonemap, 38 | } 39 | } 40 | 41 | pub fn make_ray(&self, pixel: (usize, usize), img_uv: (f32, f32), lens_uv: (f32, f32)) -> Ray { 42 | let pj = warp::tent(img_uv); 43 | let img_plane_pos = Vec3 { 44 | x: -1.0 + (pixel.0 as f32 + pj.0 + 0.5) * 2.0 * self.pixel_size.0, 45 | y: self.ratio - (pixel.1 as f32 + pj.1 + 0.5) * 2.0 * self.pixel_size.0, 46 | z: self.plane_dist, 47 | }; 48 | let focus_plane_pos = img_plane_pos * (self.focus_dist / img_plane_pos.z); 49 | let lj = warp::uniform_disk(lens_uv); 50 | let lens_pos = Vec3::new(lj.0 * self.aperture_radius, lj.1 * self.aperture_radius, 0.0); 51 | let local_dir = (focus_plane_pos - lens_pos).normalized(); 52 | 53 | Ray { 54 | origin: self.transform.transform_point(lens_pos), 55 | direction: self.transform.transform_vector(local_dir).normalized(), 56 | } 57 | } 58 | 59 | pub fn resolution(&self) -> (usize, usize) { 60 | self.resolution 61 | } 62 | 63 | pub fn set_focus_dist(&mut self, focus_dist: Option) { 64 | self.focus_dist = focus_dist.unwrap_or(self.plane_dist) 65 | } 66 | } 67 | 68 | pub type Tonemap = fn(Vec3) -> Vec3; 69 | 70 | pub fn gamma(c: Vec3) -> Vec3 { 71 | c.map(|v| v.min(1.0).powf(1.0 / 2.2)) 72 | } 73 | 74 | pub fn filmic(c: Vec3) -> Vec3 { 75 | c.map(|v| { 76 | let x = (v - 0.004).max(0.0); 77 | (x*(6.2*x + 0.5)) / (x*(6.2*x + 1.7) + 0.06) 78 | }) 79 | } 80 | -------------------------------------------------------------------------------- /src/math/aabb.rs: -------------------------------------------------------------------------------- 1 | use math::*; 2 | 3 | /// Axis-Aligned Bounding Box 4 | #[derive(Copy, Clone, Debug, Serialize, Deserialize)] 5 | pub struct AABB { 6 | pub min: Vec3, 7 | pub max: Vec3, 8 | } 9 | 10 | impl AABB { 11 | #[inline(always)] 12 | pub fn intersect_fast(&self, ray: Ray, inv_dir: Vec3) -> (f32, f32) { 13 | let t_min = (self.min - ray.origin) * inv_dir; 14 | let t_max = (self.max - ray.origin) * inv_dir; 15 | let t1 = Vec3::min(t_min, t_max); 16 | let t2 = Vec3::max(t_min, t_max); 17 | let t_near = t1.x.max(t1.y).max(t1.z); 18 | let t_far = t2.x.min(t2.y).min(t2.z); 19 | if t_near > t_far { 20 | return (-1.0, -1.0); 21 | } 22 | (t_near, t_far) 23 | //if t_near < 0.0 { t_far } else { t_near } 24 | } 25 | 26 | pub fn intersect(&self, ray: Ray) -> (f32, f32) { 27 | let inv_dir = 1.0 / ray.direction; 28 | self.intersect_fast(ray, inv_dir) 29 | } 30 | 31 | pub fn zero() -> AABB { 32 | AABB { min: Vec3::zero(), max: Vec3::zero() } 33 | } 34 | 35 | pub fn empty() -> AABB { 36 | AABB { min: Vec3::thrice(INFINITY), max: Vec3::thrice(NEG_INFINITY) } 37 | } 38 | 39 | pub fn from_point(p: Vec3) -> AABB { 40 | AABB { min: p, max: p } 41 | } 42 | 43 | pub fn extend_point(&mut self, p: Vec3) { 44 | self.min = Vec3::min(self.min, p); 45 | self.max = Vec3::max(self.max, p); 46 | } 47 | 48 | pub fn union(&self, b: &AABB) -> AABB { 49 | AABB { 50 | min: Vec3::min(self.min, b.min), 51 | max: Vec3::max(self.max, b.max), 52 | } 53 | } 54 | 55 | pub fn longuest_axis(&self) -> Axis { 56 | let dx = self.max.x - self.min.x; 57 | let dy = self.max.y - self.min.y; 58 | let dz = self.max.z - self.min.z; 59 | 60 | if dx > dy { 61 | if dx > dz { Axis::X } else { Axis::Z } 62 | } else { 63 | if dy > dz { Axis::Y } else { Axis::Z } 64 | } 65 | } 66 | 67 | pub fn center(&self) -> Vec3 { 68 | (self.min + self.max) * 0.5 69 | } 70 | 71 | /* 72 | fn normal_at(&self, p: Vec3) -> Vec3 { 73 | if p.x < self.min.x + EPSILON { 74 | Vec3::new(-1.0, 0.0, 0.0) 75 | } else if p.x > self.max.x - EPSILON { 76 | Vec3::new(1.0, 0.0, 0.0) 77 | } else if p.y < self.min.y + EPSILON { 78 | Vec3::new(0.0, -1.0, 0.0) 79 | } else if p.y > self.max.y - EPSILON { 80 | Vec3::new(0.0, 1.0, 0.0) 81 | } else if p.z < self.min.z + EPSILON { 82 | Vec3::new(0.0, 0.0, -1.0) 83 | } else { 84 | Vec3::new(0.0, 0.0, 1.0) 85 | } 86 | } 87 | */ 88 | 89 | pub fn surface_area(&self) -> f32 { 90 | let d = self.max - self.min; 91 | 2.0 * (d.x * d.y + d.x * d.z + d.y * d.z) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A simple physically-based path tracer 2 | 3 | This is a physically-based path tracer I wrote from scratch in Rust to better understand how modern renderers work. 4 | Below are some renders it produced. 5 | 6 | ![testball](renders/bathroom.png) 7 | 8 | ![testball](renders/staircase.png) 9 | 10 | ![testball](renders/dragon.png) 11 | 12 | ![testball](renders/testball.png) 13 | 14 | Rendering actually touches many domains (geometry, image reconstruction, spatial data structures, optics, …) and learning all of them was fun, challenging and rewarding. 15 | The codebase is much simpler than feature-rich reference implementations such as [pbrt](https://www.pbrt.org/) or [Mitsuba](http://mitsuba-renderer.org/), but poorly documented. 16 | 17 | ## Features 18 | 19 | - Full physically based global illumination using unbiased unidirectional pathtracing with multiple importance sampling (BSDF and direct light sampling) 20 | - Support meshes composed millions of triangles (the dragon above has more than 800k triangles) loaded from OBJ files and some geometric primitives (sphere, parallelogram, …) 21 | - Area lights (parallelogram and sphere shaped) and Image Based Lighting (see the dragon above) 22 | - Several physically based materials: 23 | - Smooth dielectric (glass), conductor (metal) and plastic with accurate Fresnel effects 24 | - Rough dielectric, conductor and plastic based on the GGX microfacet model described in [Microfacet Models for Refraction through Rough Surfaces](https://diglib.eg.org/handle/10.2312/EGWR.EGSR07.195-206) 25 | - Clearcoat model as described in [Arbitrarily layered micro-facet surfaces](https://dl.acm.org/citation.cfm?doid=1321261.1321292) which produces nice metallic paint effects (see the test ball above) 26 | - Bitmap textures (loaded from common image formats) that can be used to describe material albedo but also roughness (when applicable) 27 | - Two-levels acceleration structure system based on Bounded Volume Hierarchies constructed with the Surface Area Heuristic 28 | - Depth of Field effect 29 | - Fully parallelized rendering 30 | - Progressive rendering with live preview in a SDL window (can be disabled) 31 | 32 | ## Usage 33 | 34 | As this project is not meant for real-use, the interface is not very polished. 35 | There are two ways to describe a scene to render. 36 | 37 | - Describe a scene in Rust with the provided library. 38 | An example is given in `examples/cornell.rs` and can be executed with: 39 | 40 | ``` 41 | cargo run --release --example=cornell 42 | ``` 43 | 44 | - Use the Tungsten file format, although only a *very* limited subset is supported. 45 | An example of scene can be found in `scenes/material-testball` and rendered with: 46 | 47 | ``` 48 | cargo run --release --bin=render_tungsten -- scenes/material-testball/scene.json 49 | ``` 50 | 51 | The current render is saved in `/tmp/image.ppm` when exiting the preview. 52 | 53 | ## TODO 54 | 55 | - Bump mapping 56 | - Adaptive sampling 57 | 58 | ## Credits 59 | 60 | The bathroom, staircase and material test ball scenes are [courtesy of Benedikt Bitterli](https://benedikt-bitterli.me/resources). 61 | 62 | -------------------------------------------------------------------------------- /src/distribution.rs: -------------------------------------------------------------------------------- 1 | use rayon::prelude::*; 2 | 3 | /// Make an importance samplable 1D distribution 4 | /// 5 | /// Will normalize both the `pdf` and the computed `cdf`. 6 | /// Returns the sum of the given PDF. 7 | fn make1d(pdf: &mut [f32], cdf: &mut [f32]) -> f32 { 8 | cdf[0] = pdf[0]; 9 | for i in 1..cdf.len() { 10 | cdf[i] = cdf[i - 1] + pdf[i]; 11 | } 12 | 13 | let total = *cdf.last().unwrap(); 14 | debug_assert!(total.is_finite(), "distribution has non-finite values"); 15 | 16 | if total <= 0.0 { 17 | println!("Warning: the distribution is null and should not be sampled"); 18 | return 0.0; 19 | } 20 | 21 | for v in pdf.iter_mut() { 22 | *v /= total; 23 | } 24 | for v in cdf.iter_mut() { 25 | *v /= total; 26 | } 27 | 28 | return total; 29 | } 30 | 31 | /// Importance sample a 1D distribution 32 | /// 33 | /// Returns the sampled index and its PDF. 34 | fn sample1d(pdf: &[f32], cdf: &[f32], u: f32) -> (usize, f32) { 35 | let i = match cdf.binary_search_by(|v| v.partial_cmp(&u).unwrap()) { 36 | Ok(v) => v, 37 | Err(v) => v, 38 | }; 39 | (i, pdf[i]) 40 | } 41 | 42 | pub struct Distribution1D { 43 | pdf: Vec, 44 | cdf: Vec, 45 | } 46 | 47 | impl Distribution1D { 48 | pub fn new(weights: Vec) -> Distribution1D { 49 | let mut pdf = weights; 50 | let mut cdf = vec![0.0; pdf.len()]; 51 | let _ = make1d(&mut pdf[..], &mut cdf[..]); 52 | Distribution1D { pdf, cdf } 53 | } 54 | 55 | pub fn sample(&self, u: f32) -> (usize, f32) { 56 | sample1d(&self.pdf[..], &self.cdf[..], u) 57 | } 58 | } 59 | 60 | pub struct Distribution2D { 61 | width: usize, 62 | height: usize, 63 | conditional_pdf: Vec, 64 | conditional_cdf: Vec, 65 | marginal_pdf: Vec, 66 | marginal_cdf: Vec, 67 | } 68 | 69 | impl Distribution2D { 70 | pub fn new(weights: Vec, width: usize, height: usize) -> Distribution2D { 71 | debug_assert_eq!(weights.len(), width * height); 72 | 73 | let mut conditional_cdf = vec![0.0; weights.len()]; 74 | let mut conditional_pdf = weights; 75 | let mut marginal_cdf = vec![0.0; height]; 76 | let mut marginal_pdf = { 77 | let chunks_pdf = conditional_pdf.par_chunks_mut(width); 78 | let chunks_cdf = conditional_cdf.par_chunks_mut(width); 79 | chunks_pdf.zip(chunks_cdf).map(|(pdf, cdf)| make1d(pdf, cdf)).collect::>() 80 | }; 81 | let _ = make1d(&mut marginal_pdf[..], &mut marginal_cdf[..]); 82 | 83 | Distribution2D { width, height, conditional_pdf, conditional_cdf, marginal_pdf, marginal_cdf } 84 | } 85 | 86 | pub fn sample(&self, u: f32, v: f32) -> ((usize, usize), f32) { 87 | // first sample the marginal distribution to find a row 88 | let (s1, p1) = sample1d(&self.marginal_pdf[..], &self.marginal_cdf[..], v); 89 | // then sample the distribution of the chosen row `s1` 90 | let i0 = self.width * s1; 91 | let i1 = i0 + self.width; 92 | let (s0, p0) = sample1d(&self.conditional_pdf[i0..i1], &self.conditional_cdf[i0..i1], u); 93 | ((s0, s1), p0 * p1) 94 | } 95 | 96 | pub fn pdf(&self, x: usize, y: usize) -> f32 { 97 | let x = x.min(self.width - 1); 98 | let y = y.min(self.height - 1); 99 | self.conditional_pdf[y * self.width + x] * self.marginal_pdf[y] 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/integrator.rs: -------------------------------------------------------------------------------- 1 | use math::*; 2 | use geometry::*; 3 | use scene::*; 4 | use texture::*; 5 | use material::*; 6 | use warp::*; 7 | use light::*; 8 | 9 | use rand::Rng; 10 | 11 | pub fn estimate_radiance(scene: &Scene, ray: Ray, rng: &mut R) -> Vec3 { 12 | let nb_lights = scene.nb_lights(); 13 | let light_pick_prob = 1.0 / nb_lights as f32; 14 | 15 | let mut path_weight = Vec3::thrice(1.0); 16 | let mut radiance = Vec3::zero(); 17 | let mut ray = ray; 18 | let mut specular_bounce = true; 19 | let mut last_pdf_dir = 1.0; 20 | 21 | for nb_bounces in 0.. { 22 | let (intersection, material) = match scene.intersect(ray) { 23 | Some(Hit::Scatterer(its, mat)) => (its, mat), 24 | Some(Hit::Emitter(light, dist)) => { 25 | let contrib = light.eval_direct(ray.direction); 26 | let mis_weight = if !specular_bounce { 27 | let direct_pdf = light.pdf_direct(ray.direction, dist); 28 | mis2(last_pdf_dir, direct_pdf * light_pick_prob) 29 | } else { 30 | 1.0 31 | }; 32 | radiance += path_weight * mis_weight * contrib; 33 | 34 | // lights do not reflect; stop here 35 | break; 36 | } 37 | None => break, 38 | }; 39 | 40 | // compute some geometry at intersection 41 | let normal = intersection.normal; 42 | let hit = ray.point_at(intersection.distance); 43 | let shading_frame = Frame::from_up(normal); 44 | let local_in = shading_frame.to_local(ray.direction); 45 | 46 | let cont_prob = path_weight.max_elem().min(1.0); 47 | 48 | if !material.is_purely_specular() && nb_lights > 0 { 49 | // direct light sampling (also known as "next event estimation") 50 | let light_idx = (nb_lights as f32 * rng.gen::()) as usize; 51 | if let Some(ref light) = scene.get_light(light_idx) { 52 | let (emission, light_sample) = light.sample_direct(hit, rng.gen()); 53 | if !scene.occluded(hit, normal, light_sample.dir, light_sample.dist) { 54 | let local_out = shading_frame.to_local(light_sample.dir); 55 | let bsdf_eval = material.eval(local_in, local_out, intersection.uv); 56 | let bsdf_pdf = material.pdf(local_in, local_out, intersection.uv); 57 | let mis_weight = mis2(light_sample.pdf * light_pick_prob, bsdf_pdf * cont_prob); 58 | radiance += path_weight * emission * bsdf_eval * (mis_weight / (light_sample.pdf * light_pick_prob)); 59 | } 60 | } 61 | } 62 | 63 | let bsdf_sample = material.sample(local_in, intersection.uv, Vec3::new(rng.gen(), rng.gen(), rng.gen())); 64 | 65 | if bsdf_sample.weight == Vec3::zero() { 66 | break; 67 | } 68 | 69 | last_pdf_dir = bsdf_sample.pdf; 70 | 71 | // possibly terminate path 72 | //if nb_bounces >= 3 { 73 | if nb_bounces > 64 { 74 | // we are probably stuck inside a non transmitive object 75 | //eprintln!("Infinite bouncing detected (cont_prob {:?}, path_weight {:?}, bsdf_sample {:?})", cont_prob, path_weight, (bsdf_sample.weight, bsdf_sample.pdf)); 76 | break; 77 | } 78 | // russian roulette 79 | last_pdf_dir *= cont_prob; 80 | if cont_prob < 1.0 { 81 | if rng.gen::() >= cont_prob { 82 | break; 83 | } 84 | path_weight = path_weight / cont_prob; 85 | } 86 | //} 87 | 88 | path_weight *= bsdf_sample.weight; 89 | specular_bounce = bsdf_sample.is_specular; 90 | 91 | // nudge ray origin to avoid self-intersection 92 | ray.direction = shading_frame.to_world(bsdf_sample.direction).normalized(); 93 | let eps = if Vec3::dot(normal, ray.direction) >= 0.0 { EPSILON } else { -EPSILON }; 94 | ray.origin = hit + normal * eps * 2.0; 95 | } 96 | 97 | radiance 98 | } 99 | 100 | fn mis2(sample_pdf: f32, other_pdf: f32) -> f32 { 101 | let power = |x| x*x; 102 | power(sample_pdf) / (power(sample_pdf) + power(other_pdf)) 103 | } 104 | -------------------------------------------------------------------------------- /src/light.rs: -------------------------------------------------------------------------------- 1 | use distribution::Distribution2D; 2 | use math::*; 3 | use texture::*; 4 | use geometry::Surface; 5 | 6 | pub struct DirectSample { 7 | pub dir: Vec3, 8 | pub dist: f32, 9 | pub pdf: f32, 10 | } 11 | 12 | pub trait SampleDirectSurface: Surface { 13 | fn sample_direct(&self, p: Vec3, uv: (f32, f32)) -> DirectSample; 14 | fn pdf_direct(&self, dir: Vec3, dist: f32) -> f32; 15 | } 16 | 17 | pub trait Light { 18 | fn eval_direct(&self, dir: Vec3) -> Vec3; 19 | 20 | fn sample_direct(&self, p: Vec3, uv: (f32, f32)) -> (Vec3, DirectSample); 21 | fn pdf_direct(&self, dir: Vec3, dist: f32) -> f32; 22 | } 23 | 24 | pub struct EnvMap { 25 | img: Image, 26 | dist: Distribution2D, 27 | transform: Mat4, 28 | inv_transform: Mat4, 29 | } 30 | 31 | impl EnvMap { 32 | pub fn from_image(img: Image, transform: &Mat4) -> EnvMap { 33 | // Construct a row-major 2d distribution based on texels luminance. 34 | // It will be used to importance sample texels according to their contribution. 35 | // The sin(theta) factor counteracts the deformation of equi-rectangular mapping. 36 | let mut weights = vec![0f32; img.width * img.height]; 37 | for y in 0..img.height { 38 | // Compute sin(theta) for the current row, accounting for the half-pixel offset 39 | // due to conversion from discrete to continuous coordinates 40 | let sin_theta = ((y as f32 + 0.5) * PI / img.height as f32).sin(); 41 | for x in 0..img.width { 42 | weights[y * img.width + x] = luminance(img.get(x, y)) * sin_theta; 43 | } 44 | } 45 | let dist = Distribution2D::new(weights, img.width, img.height); 46 | 47 | // TODO: only keep rotation from transform 48 | EnvMap { 49 | img, 50 | dist, 51 | transform: transform.clone(), 52 | inv_transform: transform.inverse(), 53 | } 54 | } 55 | 56 | fn direction_to_uv(&self, d: Vec3) -> ((f32, f32), f32) { 57 | let l = self.inv_transform.transform_vector(d); 58 | let u = l.z.atan2(l.x) * INV_2_PI + 0.5; 59 | let v = (-l.y).acos() * INV_PI; 60 | let sin_theta = (1.0 - l.y * l.y).max(EPSILON).sqrt(); 61 | ((u, v), sin_theta) 62 | } 63 | 64 | fn uv_to_direction(&self, (u, v): (f32, f32)) -> (Vec3, f32) { 65 | let phi = (u - 0.5) * 2.0 * PI; 66 | let theta = v * PI; 67 | let (sin_theta, cos_theta) = theta.sin_cos(); 68 | let (sin_phi, cos_phi) = phi.sin_cos(); 69 | let local_dir = Vec3::new(sin_theta * cos_phi, -cos_theta, sin_theta * sin_phi); 70 | let dir = self.transform.transform_vector(local_dir); 71 | (dir, sin_theta) 72 | } 73 | } 74 | 75 | impl Light for EnvMap { 76 | fn eval_direct(&self, dir: Vec3) -> Vec3 { 77 | let (uv, _) = self.direction_to_uv(dir); 78 | self.img.eval(uv) 79 | } 80 | 81 | fn sample_direct(&self, _p: Vec3, (u, v): (f32, f32)) -> (Vec3, DirectSample) { 82 | let ((x, y), tex_pdf) = self.dist.sample(u, v); 83 | 84 | let u = (x as f32 + 0.5) / self.img.width as f32; 85 | let v = 1.0 - (y as f32 + 0.5) / self.img.height as f32; 86 | let (dir, sin_theta) = self.uv_to_direction((u, v)); 87 | 88 | let img_size = (self.img.width * self.img.height) as f32; 89 | let dir_pdf = tex_pdf * img_size / (2.0 * PI * PI * sin_theta); 90 | let emission = self.img.eval((u, v)); 91 | 92 | (emission, DirectSample { dir, dist: INFINITY, pdf: dir_pdf }) 93 | } 94 | 95 | fn pdf_direct(&self, dir: Vec3, _dist: f32) -> f32 { 96 | let ((u, v), sin_theta) = self.direction_to_uv(dir); 97 | let x = (u * self.img.width as f32) as usize; 98 | let y = ((1.0 - v) * self.img.height as f32) as usize; 99 | 100 | let img_size = (self.img.width * self.img.height) as f32; 101 | let tex_pdf = self.dist.pdf(x, y); 102 | 103 | tex_pdf * img_size / (2.0 * PI * PI * sin_theta) 104 | } 105 | } 106 | 107 | pub struct AreaLight { 108 | pub surface: Box, 109 | pub emission: Vec3, 110 | } 111 | 112 | impl Light for AreaLight { 113 | fn eval_direct(&self, _dir: Vec3) -> Vec3 { 114 | self.emission 115 | } 116 | 117 | fn sample_direct(&self, p: Vec3, uv: (f32, f32)) -> (Vec3, DirectSample) { 118 | (self.emission, self.surface.sample_direct(p, uv)) 119 | } 120 | 121 | fn pdf_direct(&self, dir: Vec3, dist: f32) -> f32 { 122 | self.surface.pdf_direct(dir, dist) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/scene.rs: -------------------------------------------------------------------------------- 1 | use geometry::*; 2 | use material::*; 3 | use light::*; 4 | use math::*; 5 | use bvh::BVH; 6 | use std::sync::Arc; 7 | 8 | pub enum Object { 9 | Emitter(AreaLight), 10 | Scatterer { 11 | surface: Box, 12 | material: Arc 13 | }, 14 | } 15 | 16 | impl Object { 17 | fn is_emitter(&self) -> bool { 18 | match self { 19 | Object::Emitter(_) => true, 20 | _ => false, 21 | } 22 | } 23 | } 24 | 25 | impl Surface for Object { 26 | fn intersect(&self, ray: Ray) -> Option { 27 | match *self { 28 | Object::Emitter(ref area_light) => area_light.surface.intersect(ray), 29 | Object::Scatterer { ref surface, .. } => surface.intersect(ray), 30 | } 31 | } 32 | 33 | fn aabb(&self) -> AABB { 34 | match *self { 35 | Object::Emitter(ref area_light) => area_light.surface.aabb(), 36 | Object::Scatterer { ref surface, .. } => surface.aabb(), 37 | } 38 | } 39 | } 40 | 41 | pub(crate) enum Hit<'a> { 42 | Emitter(&'a Light, f32), 43 | Scatterer(Intersection, &'a Material), 44 | } 45 | 46 | pub struct Scene { 47 | objects: Vec, 48 | background: Option, 49 | light_idxs: Vec, 50 | bvh: BVH, 51 | } 52 | 53 | impl Scene { 54 | pub fn new(background: Option, objects: Vec) -> Scene { 55 | let mut objects = objects; 56 | 57 | let proj_centroid = |o: &Object, axis| o.aabb().center()[axis]; 58 | let obj_bbox = |o: &Object| o.aabb(); 59 | let bvh = BVH::build(&proj_centroid, &obj_bbox, &mut objects[..]); 60 | 61 | let light_idxs: Vec = objects.iter() 62 | .enumerate() 63 | .filter(|(_, o)| o.is_emitter()) 64 | .map(|(i, _)| i) 65 | .collect(); 66 | 67 | Scene { objects, background, bvh, light_idxs } 68 | } 69 | 70 | pub(crate) fn intersect(&self, ray: Ray) -> Option { 71 | //return self.intersect_objects(ray, 0, self.objects.len()); 72 | let intersect_item = |ray, i| { 73 | let o: &Object = &self.objects[i]; 74 | match o.intersect(ray) { 75 | None => (-1.0, Default::default()), 76 | Some(its) => (its.distance, (its.normal, its.uv)), 77 | } 78 | }; 79 | let (t, i, (n, uv)) = self.bvh.intersect(&intersect_item, ray); 80 | 81 | if t > 0.0 { 82 | Some(match self.objects[i] { 83 | Object::Scatterer { ref material, .. } => { 84 | Hit::Scatterer(Intersection { distance: t, normal: n, uv }, material.as_ref()) 85 | } 86 | Object::Emitter(ref area_light) => { 87 | Hit::Emitter(area_light, t) 88 | } 89 | }) 90 | } else { 91 | self.background.as_ref().map(|envmap| Hit::Emitter(envmap, INFINITY)) 92 | } 93 | } 94 | 95 | pub(crate) fn nb_lights(&self) -> usize { 96 | self.light_idxs.len() + self.background.is_some() as usize 97 | } 98 | 99 | pub(crate) fn get_light(&self, i: usize) -> Option<&Light> { 100 | match self.light_idxs.get(i).and_then(|&i_obj| self.objects.get(i_obj)) { 101 | Some(Object::Emitter(light)) => Some(light), 102 | _ => match self.background { 103 | Some(ref envmap) => Some(envmap), 104 | None => None, 105 | }, 106 | } 107 | } 108 | 109 | /* 110 | fn intersect_objects(&self, ray: Ray, begin: usize, end: usize) -> Option<(Intersection, &Object)> { 111 | let mut closest = None; 112 | 113 | for object in &self.objects[begin..end] { 114 | if let Some(intersection) = object.surface.intersect(ray) { 115 | match closest { 116 | None => closest = Some((intersection, object)), 117 | Some((Intersection { distance: t_min, .. }, _)) => { 118 | if 0.0 < intersection.distance && intersection.distance < t_min { 119 | closest = Some((intersection, object)); 120 | } 121 | }, 122 | } 123 | } 124 | } 125 | 126 | return closest; 127 | } 128 | */ 129 | 130 | pub(crate) fn occluded(&self, point: Vec3, normal: Vec3, dir: Vec3, max_dist: f32) -> bool { 131 | if Vec3::dot(dir, normal) <= 0.0 { 132 | return true; 133 | } 134 | 135 | let intersect_item = |ray, i| { 136 | let o: &Object = &self.objects[i]; 137 | match o.intersect(ray) { 138 | None => (-1.0, ()), 139 | Some(its) => (its.distance, ()), 140 | } 141 | }; 142 | 143 | let shadow_ray = Ray { origin: point + normal * EPSILON, direction: dir }; 144 | let (t, _, ()) = self.bvh.intersect(&intersect_item, shadow_ray); 145 | 146 | t > 0.0 && t < max_dist - 2.0 * EPSILON 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/mesh.rs: -------------------------------------------------------------------------------- 1 | use time::PreciseTime; 2 | 3 | use geometry::*; 4 | use math::*; 5 | use bvh::BVH; 6 | 7 | /// Represent vertex indices in triangles; 2^32 vertices should be enough 8 | pub type Index = u32; 9 | 10 | #[derive(Serialize, Deserialize)] 11 | pub struct Triangle { 12 | pub idxs: [Index; 3], 13 | } 14 | 15 | // N.B. There is a 1-1 correspondence between vertices and normals. 16 | // Those are addressed through the indices stored in triangles. 17 | #[derive(Serialize, Deserialize)] 18 | pub struct Mesh { 19 | vertices: Vec, 20 | normals: Vec, 21 | uvs: Vec<(f32, f32)>, 22 | triangles: Vec, 23 | triangles_e1: Vec, 24 | triangles_e2: Vec, 25 | bvh: BVH, 26 | } 27 | 28 | impl Mesh { 29 | pub fn new(vertices: Vec, normals: Vec, uvs: Vec<(f32, f32)>, triangles: Vec) -> Mesh { 30 | if normals.is_empty() { 31 | panic!("Missing normals"); 32 | } 33 | 34 | let mut triangles = triangles; 35 | 36 | let bvh = { 37 | let third = 1.0 / 3.0; 38 | let proj_centroid = |t: &Triangle, axis| -> f32 { 39 | let v0 = vertices[t.idxs[0] as usize]; 40 | let v1 = vertices[t.idxs[1] as usize]; 41 | let v2 = vertices[t.idxs[2] as usize]; 42 | (v0[axis] + v1[axis] + v2[axis]) * third 43 | }; 44 | 45 | let tri_bbox = |t: &Triangle| -> AABB { 46 | let b0 = AABB::from_point(vertices[t.idxs[0] as usize]); 47 | let b1 = AABB::from_point(vertices[t.idxs[1] as usize]); 48 | let b2 = AABB::from_point(vertices[t.idxs[2] as usize]); 49 | b0.union(&b1).union(&b2) 50 | }; 51 | 52 | // build the BVH 53 | println!("Building BVH for {} triangles...", triangles.len()); 54 | let start = PreciseTime::now(); 55 | let bvh = BVH::build(&proj_centroid, &tri_bbox, &mut triangles[..]); 56 | let end = PreciseTime::now(); 57 | println!("BVH built in {} seconds", start.to(end).num_milliseconds() as f32 / 1000.0); 58 | bvh 59 | }; 60 | 61 | let (triangles_e1, triangles_e2) = Mesh::compute_edges(&vertices[..], &triangles[..]); 62 | 63 | Mesh { 64 | vertices: vertices, 65 | normals: normals, 66 | uvs: uvs, 67 | triangles: triangles, 68 | triangles_e1: triangles_e1, 69 | triangles_e2: triangles_e2, 70 | bvh: bvh, 71 | } 72 | } 73 | 74 | fn compute_edges(vertices: &[Vec3], triangles: &[Triangle]) -> (Vec, Vec) { 75 | // cache triangle edges 76 | let mut triangles_e1 = Vec::with_capacity(triangles.len()); 77 | let mut triangles_e2 = Vec::with_capacity(triangles.len()); 78 | for t in triangles { 79 | triangles_e1.push(vertices[t.idxs[1] as usize] - vertices[t.idxs[0] as usize]); 80 | triangles_e2.push(vertices[t.idxs[2] as usize] - vertices[t.idxs[0] as usize]); 81 | } 82 | (triangles_e1, triangles_e2) 83 | } 84 | 85 | fn intersect_triangle(&self, ray: Ray, i: usize) -> (f32, (f32, f32)) { 86 | let v0 = self.vertices[self.triangles[i].idxs[0] as usize]; 87 | let edge1 = self.triangles_e1[i]; 88 | let edge2 = self.triangles_e2[i]; 89 | 90 | let p = Vec3::cross(ray.direction, edge2); 91 | let idet = 1.0 / Vec3::dot(edge1, p); 92 | 93 | let t = ray.origin - v0; 94 | let u = Vec3::dot(t, p) * idet; 95 | if u < 0.0 || u > 1.0 { 96 | return (-1.0, (0.0, 0.0)); 97 | } 98 | 99 | let q = Vec3::cross(t, edge1); 100 | let v = Vec3::dot(ray.direction, q) * idet; 101 | if v < 0.0 || (u + v) > 1.0 { 102 | return (-1.0, (0.0, 0.0)); 103 | } 104 | 105 | return (Vec3::dot(edge2, q) * idet, (u, v)); 106 | } 107 | 108 | } 109 | 110 | impl Surface for Mesh { 111 | fn intersect(&self, ray: Ray) -> Option { 112 | let intersect_item = |ray, i| self.intersect_triangle(ray, i); 113 | let (t, i, (u, v)) = self.bvh.intersect(&intersect_item, ray); 114 | 115 | if 0.0 < t { 116 | let idxs = self.triangles[i].idxs; 117 | let n0 = self.normals[idxs[0] as usize]; 118 | let n1 = self.normals[idxs[1] as usize]; 119 | let n2 = self.normals[idxs[2] as usize]; 120 | let n = (n0 * (1.0 - u - v) + n1 * u + n2 * v).normalized(); 121 | //let n = Vec3::cross(self.triangles_e1[i_min], self.triangles_e2[i_min]).normalized(); 122 | 123 | let uv0 = self.uvs[idxs[0] as usize]; 124 | let uv1 = self.uvs[idxs[1] as usize]; 125 | let uv2 = self.uvs[idxs[2] as usize]; 126 | 127 | let tu = uv0.0 * (1.0 - u - v) + uv1.0 * u + uv2.0 * v; 128 | let tv = uv0.1 * (1.0 - u - v) + uv1.1 * u + uv2.1 * v; 129 | 130 | Some(Intersection { 131 | distance: t, 132 | normal: n, 133 | uv: (tu, tv), 134 | }) 135 | } else { 136 | None 137 | } 138 | } 139 | 140 | fn aabb(&self) -> AABB { 141 | self.bvh.bbox() 142 | } 143 | } 144 | 145 | -------------------------------------------------------------------------------- /examples/cornell.rs: -------------------------------------------------------------------------------- 1 | extern crate tracing; 2 | 3 | use std::sync::Arc; 4 | 5 | use tracing::math::*; 6 | use tracing::primitive::*; 7 | use tracing::material::*; 8 | use tracing::scene::*; 9 | use tracing::camera::*; 10 | use tracing::*; 11 | use tracing::texture::*; 12 | use tracing::light::*; 13 | 14 | const HEIGHT: usize = 512; 15 | const WIDTH: usize = HEIGHT; 16 | 17 | fn main() { 18 | let white = Vec3::new(0.740063,0.742313,0.733934); 19 | let green = Vec3::new(0.162928,0.408903,0.0833759); 20 | let red = Vec3::new(0.366046,0.0371827,0.0416385); 21 | //let light_color = Vec3::new(0.780131,0.780409,0.775833); 22 | let scene = Scene::new( 23 | None, 24 | vec![ 25 | /* 26 | Object::Emitter(AreaLight { // light 27 | surface: Box::new(Sphere { 28 | position: Vec3 { x: 0.0, y: 0.7, z: 0.0 }, 29 | radius: 0.15, 30 | }), 31 | emission: Vec3::thrice(1.0) * 20.0, 32 | }), 33 | */ 34 | Object::Emitter(AreaLight { // light 35 | surface: Box::new(Parallelogram::from_square( 36 | Vec3 { x: 0.0, y: 1.0 - EPSILON, z: 0.0 }, 37 | Vec3 { x: 0.0, y: -1.0, z: 0.0 }, 38 | 0.5 39 | )), 40 | emission: Vec3::new(1.0, 0.772549, 0.560784) * 40.0, 41 | }), 42 | /* 43 | Object::Emitter(AreaLight { // light 44 | surface: Box::new(Disk::new( 45 | Vec3 { x: 0.0, y: 1.0 - EPSILON, z: 0.0 }, 46 | Vec3 { x: 0.0, y: -1.0, z: 0.0 }, 47 | 0.25 48 | )), 49 | emission: Vec3::thrice(1.0) * 20.0, 50 | }), 51 | */ 52 | /* 53 | Object::Emitter(AreaLight { // light 54 | surface: Box::new(Sphere::new(0.10, Vec3 { x: -3.0, y: 3.0, z: -3.0 })), 55 | emission: Vec3::new(1.0, 0.772549, 0.560784) * 4000.0, 56 | }), 57 | */ 58 | Object::Scatterer { 59 | surface: Box::new(Sphere::new( 60 | 0.35, 61 | Vec3 { x: -0.5, y: -0.65, z: -0.3 }, 62 | )), 63 | material: Arc::new(Diffuse { 64 | albedo: Texture::Constant(Vec3::thrice(0.99)), 65 | }), 66 | }, 67 | Object::Scatterer { 68 | surface: Box::new(Sphere::new( 69 | 0.35, 70 | Vec3 { x: 0.5, y: -0.65, z: 0.3 }, 71 | )), 72 | material: Arc::new(Diffuse { 73 | albedo: Texture::Constant(Vec3::thrice(0.99)), 74 | }), 75 | }, 76 | /* 77 | Object::Scatterer { 78 | surface: Box::new(Mesh::load_obj("data/bunny.obj", Vec3::new(0.4, 0.4, -0.4), 0.0, Vec3::new(-0.0, -0.88, 0.0))), 79 | //surface: Box::new(Mesh::load_obj("/tmp/dragon.obj", Vec3::thrice(1.0), 0.0, Vec3::new(0.0, 0.0, 0.0))), 80 | //surface: Box::new(Mesh::load_obj("data/teapot.obj", Vec3::thrice(0.8), 0.0, Vec3::new(0.0, -1.0, 0.0))), 81 | material: Arc::new(Material { 82 | bsdf: BSDF::Glass, 83 | //albedo: Texture::Constant(white), 84 | albedo: Texture::Constant(Vec3::thrice(0.99)), 85 | }), 86 | }, 87 | */ 88 | Object::Scatterer { // Left wall 89 | surface: Box::new(Parallelogram::from_square( 90 | Vec3 { x: -1.0, y: 0.0, z: 0.0 }, 91 | Vec3 { x: 1.0, y: 0.0, z: 0.0 }, 92 | 2.0 93 | )), 94 | material: Arc::new(Diffuse { 95 | albedo: Texture::Constant(red), 96 | }), 97 | }, 98 | Object::Scatterer { // Right wall 99 | surface: Box::new(Parallelogram::from_square( 100 | Vec3 { x: 1.0, y: 0.0, z: 0.0 }, 101 | Vec3 { x: -1.0, y: 0.0, z: 0.0 }, 102 | 2.0 103 | )), 104 | material: Arc::new(Diffuse { 105 | albedo: Texture::Constant(green), 106 | }), 107 | }, 108 | Object::Scatterer { // Back wall 109 | surface: Box::new(Parallelogram::from_square( 110 | Vec3 { x: 0.0, y: 0.0, z: -1.0 }, 111 | Vec3 { x: 0.0, y: 0.0, z: 1.0 }, 112 | 2.0 113 | )), 114 | material: Arc::new(Diffuse { 115 | albedo: Texture::Constant(white), 116 | }), 117 | }, 118 | Object::Scatterer { // Ceiling 119 | surface: Box::new(Parallelogram::from_square( 120 | Vec3 { x: 0.0, y: 1.0, z: 0.0 }, 121 | Vec3 { x: 0.0, y: -1.0, z: 0.0 }, 122 | 2.0 123 | )), 124 | material: Arc::new(Diffuse { 125 | albedo: Texture::Constant(white), 126 | }), 127 | }, 128 | Object::Scatterer { // Floor 129 | surface: Box::new(Parallelogram::from_square( 130 | Vec3 { x: 0.0, y: -1.0, z: 0.0 }, 131 | Vec3 { x: 0.0, y: 1.0, z: 0.0 }, 132 | 2.0 133 | )), 134 | material: Arc::new(Diffuse { 135 | //albedo: Texture::Constant(white), 136 | albedo: Texture::Grid(white, Vec3::thrice(0.25), 4, 0.02), 137 | }), 138 | }, 139 | ] 140 | ); 141 | 142 | let camera = Camera::new( 143 | &Mat4::look_at(Vec3::new(0.0, 0.0, 4.5), Vec3::zero(), Vec3::new(0.0, 1.0, 0.0)), 144 | (WIDTH, HEIGHT), 145 | 30.0, 146 | gamma, 147 | None, 148 | None, 149 | ); 150 | 151 | render_preview(scene, camera); 152 | //render(scene, camera, 64); 153 | } 154 | -------------------------------------------------------------------------------- /src/texture.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::{BufReader, Write, BufWriter}; 3 | use std::path::Path; 4 | use image; 5 | use math::{Vec3, bilerp}; 6 | use camera::Tonemap; 7 | 8 | pub enum Texture { 9 | Constant(Vec3), 10 | Grid(Vec3, Vec3, usize, f32), 11 | Checker { on_color: Vec3, off_color: Vec3, resolution: (f32, f32) }, 12 | Bitmap(Image), 13 | } 14 | 15 | impl Texture { 16 | pub fn eval(&self, (u, v): (f32, f32)) -> Vec3 { 17 | match *self { 18 | Texture::Constant(c) => c, 19 | Texture::Grid(c1, c2, s, w) => { 20 | let m = 1.0 / s as f32; 21 | let in_band = (u + 1.0 + w / 2.0) % m < w || (v + 1.0 + w / 2.0) % m < w; 22 | if in_band { c2 } else { c1 } 23 | }, 24 | Texture::Checker { on_color, off_color, resolution } => { 25 | let ui = (resolution.0 * u) as i32; 26 | let vi = (resolution.1 * v) as i32; 27 | let on = (ui ^ vi) & 1 != 0; 28 | if on { on_color } else { off_color } 29 | }, 30 | Texture::Bitmap(ref img) => { 31 | img.eval((u, v)) 32 | }, 33 | } 34 | } 35 | } 36 | 37 | pub struct Image { 38 | pub width: usize, 39 | pub height: usize, 40 | pixels: Vec, 41 | } 42 | 43 | impl Image { 44 | pub fn load_ldr>(filepath: P) -> Image { 45 | let img = image::open(&filepath).expect("failed to load texture").to_rgb(); 46 | let (width, height) = img.dimensions(); 47 | 48 | Image { 49 | width: width as usize, 50 | height: height as usize, 51 | pixels: img.pixels().map(|p| gamma_decode(p.data)).collect(), 52 | } 53 | } 54 | 55 | pub fn load_hdr>(filepath: P) -> Image { 56 | let reader = BufReader::new(File::open(filepath).expect("failed to open texture")); 57 | let decoder = image::hdr::HDRDecoder::with_strictness(reader, false).expect("failed to decode texture"); 58 | let meta = decoder.metadata(); 59 | let pixels = decoder.read_image_hdr().expect("failed to decode texture"); 60 | 61 | Image { 62 | width: meta.width as usize, 63 | height: meta.height as usize, 64 | pixels: pixels.iter().map(|p| Vec3::new(p[0], p[1], p[2])).collect(), 65 | } 66 | } 67 | 68 | pub fn get(&self, x: usize, y: usize) -> Vec3 { 69 | self.pixels[self.width * y + x] 70 | } 71 | 72 | /// Evaluate the texture using parametric coordinates and bilinear interpolation 73 | pub fn eval(&self, (u, v): (f32, f32)) -> Vec3 { 74 | let w = self.width as isize; 75 | let h = self.height as isize; 76 | 77 | // Convert parametric coordinates to texture coordinates, accounting for 78 | // - the half-pixel offset due to the continuous to discrete conversion 79 | // - the vertical flip of texture coordinates 80 | let tu = w as f32 * u - 0.5; 81 | let tv = h as f32 * (1.0 - v) - 0.5; 82 | 83 | let x0 = tu.floor() as isize; 84 | let y0 = tv.floor() as isize; 85 | let x1 = x0 + 1; 86 | let y1 = y0 + 1; 87 | 88 | // Compute the offset from the pixel due to the fractional part of coordinates 89 | let dx = tu - x0 as f32; 90 | let dy = tv - y0 as f32; 91 | 92 | // Handle off-boundaries coordinates by wrapping them around, thus repeating the texture 93 | let x0 = modulo(x0, w); 94 | let x1 = modulo(x1, w); 95 | let y0 = modulo(y0, h); 96 | let y1 = modulo(y1, h); 97 | 98 | // Finally, returns the bilinear interpolation of the 4 surrounding pixel values 99 | let v00 = self.get(x0, y0); 100 | let v01 = self.get(x1, y0); 101 | let v10 = self.get(x0, y1); 102 | let v11 = self.get(x1, y1); 103 | bilerp(v00, v01, v10, v11, (dx, dy)) 104 | } 105 | } 106 | 107 | /// Non-negative remainder of a divided by b. 108 | fn modulo(a: isize, b: isize) -> usize { 109 | let r = a % b; 110 | (if r < 0 { r + b } else { r }) as usize 111 | } 112 | 113 | fn gamma_decode([r, g, b]: [u8; 3]) -> Vec3 { 114 | // TODO: static lookup table for converting sRGB values into linear RGB values 115 | // static GAMMA_LOOKUP: Vec = (0..256).map(|v| (v as f32 / 255.0).powf(2.2)).collect(); 116 | let f = |v| (v as f32 / 255.0).powf(2.2); 117 | Vec3::new(f(r), f(g), f(b)) 118 | } 119 | 120 | pub fn luminance(color: Vec3) -> f32 { 121 | 0.2126 * color.x + 0.7152 * color.y + 0.0722 * color.z 122 | } 123 | 124 | pub fn write_ppm_srgb(path: &str, width: usize, height: usize, tonemap: Tonemap, pixels: I) 125 | where I: IntoIterator 126 | { 127 | let mut f = BufWriter::new(File::create(path).unwrap()); 128 | write!(f, "P6\n{} {}\n{}\n", width, height, 255).unwrap(); 129 | for p in pixels { 130 | let v = tonemap(p).map(|x| x * 255.0 + 0.5); 131 | f.write(&[v.x as u8, v.y as u8, v.z as u8]).unwrap(); 132 | } 133 | } 134 | 135 | pub fn write_ppm_raw(path: &str, width: usize, height: usize, pixels: &[u8]) 136 | { 137 | let mut f = BufWriter::new(File::create(path).unwrap()); 138 | write!(f, "P6\n{} {}\n{}\n", width, height, 255).unwrap(); 139 | f.write_all(&pixels).unwrap(); 140 | } 141 | 142 | /* 143 | pub fn write_hdr(path: &str, width: usize, height: usize, buf: &[(Vec3, f32)]) { 144 | let mut data: Vec> = Vec::with_capacity(width * height); 145 | for &(v, w) in buf { 146 | let r = v / w; 147 | data.push(image::Rgb { data: [r.x, r.y, r.z]}); 148 | } 149 | 150 | let f = BufWriter::new(File::create(path).unwrap()); 151 | let enc = image::hdr::HDREncoder::new(f); 152 | enc.encode(&data[..], width, height).unwrap(); 153 | } 154 | */ 155 | -------------------------------------------------------------------------------- /src/math/vec3.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Add, AddAssign, Sub, Mul, MulAssign, Div, Neg, Index, IndexMut}; 2 | use math::Axis; 3 | 4 | #[derive(Copy, Clone, PartialEq, Debug, Default, Serialize, Deserialize)] 5 | pub struct Vec3 { 6 | pub x: f32, 7 | pub y: f32, 8 | pub z: f32, 9 | } 10 | 11 | impl Vec3 { 12 | #[inline(always)] 13 | pub fn new(x: f32, y: f32, z: f32) -> Vec3 { 14 | Vec3 { x, y, z } 15 | } 16 | 17 | #[inline(always)] 18 | pub fn zero() -> Vec3 { 19 | Vec3 { x: 0.0, y: 0.0, z: 0.0 } 20 | } 21 | 22 | #[inline(always)] 23 | pub fn thrice(v: f32) -> Vec3 { 24 | Vec3 { x: v, y: v, z: v } 25 | } 26 | 27 | #[inline(always)] 28 | pub fn dot(lhs: Vec3, rhs: Vec3) -> f32 { 29 | (lhs * rhs).sum() 30 | } 31 | 32 | #[inline(always)] 33 | pub fn cross(lhs: Vec3, rhs: Vec3) -> Vec3 { 34 | Vec3 { 35 | x: lhs.y * rhs.z - lhs.z * rhs.y, 36 | y: lhs.z * rhs.x - lhs.x * rhs.z, 37 | z: lhs.x * rhs.y - lhs.y * rhs.x 38 | } 39 | } 40 | 41 | #[inline(always)] 42 | pub fn length(self) -> f32 { 43 | Vec3::dot(self, self).sqrt() 44 | } 45 | 46 | #[inline(always)] 47 | pub fn normalized(self) -> Vec3 { 48 | self / self.length() 49 | } 50 | 51 | #[inline(always)] 52 | pub fn dir_and_dist(p1: Vec3, p2: Vec3) -> (Vec3, f32) { 53 | let d = p2 - p1; 54 | let l = d.length(); 55 | (d / l, l) 56 | } 57 | 58 | #[inline(always)] 59 | pub fn min(lhs: Vec3, rhs: Vec3) -> Vec3 { 60 | Vec3 { 61 | x: lhs.x.min(rhs.x), 62 | y: lhs.y.min(rhs.y), 63 | z: lhs.z.min(rhs.z), 64 | } 65 | } 66 | 67 | #[inline(always)] 68 | pub fn max(lhs: Vec3, rhs: Vec3) -> Vec3 { 69 | Vec3 { 70 | x: lhs.x.max(rhs.x), 71 | y: lhs.y.max(rhs.y), 72 | z: lhs.z.max(rhs.z), 73 | } 74 | } 75 | 76 | #[inline(always)] 77 | pub fn sum(self) -> f32 { 78 | self.x + self.y + self.z 79 | } 80 | 81 | #[inline(always)] 82 | pub fn avg(self) -> f32 { 83 | self.sum() / 3.0 84 | } 85 | 86 | #[inline(always)] 87 | pub fn min_elem(self) -> f32 { 88 | self.x.min(self.y).min(self.z) 89 | } 90 | 91 | #[inline(always)] 92 | pub fn max_elem(self) -> f32 { 93 | self.x.max(self.y).max(self.z) 94 | } 95 | 96 | #[inline(always)] 97 | pub fn has_nan(self) -> bool { 98 | self.x.is_nan() || self.y.is_nan() || self.z.is_nan() 99 | } 100 | 101 | #[inline(always)] 102 | pub fn all_finite(self) -> bool { 103 | self.x.is_finite() && self.y.is_finite() && self.z.is_finite() 104 | } 105 | 106 | #[inline(always)] 107 | pub fn map(self, f: F) -> Vec3 108 | where F : Fn(f32) -> f32 109 | { 110 | Vec3 { 111 | x: f(self.x), 112 | y: f(self.y), 113 | z: f(self.z), 114 | } 115 | } 116 | } 117 | 118 | impl Add for Vec3 { 119 | type Output = Vec3; 120 | #[inline(always)] 121 | fn add(self, rhs: Vec3) -> Vec3 { 122 | Vec3 { x: self.x + rhs.x, y: self.y + rhs.y, z: self.z + rhs.z } 123 | } 124 | } 125 | 126 | impl AddAssign for Vec3 { 127 | #[inline(always)] 128 | fn add_assign(&mut self, rhs: Vec3) { 129 | *self = *self + rhs; 130 | } 131 | } 132 | 133 | impl Sub for Vec3 { 134 | type Output = Vec3; 135 | #[inline(always)] 136 | fn sub(self, rhs: Vec3) -> Vec3 { 137 | Vec3 { x: self.x - rhs.x, y: self.y - rhs.y, z: self.z - rhs.z } 138 | } 139 | } 140 | 141 | impl Mul for Vec3 { 142 | type Output = Vec3; 143 | #[inline(always)] 144 | fn mul(self, rhs: Vec3) -> Vec3 { 145 | Vec3 { x: self.x * rhs.x, y: self.y * rhs.y, z: self.z * rhs.z } 146 | } 147 | } 148 | 149 | impl Mul for Vec3 { 150 | type Output = Vec3; 151 | #[inline(always)] 152 | fn mul(self, rhs: f32) -> Vec3 { 153 | Vec3 { x: self.x * rhs, y: self.y * rhs, z: self.z * rhs } 154 | } 155 | } 156 | 157 | impl Mul for f32 { 158 | type Output = Vec3; 159 | #[inline(always)] 160 | fn mul(self, rhs: Vec3) -> Vec3 { 161 | Vec3 { x: self * rhs.x, y: self * rhs.y, z: self * rhs.z } 162 | } 163 | } 164 | 165 | impl MulAssign for Vec3 { 166 | #[inline(always)] 167 | fn mul_assign(&mut self, rhs: Vec3) { 168 | *self = *self * rhs; 169 | } 170 | } 171 | 172 | impl Div for Vec3 { 173 | type Output = Vec3; 174 | #[inline(always)] 175 | fn div(self, rhs: Vec3) -> Vec3 { 176 | Vec3 { x: self.x / rhs.x, y: self.y / rhs.y, z: self.z / rhs.z } 177 | } 178 | } 179 | 180 | impl Div for Vec3 { 181 | type Output = Vec3; 182 | #[inline(always)] 183 | fn div(self, rhs: f32) -> Vec3 { 184 | let s = 1.0 / rhs; 185 | self * s 186 | } 187 | } 188 | 189 | impl Div for f32 { 190 | type Output = Vec3; 191 | #[inline(always)] 192 | fn div(self, rhs: Vec3) -> Vec3 { 193 | Vec3 { x: self / rhs.x, y: self / rhs.y, z: self / rhs.z } 194 | } 195 | } 196 | 197 | impl Neg for Vec3 { 198 | type Output = Vec3; 199 | #[inline(always)] 200 | fn neg(self) -> Vec3 { 201 | Vec3 { x: -self.x, y: -self.y, z: -self.z } 202 | } 203 | } 204 | 205 | impl Index for Vec3 { 206 | type Output = f32; 207 | #[inline(always)] 208 | fn index(&self, index: Axis) -> &f32 { 209 | match index { 210 | Axis::X => &self.x, 211 | Axis::Y => &self.y, 212 | Axis::Z => &self.z, 213 | } 214 | } 215 | } 216 | 217 | impl IndexMut for Vec3 { 218 | #[inline(always)] 219 | fn index_mut(&mut self, index: Axis) -> &mut f32 { 220 | match index { 221 | Axis::X => &mut self.x, 222 | Axis::Y => &mut self.y, 223 | Axis::Z => &mut self.z, 224 | } 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/math/mat4.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Index, IndexMut, Mul}; 2 | use math::{Vec3, PI}; 3 | 4 | /// row-major 4x4 matrix 5 | #[derive(Debug, PartialEq, Clone, Copy)] 6 | // TODO: remove pub 7 | pub struct Mat4(pub(crate) [f32; 16]); 8 | 9 | impl Mat4 { 10 | pub fn zero() -> Mat4 { 11 | Mat4([0f32; 16]) 12 | } 13 | 14 | pub fn identity() -> Mat4 { 15 | Mat4([ 16 | 1.0, 0.0, 0.0, 0.0, 17 | 0.0, 1.0, 0.0, 0.0, 18 | 0.0, 0.0, 1.0, 0.0, 19 | 0.0, 0.0, 0.0, 1.0, 20 | ]) 21 | } 22 | 23 | pub fn inverse(&self) -> Mat4 { 24 | // Code derived from MESA, see https://stackoverflow.com/a/1148405 25 | let a = &self.0; 26 | let mut inv = [0.0; 16]; 27 | 28 | inv[ 0] = a[5]*a[10]*a[15] - a[5]*a[11]*a[14] - a[9]*a[6]*a[15] + a[9]*a[7]*a[14] + a[13]*a[6]*a[11] - a[13]*a[7]*a[10]; 29 | inv[ 1] = -a[1]*a[10]*a[15] + a[1]*a[11]*a[14] + a[9]*a[2]*a[15] - a[9]*a[3]*a[14] - a[13]*a[2]*a[11] + a[13]*a[3]*a[10]; 30 | inv[ 2] = a[1]*a[ 6]*a[15] - a[1]*a[ 7]*a[14] - a[5]*a[2]*a[15] + a[5]*a[3]*a[14] + a[13]*a[2]*a[ 7] - a[13]*a[3]*a[ 6]; 31 | inv[ 3] = -a[1]*a[ 6]*a[11] + a[1]*a[ 7]*a[10] + a[5]*a[2]*a[11] - a[5]*a[3]*a[10] - a[ 9]*a[2]*a[ 7] + a[ 9]*a[3]*a[ 6]; 32 | inv[ 4] = -a[4]*a[10]*a[15] + a[4]*a[11]*a[14] + a[8]*a[6]*a[15] - a[8]*a[7]*a[14] - a[12]*a[6]*a[11] + a[12]*a[7]*a[10]; 33 | inv[ 5] = a[0]*a[10]*a[15] - a[0]*a[11]*a[14] - a[8]*a[2]*a[15] + a[8]*a[3]*a[14] + a[12]*a[2]*a[11] - a[12]*a[3]*a[10]; 34 | inv[ 6] = -a[0]*a[ 6]*a[15] + a[0]*a[ 7]*a[14] + a[4]*a[2]*a[15] - a[4]*a[3]*a[14] - a[12]*a[2]*a[ 7] + a[12]*a[3]*a[ 6]; 35 | inv[ 8] = a[4]*a[ 9]*a[15] - a[4]*a[11]*a[13] - a[8]*a[5]*a[15] + a[8]*a[7]*a[13] + a[12]*a[5]*a[11] - a[12]*a[7]*a[ 9]; 36 | inv[ 7] = a[0]*a[ 6]*a[11] - a[0]*a[ 7]*a[10] - a[4]*a[2]*a[11] + a[4]*a[3]*a[10] + a[ 8]*a[2]*a[ 7] - a[ 8]*a[3]*a[ 6]; 37 | inv[ 9] = -a[0]*a[ 9]*a[15] + a[0]*a[11]*a[13] + a[8]*a[1]*a[15] - a[8]*a[3]*a[13] - a[12]*a[1]*a[11] + a[12]*a[3]*a[ 9]; 38 | inv[10] = a[0]*a[ 5]*a[15] - a[0]*a[ 7]*a[13] - a[4]*a[1]*a[15] + a[4]*a[3]*a[13] + a[12]*a[1]*a[ 7] - a[12]*a[3]*a[ 5]; 39 | inv[11] = -a[0]*a[ 5]*a[11] + a[0]*a[ 7]*a[ 9] + a[4]*a[1]*a[11] - a[4]*a[3]*a[ 9] - a[ 8]*a[1]*a[ 7] + a[ 8]*a[3]*a[ 5]; 40 | inv[12] = -a[4]*a[ 9]*a[14] + a[4]*a[10]*a[13] + a[8]*a[5]*a[14] - a[8]*a[6]*a[13] - a[12]*a[5]*a[10] + a[12]*a[6]*a[ 9]; 41 | inv[13] = a[0]*a[ 9]*a[14] - a[0]*a[10]*a[13] - a[8]*a[1]*a[14] + a[8]*a[2]*a[13] + a[12]*a[1]*a[10] - a[12]*a[2]*a[ 9]; 42 | inv[14] = -a[0]*a[ 5]*a[14] + a[0]*a[ 6]*a[13] + a[4]*a[1]*a[14] - a[4]*a[2]*a[13] - a[12]*a[1]*a[ 6] + a[12]*a[2]*a[ 5]; 43 | inv[15] = a[0]*a[ 5]*a[10] - a[0]*a[ 6]*a[ 9] - a[4]*a[1]*a[10] + a[4]*a[2]*a[ 9] + a[ 8]*a[1]*a[ 6] - a[ 8]*a[2]*a[ 5]; 44 | 45 | let det = a[0] * inv[0] + a[1] * inv[4] + a[2] * inv[8] + a[3] * inv[12]; 46 | debug_assert!(det != 0.0); 47 | let inv_det = 1.0 / det; 48 | 49 | for x in inv.iter_mut() { 50 | *x *= inv_det; 51 | } 52 | 53 | Mat4(inv) 54 | } 55 | 56 | pub fn scale(v: Vec3) -> Mat4 { 57 | Mat4([ 58 | v.x, 0.0, 0.0, 0.0, 59 | 0.0, v.y, 0.0, 0.0, 60 | 0.0, 0.0, v.z, 0.0, 61 | 0.0, 0.0, 0.0, 1.0, 62 | ]) 63 | } 64 | 65 | pub fn translate(v: Vec3) -> Mat4 { 66 | Mat4([ 67 | 1.0, 0.0, 0.0, v.x, 68 | 0.0, 1.0, 0.0, v.y, 69 | 0.0, 0.0, 1.0, v.z, 70 | 0.0, 0.0, 0.0, 1.0, 71 | ]) 72 | } 73 | 74 | pub fn rot_yxz(v: Vec3) -> Mat4 { 75 | let r = v * (PI / 180.0); 76 | let c = [f32::cos(r.x), f32::cos(r.y), f32::cos(r.z)]; 77 | let s = [f32::sin(r.x), f32::sin(r.y), f32::sin(r.z)]; 78 | 79 | Mat4([ 80 | c[1]*c[2] - s[1]*s[0]*s[2], -c[1]*s[2] - s[1]*s[0]*c[2], -s[1]*c[0], 0.0, 81 | c[0]*s[2], c[0]*c[2], -s[0], 0.0, 82 | s[1]*c[2] + c[1]*s[0]*s[2], -s[1]*s[2] + c[1]*s[0]*c[2], c[1]*c[0], 0.0, 83 | 0.0, 0.0, 0.0, 1.0 84 | ]) 85 | } 86 | 87 | pub fn transform_point(&self, p: Vec3) -> Vec3 { 88 | let a = &self; 89 | Vec3 { 90 | x: a[(0,0)] * p.x + a[(0,1)] * p.y + a[(0,2)] * p.z + a[(0,3)], 91 | y: a[(1,0)] * p.x + a[(1,1)] * p.y + a[(1,2)] * p.z + a[(1,3)], 92 | z: a[(2,0)] * p.x + a[(2,1)] * p.y + a[(2,2)] * p.z + a[(2,3)], 93 | } 94 | } 95 | 96 | pub fn transform_vector(&self, p: Vec3) -> Vec3 { 97 | let a = &self; 98 | Vec3 { 99 | x: a[(0,0)] * p.x + a[(0,1)] * p.y + a[(0,2)] * p.z, 100 | y: a[(1,0)] * p.x + a[(1,1)] * p.y + a[(1,2)] * p.z, 101 | z: a[(2,0)] * p.x + a[(2,1)] * p.y + a[(2,2)] * p.z, 102 | } 103 | } 104 | 105 | pub fn look_at(pos: Vec3, look_at: Vec3, up: Vec3) -> Mat4 { 106 | let f = (look_at - pos).normalized(); 107 | let r = Vec3::cross(f, up).normalized(); 108 | let u = Vec3::cross(r, f).normalized(); 109 | 110 | Mat4([ 111 | r.x, u.x, f.x, pos.x, 112 | r.y, u.y, f.y, pos.y, 113 | r.z, u.z, f.z, pos.z, 114 | 0.0, 0.0, 0.0, 1.0 115 | ]) 116 | } 117 | } 118 | 119 | impl Mul for Mat4 { 120 | type Output = Mat4; 121 | fn mul(self, rhs: Mat4) -> Mat4 { 122 | let a = &self.0; 123 | let b = &rhs.0; 124 | let mut result = [0.0; 16]; 125 | 126 | for i in 0..4 { 127 | for t in 0..4 { 128 | result[i*4 + t] = 129 | a[i*4 + 0]*b[0*4 + t] + 130 | a[i*4 + 1]*b[1*4 + t] + 131 | a[i*4 + 2]*b[2*4 + t] + 132 | a[i*4 + 3]*b[3*4 + t]; 133 | } 134 | } 135 | 136 | Mat4(result) 137 | } 138 | } 139 | 140 | impl Index<(usize, usize)> for Mat4 { 141 | type Output = f32; 142 | 143 | fn index<'a>(&'a self, coord: (usize, usize)) -> &'a f32 { 144 | &self.0[4 * coord.0 + coord.1] 145 | } 146 | } 147 | 148 | impl IndexMut<(usize, usize)> for Mat4 { 149 | fn index_mut<'a>(&'a mut self, coord: (usize, usize)) -> &'a mut f32 { 150 | &mut self.0[4 * coord.0 + coord.1] 151 | } 152 | } 153 | 154 | #[test] 155 | fn test_inverse_identity() { 156 | assert!(Mat4::identity().inverse() == Mat4::identity()); 157 | } 158 | -------------------------------------------------------------------------------- /scenes/material-testball/scene.json: -------------------------------------------------------------------------------- 1 | { 2 | "media": [], 3 | "bsdfs": [ 4 | { 5 | "name": "Material", 6 | "type": "smooth_coat", 7 | "ior": 1.5, 8 | "sigma_a": [0.02, 0.25, 0.55], 9 | "thickness": 5.0, 10 | "substrate": { 11 | "name": "", 12 | "type": "rough_conductor", 13 | "albedo": 0.99, 14 | "material": "Al", 15 | "roughness": 0.1 16 | } 17 | }, 18 | { 19 | "name": "Stand", 20 | "albedo": 0.2, 21 | "type": "lambert" 22 | }, 23 | { 24 | "name": "Floor", 25 | "albedo": { 26 | "type": "checker", 27 | "on_color": [ 28 | 0.725, 29 | 0.71, 30 | 0.68 31 | ], 32 | "off_color": [ 33 | 0.325, 34 | 0.31, 35 | 0.25 36 | ], 37 | "res_u": 20, 38 | "res_v": 20 39 | }, 40 | "type": "lambert" 41 | } 42 | ], 43 | "primitives": [ 44 | { 45 | "bump_strength": 1, 46 | "transform": { 47 | "position": [ 48 | 0, 49 | 0, 50 | 1.17369 51 | ], 52 | "rotation": [ 53 | 0, 54 | -67.2614, 55 | 0 56 | ] 57 | }, 58 | "emission": "textures/envmap.hdr", 59 | "type": "infinite_sphere", 60 | "sample": true, 61 | "bsdf": { 62 | "albedo": 1, 63 | "type": "null" 64 | } 65 | }, 66 | { 67 | "transform": { 68 | "position": [ 69 | -0.708772, 70 | 0, 71 | -0.732108 72 | ], 73 | "scale": 5.43618, 74 | "rotation": [ 75 | 0, 76 | 46.1511, 77 | 180 78 | ] 79 | }, 80 | "bump_strength": 1, 81 | "type": "quad", 82 | "bsdf": "Floor" 83 | }, 84 | { 85 | "transform": { 86 | "position": [ 87 | 0.0571719, 88 | 0.213656, 89 | 0.0682078 90 | ], 91 | "scale": 0.482906 92 | }, 93 | "bump_strength": 1, 94 | "type": "mesh", 95 | "file": "models/Mesh001.obj", 96 | "smooth": true, 97 | "backface_culling": false, 98 | "recompute_normals": false, 99 | "bsdf": "Material" 100 | }, 101 | { 102 | "transform": { 103 | "position": [ 104 | 0.156382, 105 | 0.777229, 106 | 0.161698 107 | ], 108 | "scale": 0.482906 109 | }, 110 | "bump_strength": 1, 111 | "type": "mesh", 112 | "file": "models/Mesh002.obj", 113 | "smooth": true, 114 | "backface_culling": false, 115 | "recompute_normals": false, 116 | "bsdf": "Material" 117 | }, 118 | { 119 | "transform": { 120 | "position": [ 121 | 0.110507, 122 | 0.494301, 123 | 0.126194 124 | ], 125 | "scale": 0.482906 126 | }, 127 | "bump_strength": 1, 128 | "type": "mesh", 129 | "file": "models/Mesh000.obj", 130 | "smooth": true, 131 | "backface_culling": false, 132 | "recompute_normals": false, 133 | "bsdf": "Stand" 134 | } 135 | ], 136 | "camera": { 137 | "tonemap": "filmic", 138 | "resolution": [ 139 | 1280, 140 | 720 141 | ], 142 | "reconstruction_filter": "tent", 143 | "transform": { 144 | "position": [ 145 | 3.04068, 146 | 3.17153, 147 | 3.20454 148 | ], 149 | "look_at": [ 150 | 0.118789, 151 | 0.473398, 152 | 0.161081 153 | ], 154 | "up": [ 155 | 0, 156 | 1, 157 | 0 158 | ] 159 | }, 160 | "type": "pinhole", 161 | "fov": 35, 162 | "focus_distance": 4.6, 163 | "aperture_size": 0.04 164 | }, 165 | "integrator": { 166 | "type": "path_tracer", 167 | "min_bounces": 0, 168 | "max_bounces": 64, 169 | "enable_light_sampling": true, 170 | "enable_volume_light_sampling": true, 171 | "enable_consistency_checks": false, 172 | "enable_two_sided_shading": true 173 | }, 174 | "renderer": { 175 | "output_file": "material-testball.png", 176 | "resume_render_file": "RenderState.dat", 177 | "overwrite_output_files": true, 178 | "adaptive_sampling": true, 179 | "enable_resume_render": false, 180 | "stratified_sampler": true, 181 | "scene_bvh": true, 182 | "spp": 64, 183 | "spp_step": 16, 184 | "checkpoint_interval": "0", 185 | "hdr_output_file": "material-testball.exr" 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/obj.rs: -------------------------------------------------------------------------------- 1 | use std::fs::{File, create_dir}; 2 | use std::env::temp_dir; 3 | use std::io::{BufRead, BufReader, BufWriter}; 4 | use std::path::Path; 5 | use std::collections::hash_map::DefaultHasher; 6 | use std::hash::{Hash, Hasher}; 7 | use std::collections::HashMap; 8 | use bincode; 9 | 10 | use mesh::*; 11 | use math::*; 12 | use mat4::*; 13 | 14 | struct ObjTriangle { 15 | vidxs: [Index; 3], 16 | nidxs: [Index; 3], 17 | uidxs: [Index; 3], 18 | } 19 | 20 | pub fn load>(path: P, transform: &Mat4) -> Vec<(String, Mesh)> { 21 | // compute cache path from the filepath 22 | let hash = { 23 | let mut hasher = DefaultHasher::new(); 24 | path.as_ref().hash(&mut hasher); 25 | for f in &transform.0 { 26 | f.to_bits().hash(&mut hasher); 27 | } 28 | hasher.finish() 29 | }; 30 | let cache_path = temp_dir().join("obj_cache"); 31 | let obj_cache = cache_path.join(hash.to_string()); 32 | 33 | // load from cache if possible 34 | if let Ok(r) = File::open(&obj_cache) { 35 | println!("Mesh {} found in cache", path.as_ref().to_str().unwrap()); 36 | let mut br = BufReader::new(r); 37 | if let Ok(meshes) = bincode::deserialize_from(&mut br) { 38 | return meshes; 39 | } 40 | println!("Failed to load OBJ mesh from cache"); 41 | } 42 | 43 | println!("Loading mesh {}", path.as_ref().to_str().unwrap()); 44 | 45 | let f = BufReader::new(File::open(path).unwrap()); 46 | let mut vertices: Vec = Vec::new(); 47 | let mut normals: Vec = Vec::new(); 48 | let mut uvs: Vec<(f32, f32)> = Vec::new(); 49 | let mut triangles: Vec = Vec::new(); 50 | let mut meshes = Vec::new(); 51 | let mut curr_name = String::new(); 52 | 53 | fn normalize_obj_idx(idx: isize, len: usize) -> Index { 54 | if idx < 0 { 55 | (len as isize + idx) as Index 56 | } else { 57 | idx as Index - 1 58 | } 59 | } 60 | 61 | for line in f.lines() { 62 | let s = line.unwrap(); 63 | let mut iter = s.split_whitespace(); 64 | match iter.next() { 65 | Some("v") => { 66 | let vs: Vec = iter.map(|s| s.parse::().unwrap()).collect(); 67 | let v = Vec3::new(vs[0], vs[1], vs[2]); 68 | vertices.push(transform.transform_point(v)); 69 | }, 70 | Some("vt") => { 71 | let v: Vec = iter.map(|s| s.parse::().unwrap()).collect(); 72 | uvs.push((v[0], v[1])); 73 | }, 74 | Some("vn") => { 75 | let v: Vec = iter.map(|s| s.parse::().unwrap()).collect(); 76 | let n = Vec3::new(v[0], v[1], v[2]).normalized(); 77 | debug_assert!(!n.has_nan(), "invalid normal"); 78 | // TODO: transform normal 79 | normals.push(n); 80 | }, 81 | Some("f") => { 82 | let g: Vec<(Index, Index, Index)> = iter.map(|group| { 83 | let mut iter = group.split('/'); 84 | let vi = iter.next().unwrap().parse::().unwrap(); 85 | let ui = iter.next().unwrap().parse::().unwrap(); 86 | let ni = iter.next().unwrap().parse::().unwrap(); 87 | (normalize_obj_idx(vi, vertices.len()), normalize_obj_idx(ui, uvs.len()), normalize_obj_idx(ni, normals.len())) 88 | }).collect(); 89 | for i in 2..g.len() { 90 | triangles.push(ObjTriangle { 91 | vidxs: [g[0].0, g[i-1].0, g[i].0], 92 | uidxs: [g[0].1, g[i-1].1, g[i].1], 93 | nidxs: [g[0].2, g[i-1].2, g[i].2], 94 | }); 95 | } 96 | }, 97 | Some("o") => { 98 | if !triangles.is_empty() { 99 | meshes.push((curr_name.clone(), create_mesh(&vertices, &normals, &uvs, &triangles))); 100 | triangles.clear(); 101 | } 102 | curr_name = iter.next().unwrap_or_default().to_owned(); 103 | } 104 | _ => {} 105 | } 106 | } 107 | 108 | if !triangles.is_empty() { 109 | meshes.push((curr_name.clone(), create_mesh(&vertices, &normals, &uvs, &triangles))); 110 | } 111 | println!("Loaded obj scene with {} meshes", meshes.len()); 112 | 113 | //println!("Mesh bounding box: {:?}", mesh.bvh.bbox()); 114 | 115 | // Cache the mesh 116 | { 117 | println!("Caching OBJ mesh"); 118 | let _ = create_dir(cache_path); 119 | let mut bw = BufWriter::new(File::create(&obj_cache).unwrap()); 120 | // TODO: serializing edges is not necessary as they can be recomputed quickly 121 | bincode::serialize_into(&mut bw, &meshes).unwrap(); 122 | } 123 | 124 | return meshes; 125 | } 126 | 127 | /// create a mesh with given vertices, normals and triangles so that 128 | /// - there is a 1-1 correspondence between vertices and normals 129 | /// - vertices and normals are stored only if referred to in a triangle 130 | fn create_mesh(vertices: &[Vec3], normals: &[Vec3], uvs: &[(f32, f32)], triangles: &[ObjTriangle]) -> Mesh { 131 | let mut vs = Vec::new(); 132 | let mut ns = Vec::new(); 133 | let mut us = Vec::new(); 134 | let mut ts = Vec::new(); 135 | let mut vertex_map = HashMap::with_capacity(vertices.len()); 136 | 137 | for obj_tri in triangles { 138 | let mut new_tri = Triangle { idxs: [0; 3] }; 139 | 140 | for i in 0..3 { 141 | let vidx = obj_tri.vidxs[i]; 142 | let nidx = obj_tri.nidxs[i]; 143 | let uidx = obj_tri.uidxs[i]; 144 | 145 | // check if we already created a new vertex for this couple 146 | new_tri.idxs[i] = match vertex_map.get(&(vidx, nidx, uidx)) { 147 | Some(&idx) => idx, 148 | None => { 149 | // if not, create a new one and store it 150 | let idx = vs.len() as Index; 151 | vs.push(vertices[vidx as usize]); 152 | ns.push(normals[nidx as usize]); 153 | us.push(uvs[uidx as usize]); 154 | vertex_map.insert((vidx, nidx, uidx), idx); 155 | idx 156 | } 157 | }; 158 | } 159 | 160 | ts.push(new_tri); 161 | } 162 | 163 | println!("Loaded mesh with {} vertices, {} normals and {} triangles", vs.len(), ns.len(), ts.len()); 164 | Mesh::new(vs, ns, us, ts) 165 | } 166 | 167 | // Compute smooth normals 168 | /* 169 | if mesh.normals.len() == 0 { 170 | println!("Computing vertex normals..."); 171 | mesh.normals.resize(nv, Vec3::zero()); 172 | for (i, &(i0, i1, i2)) in mesh.triangles.iter().enumerate() { 173 | let normal = Vec3::cross(mesh.triangles_e1[i], mesh.triangles_e2[i]); //.normalized(); 174 | mesh.normals[i0] = mesh.normals[i0] + normal; 175 | mesh.normals[i1] = mesh.normals[i1] + normal; 176 | mesh.normals[i2] = mesh.normals[i2] + normal; 177 | } 178 | for i in 0..nv { 179 | mesh.normals[i] = mesh.normals[i].normalized(); 180 | } 181 | } 182 | */ 183 | 184 | -------------------------------------------------------------------------------- /src/primitive.rs: -------------------------------------------------------------------------------- 1 | use geometry::*; 2 | use math::*; 3 | use mat4::*; 4 | use warp::*; 5 | use light::*; 6 | 7 | #[derive(Copy, Clone)] 8 | pub struct Sphere { 9 | pub radius: f32, 10 | pub position: Vec3, 11 | } 12 | 13 | impl Sphere { 14 | pub fn new(radius: f32, position: Vec3) -> Sphere { 15 | Sphere { radius: radius, position: position } 16 | } 17 | } 18 | 19 | impl Surface for Sphere { 20 | // returns distance, infinity if no hit. 21 | fn intersect(&self, ray: Ray) -> Option { 22 | let to_sphere = ray.origin - self.position; 23 | let b = 2.0 * Vec3::dot(to_sphere, ray.direction); 24 | let c = Vec3::dot(to_sphere, to_sphere) - self.radius * self.radius; 25 | let discriminant = b * b - 4.0 * c; 26 | if discriminant > 0.0 { 27 | let t = { 28 | let s = discriminant.sqrt(); 29 | let t1 = (-b - s) / 2.0; 30 | if t1 > 0.0 { t1 } else { (-b + s) / 2.0 } 31 | }; 32 | if t > 0.0 { 33 | let normal = (ray.point_at(t) - self.position) / self.radius; 34 | let phi = normal.z.atan2(normal.x); 35 | let theta = normal.y.acos(); 36 | let u = phi * INV_2_PI; 37 | let v = theta * INV_PI; 38 | return Some(Intersection { 39 | distance: t, 40 | normal: normal, 41 | // TODO: floor not necessary? 42 | uv: (u - u.floor(), v - v.floor()), 43 | }); 44 | } 45 | } 46 | return None; 47 | } 48 | 49 | fn aabb(&self) -> AABB { 50 | AABB { 51 | min: self.position + Vec3::thrice(-self.radius), 52 | max: self.position + Vec3::thrice( self.radius), 53 | } 54 | } 55 | } 56 | 57 | pub struct Parallelogram { 58 | position: Vec3, 59 | edge1: Vec3, 60 | edge2: Vec3, 61 | // cache 62 | normal: Vec3, 63 | l1: f32, 64 | l2: f32, 65 | area: f32, 66 | } 67 | 68 | impl Parallelogram { 69 | /// Create a parallelogram by transforming the unit parallelogram 70 | /// 71 | /// The unit parallelogram is a square of width 1 centered in the plane XZ 72 | pub fn unit_transform(transform: &Mat4) -> Parallelogram { 73 | let edge1 = transform.transform_vector(Vec3::new(0.0, 0.0, 1.0)); 74 | let edge2 = transform.transform_vector(Vec3::new(1.0, 0.0, 0.0)); 75 | let position = transform.transform_point(Vec3::new(-0.5, 0.0, -0.5)); 76 | 77 | Parallelogram::new(position, edge1, edge2) 78 | } 79 | 80 | pub fn new(position: Vec3, edge1: Vec3, edge2: Vec3) -> Parallelogram { 81 | let l1 = Vec3::dot(edge1, edge1); 82 | let l2 = Vec3::dot(edge2, edge2); 83 | Parallelogram { 84 | position: position, 85 | edge1: edge1, 86 | edge2: edge2, 87 | normal: Vec3::cross(edge2, edge1).normalized(), 88 | l1: l1, 89 | l2: l2, 90 | area: (l1 * l2).sqrt(), 91 | } 92 | } 93 | 94 | pub fn from_square(center: Vec3, normal: Vec3, side_length: f32) -> Parallelogram { 95 | let a = Vec3::new(normal.y, normal.z, -normal.x).normalized(); 96 | let b = Vec3::cross(a, normal); 97 | let d1 = a * (side_length * 0.5); 98 | let d2 = b * (side_length * 0.5); 99 | Parallelogram::new(center - d1 - d2, a * side_length, b * side_length) 100 | } 101 | } 102 | 103 | impl Surface for Parallelogram { 104 | fn intersect(&self, ray: Ray) -> Option { 105 | let nd = Vec3::dot(self.normal, ray.direction); 106 | 107 | let t = (Vec3::dot(self.normal, self.position) - Vec3::dot(self.normal, ray.origin)) / nd; 108 | if t < 0.0 { 109 | return None; 110 | } 111 | 112 | let p = ray.point_at(t) - self.position; 113 | let u = Vec3::dot(self.edge1, p); 114 | let v = Vec3::dot(self.edge2, p); 115 | 116 | if !(0.0 <= u && u <= self.l1 && 0.0 <= v && v <= self.l2) { 117 | return None; 118 | } 119 | 120 | // normalize uv 121 | let u = u / self.l1; 122 | let v = v / self.l2; 123 | 124 | Some(Intersection { 125 | distance: t, 126 | normal: self.normal, 127 | uv: (u - u.floor(), v - v.floor()), 128 | }) 129 | } 130 | 131 | fn aabb(&self) -> AABB { 132 | let mut aabb = AABB::from_point(self.position); 133 | aabb.extend_point(self.position + self.edge1); 134 | aabb.extend_point(self.position + self.edge2); 135 | aabb.extend_point(self.position + self.edge1 + self.edge2); 136 | return aabb; 137 | } 138 | } 139 | 140 | impl SampleDirectSurface for Parallelogram { 141 | fn sample_direct(&self, p: Vec3, (u, v): (f32, f32)) -> DirectSample { 142 | //if Vec3::dot(self.normal, p - self.position) <= 0.0 { 143 | // return DirectSample { 144 | // dir: Vec3::zero(), 145 | // dist: 0.0, 146 | // pdf: 0.0, 147 | // } 148 | //} 149 | 150 | let q = self.position + self.edge1 * u + self.edge2 * v; 151 | let (dir, dist) = Vec3::dir_and_dist(p, q); 152 | //let cos_theta = -Vec3::dot(self.normal, dir); 153 | let cos_theta = Vec3::dot(self.normal, dir).abs(); 154 | let pdf = dist * dist / (cos_theta * self.area); 155 | 156 | DirectSample { dir, dist, pdf } 157 | } 158 | 159 | fn pdf_direct(&self, dir: Vec3, dist: f32) -> f32 { 160 | //let cos_theta = -Vec3::dot(self.normal, dir); 161 | //if cos_theta <= 0.0 { 162 | // 0.0 163 | //} else { 164 | // dist * dist / (self.area * cos_theta) 165 | //} 166 | 167 | let cos_theta = Vec3::dot(self.normal, dir).abs(); 168 | dist * dist / (self.area * cos_theta) 169 | } 170 | } 171 | 172 | pub struct Disk { 173 | center: Vec3, 174 | normal: Vec3, 175 | radius: f32, 176 | // cache 177 | u_axis: Vec3, 178 | v_axis: Vec3, 179 | } 180 | 181 | impl Disk { 182 | pub fn new(center: Vec3, normal: Vec3, radius: f32) -> Disk { 183 | let u_axis = Vec3::new(normal.y, normal.z, -normal.x).normalized(); 184 | let v_axis = Vec3::cross(u_axis, normal); 185 | 186 | Disk { 187 | center: center, 188 | normal: normal, 189 | radius: radius, 190 | u_axis: u_axis, 191 | v_axis: v_axis, 192 | } 193 | } 194 | } 195 | 196 | impl Surface for Disk { 197 | fn intersect(&self, ray: Ray) -> Option { 198 | let nd = Vec3::dot(self.normal, ray.direction); 199 | 200 | let t = (Vec3::dot(self.normal, self.center) - Vec3::dot(self.normal, ray.origin)) / nd; 201 | if t < 0.0 { 202 | return None; 203 | } 204 | 205 | let p = ray.point_at(t); 206 | let u = Vec3::dot(self.u_axis, p); 207 | let v = Vec3::dot(self.v_axis, p); 208 | 209 | if u * u + v * v > self.radius * self.radius { 210 | return None; 211 | } 212 | 213 | Some(Intersection { 214 | distance: t, 215 | normal: self.normal, 216 | uv: (u - u.floor(), v - v.floor()), 217 | }) 218 | } 219 | 220 | fn aabb(&self) -> AABB { 221 | unimplemented!(); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/bvh.rs: -------------------------------------------------------------------------------- 1 | use rayon; 2 | 3 | use geometry::*; 4 | use math::*; 5 | 6 | #[derive(Serialize, Deserialize)] 7 | pub struct BVH { 8 | bbox: AABB, 9 | node: Node, 10 | } 11 | 12 | #[derive(Serialize, Deserialize)] 13 | enum Node { 14 | Leaf { begin: usize, end: usize }, 15 | Split { split_axis: Axis, children: [Box; 2] }, 16 | } 17 | 18 | impl BVH { 19 | pub fn bbox(&self) -> AABB { 20 | self.bbox 21 | } 22 | 23 | pub fn build(proj_centroid: &F, item_bbox: &G, items: &mut [I]) -> BVH 24 | where I: Send, F: (Fn(&I, Axis) -> f32) + Sync, G: (Fn(&I) -> AABB) + Sync 25 | { 26 | let n = items.len(); 27 | build_rec(proj_centroid, item_bbox, items, 0, n, &mut vec![0.0; n]) 28 | } 29 | 30 | /// Return (t, i, data) for the closest item i or t == -1 if miss 31 | pub fn intersect(&self, intersect_item: &F, ray: Ray) -> (f32, usize, D) 32 | where D: Default, F: Fn(Ray, usize) -> (f32, D) 33 | { 34 | intersect_rec(intersect_item, ray, self, INFINITY, 1.0 / ray.direction) 35 | } 36 | } 37 | 38 | fn build_rec(proj_centroid: &F, item_bbox: &G, items: &mut [I], begin: usize, end: usize, buffer: &mut [f32]) -> BVH 39 | where I: Send, F: (Fn(&I, Axis) -> f32) + Sync, G: (Fn(&I) -> AABB) + Sync 40 | { 41 | const INTERSECTION_COST: f32 = 1.0; 42 | const TRAVERSAL_COST: f32 = 1.5; 43 | 44 | let n = end - begin; 45 | let mut best_axis = None; 46 | let mut best_cost = INTERSECTION_COST * n as f32; 47 | let mut best_index = 0; 48 | let mut node_bbox = AABB::empty(); 49 | 50 | // Try splitting along every axis 51 | for &axis in &[Axis::X, Axis::Y, Axis::Z] { 52 | sort_projected_centroid(&proj_centroid, items, axis); 53 | 54 | // Compute AABB surface areas incrementally from the left 55 | let mut bbox = AABB::empty(); 56 | for (i, t) in items.iter().enumerate() { 57 | bbox = bbox.union(&item_bbox(t)); 58 | buffer[i] = bbox.surface_area(); 59 | } 60 | 61 | if axis == Axis::X { 62 | node_bbox = bbox; 63 | } 64 | 65 | /* Choose the split plane by computing AABB surface areas incrementally from 66 | * the right, and comparing them against the one from the left with the heuristic */ 67 | let mut bbox = AABB::empty(); 68 | let tri_factor = INTERSECTION_COST / node_bbox.surface_area(); 69 | for (i, t) in items.iter().enumerate().skip(1).rev() { 70 | bbox = bbox.union(&item_bbox(t)); 71 | 72 | let left_area = buffer[i - 1]; 73 | let right_area = bbox.surface_area(); 74 | let prims_left = i as f32; 75 | let prims_right = (n - i) as f32; 76 | 77 | let sah_cost = 2.0 * TRAVERSAL_COST 78 | + tri_factor * (prims_left * left_area + prims_right * right_area); 79 | if sah_cost < best_cost { 80 | best_cost = sah_cost; 81 | best_axis = Some(axis); 82 | best_index = i; 83 | } 84 | } 85 | } 86 | 87 | let node = match best_axis { 88 | None => Node::Leaf { begin, end }, // Couldn't find a split that reduces the cost, make a leaf 89 | Some(axis) => { 90 | if axis != Axis::Z { // we just sorted on Z, only sort for X and Y 91 | // TODO: sorting is not necessary, we just need to partition 92 | sort_projected_centroid(&proj_centroid, items, axis); 93 | } 94 | let mid = begin + best_index; 95 | let (buf1, buf2) = buffer.split_at_mut(best_index); 96 | let (tri1, tri2) = items.split_at_mut(best_index); 97 | let build1 = || Box::new(build_rec(proj_centroid, item_bbox, tri1, begin, mid, buf1)); 98 | let build2 = || Box::new(build_rec(proj_centroid, item_bbox, tri2, mid, end, buf2)); 99 | // TODO: sequential fallback 100 | let (c1, c2) = rayon::join(build1, build2); 101 | Node::Split { split_axis: axis, children: [c1, c2] } 102 | }, 103 | }; 104 | 105 | BVH { bbox: node_bbox, node: node } 106 | } 107 | 108 | // return (t, i, data) for the closest item i or t == -1 if miss 109 | fn intersect_rec(intersect_item: &F, ray: Ray, bvh: &BVH, dist_max: f32, inv_dir: Vec3) -> (f32, usize, D) 110 | where D: Default, F: Fn(Ray, usize) -> (f32, D) 111 | { 112 | let (t_near, t_far) = bvh.bbox.intersect_fast(ray, inv_dir); 113 | if t_far < 0.0 || (t_near >= 0.0 && t_near >= dist_max) { 114 | return (-1.0, 0, Default::default()); 115 | } 116 | 117 | match bvh.node { 118 | Node::Leaf { begin, end } => intersect_items(intersect_item, ray, begin, end), 119 | Node::Split { split_axis, ref children } => { 120 | // order the children according to ray direction 121 | let (c1, c2) = if ray.direction[split_axis] < 0.0 { 122 | (&children[1], &children[0]) 123 | } else { 124 | (&children[0], &children[1]) 125 | }; 126 | 127 | let its1 = intersect_rec(intersect_item, ray, c1, dist_max, inv_dir); 128 | if its1.0 < 0.0 { 129 | // no intersection in first child, check the other one 130 | intersect_rec(intersect_item, ray, c2, dist_max, inv_dir) 131 | } else { 132 | // intersection in first child, check if there is a closer intersection in the other one 133 | let its2 = intersect_rec(intersect_item, ray, c2, its1.0, inv_dir); 134 | if its2.0 < 0.0 || its1.0 < its2.0 { 135 | its1 136 | } else { 137 | its2 138 | } 139 | } 140 | }, 141 | } 142 | } 143 | 144 | // returns (t, i, data) for the closest item i or t == -1 if miss 145 | fn intersect_items(intersect_item: F, ray: Ray, begin: usize, end: usize) -> (f32, usize, D) 146 | where D: Default, F: Fn(Ray, usize) -> (f32, D) 147 | { 148 | let mut t_min = INFINITY; 149 | let mut i_min = 0; 150 | let mut d_min = Default::default(); 151 | 152 | for i in begin..end { 153 | let (t, d) = intersect_item(ray, i); 154 | if 0.0 < t && t < t_min { 155 | t_min = t; 156 | i_min = i; 157 | d_min = d; 158 | } 159 | } 160 | 161 | if t_min == INFINITY { 162 | (-1.0, i_min, d_min) 163 | } else { 164 | (t_min, i_min, d_min) 165 | } 166 | } 167 | 168 | fn sort_projected_centroid(proj_centroid: F, items: &mut [I], axis: Axis) 169 | where F: Fn(&I, Axis) -> f32 170 | { 171 | items.sort_by(|a, b| 172 | proj_centroid(a, axis).partial_cmp(&proj_centroid(b, axis)).expect("centroid is NaN") 173 | ); 174 | } 175 | 176 | /* 177 | // old BVH build_rec based on median on longuest axis instead of SAH 178 | fn build_rec(&mut self, begin: usize, end: usize) -> BVH { 179 | let mut aabb = AABB::empty(); 180 | for &((i0, i1, i2), _) in &self.triangles[begin..end] { 181 | aabb.extend_point(self.vertices[i0]); 182 | aabb.extend_point(self.vertices[i1]); 183 | aabb.extend_point(self.vertices[i2]); 184 | } 185 | 186 | let n = end - begin; 187 | if n <= 8 { 188 | return BVH::Leaf(aabb, begin, end); 189 | } 190 | 191 | let split_axis = aabb.longuest_axis(); 192 | self.sort_projected_centroid(split_axis, begin, end); 193 | 194 | let mid = begin + (n + 1) / 2; 195 | let c1 = Box::new(self.build_rec(begin, mid)); 196 | let c2 = Box::new(self.build_rec(mid, end)); 197 | return BVH::Node(aabb, split_axis, c1, c2); 198 | } 199 | */ 200 | 201 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate rand; 2 | extern crate time; 3 | extern crate bincode; 4 | extern crate image; 5 | extern crate rayon; 6 | #[cfg(feature = "gui")] 7 | extern crate sdl2; 8 | #[macro_use] 9 | extern crate serde_derive; 10 | 11 | pub mod camera; 12 | pub mod geometry; 13 | pub mod light; 14 | pub mod material; 15 | pub mod math; 16 | pub mod mesh; 17 | pub mod primitive; 18 | pub mod scene; 19 | pub mod texture; 20 | pub mod obj; 21 | 22 | mod distribution; 23 | mod integrator; 24 | mod warp; 25 | mod bvh; 26 | 27 | use rand::{Rng, SeedableRng}; 28 | use time::PreciseTime; 29 | use rayon::prelude::*; 30 | 31 | use math::*; 32 | use scene::*; 33 | use camera::*; 34 | use texture::*; 35 | use integrator::estimate_radiance; 36 | 37 | static OUTPUT_FILE: &'static str = "/tmp/image.ppm"; 38 | 39 | pub fn render(scene: Scene, camera: Camera, spp: u32) { 40 | let (width, height) = camera.resolution(); 41 | let mut sum_rad = vec![Vec3::zero(); width * height]; 42 | 43 | println!("Start rendering with {} samples per pixel...", spp); 44 | let start = PreciseTime::now(); 45 | 46 | sum_rad.par_chunks_mut(width).enumerate().for_each(|(y, row)| { 47 | let mut rng: rand::XorShiftRng = rand::random(); 48 | for (x, p) in row.iter_mut().enumerate() { 49 | for _ in 0..spp { 50 | let ray = camera.make_ray((x, y), rng.gen(), rng.gen()); 51 | let v = estimate_radiance(&scene, ray, &mut rng); 52 | if !v.has_nan() { 53 | *p += v; 54 | } 55 | } 56 | } 57 | }); 58 | 59 | let end = PreciseTime::now(); 60 | let tot_s = start.to(end).num_milliseconds() as f32 / 1000.0; 61 | println!("Rendered {} spp in {:.3}s ({:.3}s per sample)", spp, tot_s, tot_s / spp as f32); 62 | 63 | write_ppm_srgb(OUTPUT_FILE, width, height, camera.tonemap, sum_rad.iter().map(|&sr| sr / spp as f32)); 64 | } 65 | 66 | pub fn render_preview(scene: Scene, camera: Camera) { 67 | #[cfg(feature = "gui")] 68 | render_preview_gui(scene, camera); 69 | #[cfg(not(feature = "gui"))] 70 | render_preview_file(scene, camera); 71 | } 72 | 73 | #[cfg(feature = "gui")] 74 | pub fn render_preview_gui(scene: Scene, camera: Camera) { 75 | use sdl2::pixels::PixelFormatEnum; 76 | use sdl2::event::Event; 77 | use sdl2::keyboard::Keycode; 78 | 79 | let mut camera = camera; 80 | let (width, height) = camera.resolution(); 81 | 82 | let mut sum_rad = vec![Vec3::zero(); width * height]; 83 | let mut spp = 0; 84 | 85 | let mut rngs: Vec = Vec::new(); 86 | for _ in 0..height { 87 | rngs.push(rand::random()); 88 | } 89 | 90 | let mut tonemapped: Vec = vec![0; width * height * 3]; 91 | 92 | // init a SDL window and a texture 93 | let sdl_context = sdl2::init().unwrap(); 94 | let video_subsystem = sdl_context.video().unwrap(); 95 | let window = video_subsystem.window("tracing", width as u32, height as u32).build().unwrap(); 96 | let mut event_pump = sdl_context.event_pump().unwrap(); 97 | let mut canvas = window.into_canvas().build().unwrap(); 98 | let texture_creator = canvas.texture_creator(); 99 | let mut texture = texture_creator.create_texture_streaming(PixelFormatEnum::RGB24, width as u32, height as u32).unwrap(); 100 | 101 | println!("Start rendering..."); 102 | let mut start = PreciseTime::now(); 103 | 104 | 'rendering: loop { 105 | // render a new frame 106 | sum_rad.par_chunks_mut(width).enumerate().zip(rngs.par_iter_mut()).for_each(|((y, row), rng)| { 107 | for (x, p) in row.iter_mut().enumerate() { 108 | let ray = camera.make_ray((x, y), rng.gen(), rng.gen()); 109 | let v = estimate_radiance(&scene, ray, rng); 110 | if !v.has_nan() { 111 | *p += v; 112 | } 113 | } 114 | }); 115 | spp += 1; 116 | //println!("{} spp", spp); 117 | 118 | // tonemap the current data and display it 119 | sum_rad.par_iter().zip(tonemapped.par_chunks_mut(3)).for_each(|(&sr, tm)| { 120 | let v = (camera.tonemap)(sr / spp as f32).map(|x| x * 255.0 + 0.5); 121 | tm[0] = v.x as u8; 122 | tm[1] = v.y as u8; 123 | tm[2] = v.z as u8; 124 | }); 125 | texture.update(None, &tonemapped, width * 3).unwrap(); 126 | canvas.copy(&texture, None, None).unwrap(); 127 | canvas.present(); 128 | canvas.window_mut().set_title(&format!("tracing - {} spp", spp)).unwrap(); 129 | 130 | // process sdl events 131 | for event in event_pump.poll_iter() { 132 | match event { 133 | Event::Quit {..} 134 | //| Event::KeyDown { keycode: Some(Keycode::Escape), .. } 135 | | Event::KeyDown { keycode: Some(Keycode::Q), .. } => { 136 | break 'rendering; 137 | } 138 | //Event::Window { win_event: sdl2::event::WindowEvent::Exposed, .. } => { 139 | // canvas.copy(&texture, None, None).unwrap(); 140 | // canvas.present(); 141 | //} 142 | Event::MouseButtonDown { x, y, .. } => { 143 | let ray = camera.make_ray((x as usize, y as usize), (0.0, 0.0), (0.0, 0.0)); 144 | match scene.intersect(ray) { 145 | Some(Hit::Scatterer(its, _)) => { 146 | println!("restarting with focal distance = {}", its.distance); 147 | camera.set_focus_dist(Some(its.distance)); 148 | for sr in &mut sum_rad { 149 | *sr = Vec3::zero(); 150 | } 151 | spp = 0; 152 | start = PreciseTime::now(); 153 | } 154 | _ => println!("no intersection"), 155 | } 156 | } 157 | _ => {} 158 | }; 159 | } 160 | } 161 | 162 | let end = PreciseTime::now(); 163 | let tot_s = start.to(end).num_milliseconds() as f32 / 1000.0; 164 | println!("Rendered {} spp in {:.3}s ({:.3}s per sample)", spp, tot_s, tot_s / spp as f32); 165 | 166 | write_ppm_srgb(OUTPUT_FILE, width, height, camera.tonemap, sum_rad.iter().map(|&sr| sr / spp as f32)); 167 | } 168 | 169 | pub fn render_preview_file(scene: Scene, camera: Camera) { 170 | let camera = camera; 171 | let (width, height) = camera.resolution(); 172 | 173 | let mut sum_rad = vec![Vec3::zero(); width * height]; 174 | let mut spp = 0; 175 | 176 | let mut rngs: Vec = Vec::new(); 177 | for _ in 0..height { 178 | rngs.push(rand::random()); 179 | } 180 | 181 | let mut tonemapped: Vec = vec![0; width * height * 3]; 182 | 183 | println!("Start rendering..."); 184 | let start = PreciseTime::now(); 185 | 186 | const SPP_STEP: usize = 16; 187 | 188 | 'rendering: loop { 189 | // render a new frame 190 | sum_rad.par_chunks_mut(width).enumerate().zip(rngs.par_iter_mut()).for_each(|((y, row), rng)| { 191 | let mut local_rng = rng.clone(); 192 | for (x, p) in row.iter_mut().enumerate() { 193 | for _ in 0..SPP_STEP { 194 | let ray = camera.make_ray((x, y), local_rng.gen(), local_rng.gen()); 195 | let v = estimate_radiance(&scene, ray, &mut local_rng); 196 | if !v.has_nan() { 197 | *p += v; 198 | } 199 | } 200 | } 201 | *rng = local_rng.clone(); 202 | }); 203 | spp += SPP_STEP; 204 | 205 | // tonemap the current data and dump it 206 | sum_rad.par_iter().zip(tonemapped.par_chunks_mut(3)).for_each(|(&sr, tm)| { 207 | let v = (camera.tonemap)(sr / spp as f32).map(|x| x * 255.0 + 0.5); 208 | tm[0] = v.x as u8; 209 | tm[1] = v.y as u8; 210 | tm[2] = v.z as u8; 211 | }); 212 | write_ppm_raw(OUTPUT_FILE, width, height, &tonemapped); 213 | 214 | let end = PreciseTime::now(); 215 | let tot_s = start.to(end).num_milliseconds() as f32 / 1000.0; 216 | println!("Rendered {} spp in {:.3}s ({:.3}s per sample)", spp, tot_s, tot_s / spp as f32); 217 | } 218 | } 219 | 220 | pub fn render_seq(scene: Scene, camera: Camera, spp: u32) { 221 | let (width, height) = camera.resolution(); 222 | let mut sum_rad = vec![Vec3::zero(); width * height]; 223 | //let mut rng: rand::XorShiftRng = rand::random(); 224 | let mut rng = rand::XorShiftRng::from_seed([0u32, 1u32, 2u32, 3u32]); 225 | 226 | println!("Start rendering with {} samples per pixel...", spp); 227 | let start = PreciseTime::now(); 228 | 229 | sum_rad.chunks_mut(width).enumerate().for_each(|(y, row)| { 230 | for (x, p) in row.iter_mut().enumerate() { 231 | for _ in 0..spp { 232 | let ray = camera.make_ray((x, y), rng.gen(), rng.gen()); 233 | let v = estimate_radiance(&scene, ray, &mut rng); 234 | if !v.has_nan() { 235 | *p += v; 236 | } 237 | } 238 | } 239 | }); 240 | 241 | let end = PreciseTime::now(); 242 | let tot_s = start.to(end).num_milliseconds() as f32 / 1000.0; 243 | println!("Rendered {} spp in {:.3}s ({:.3}s per sample)", spp, tot_s, tot_s / spp as f32); 244 | 245 | write_ppm_srgb(OUTPUT_FILE, width, height, camera.tonemap, sum_rad.iter().map(|&sr| sr / spp as f32)); 246 | } 247 | -------------------------------------------------------------------------------- /src/bin/render_tungsten.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate serde_derive; 3 | extern crate serde_json; 4 | extern crate tracing; 5 | 6 | use std::fs::File; 7 | use std::path::Path; 8 | use std::io::BufReader; 9 | use std::collections::HashMap; 10 | use std::sync::Arc; 11 | 12 | use tracing::{math, scene, camera, material, texture, primitive, obj, light}; 13 | 14 | #[derive(Deserialize, Debug)] 15 | #[serde(untagged)] 16 | enum Vec3 { 17 | Thrice(f32), 18 | Explicit(f32, f32, f32), 19 | } 20 | 21 | #[derive(Deserialize, Debug)] 22 | struct Scene { 23 | bsdfs: Vec, 24 | primitives: Vec, 25 | camera: Camera, 26 | } 27 | 28 | #[derive(Deserialize, Debug)] 29 | #[serde(tag = "type", rename_all = "snake_case")] 30 | enum Bsdf { 31 | Lambert { name: String, albedo: Texture }, 32 | Mirror { name: String, albedo: Texture }, 33 | Conductor { name: String, albedo: Texture, material: String }, 34 | Plastic { name: String, albedo: Texture, ior: f32 }, 35 | Dielectric { name: String, albedo: Texture, ior: f32 }, 36 | RoughDielectric { name: String, albedo: Texture, ior: f32, roughness: Texture }, 37 | RoughConductor { name: String, albedo: Texture, material: String, roughness: Texture }, 38 | RoughPlastic { name: String, albedo: Texture, ior: f32, roughness: Texture }, 39 | SmoothCoat { name: String, ior: f32, sigma_a: Vec3, thickness: f32, substrate: Box }, 40 | Transparency { name: String }, 41 | Thinsheet { name: String, albedo: Texture, ior: f32 }, 42 | Null { name: String }, 43 | } 44 | 45 | #[derive(Deserialize, Debug)] 46 | #[serde(untagged)] 47 | enum BsdfRef { 48 | Bsdf(Bsdf), 49 | Ref(String), 50 | } 51 | 52 | #[derive(Deserialize, Debug)] 53 | #[serde(untagged)] 54 | enum Texture { 55 | Constant(Vec3), 56 | Procedural(ProceduralTexture), 57 | Bitmap(String), 58 | } 59 | 60 | #[derive(Deserialize, Debug)] 61 | #[serde(tag = "type", rename_all = "snake_case")] 62 | enum ProceduralTexture { 63 | Checker { on_color: Vec3, off_color: Vec3, res_u: f32, res_v: f32 }, 64 | } 65 | 66 | #[derive(Deserialize, Debug)] 67 | #[serde(tag = "type", rename_all = "snake_case")] 68 | enum Primitive { 69 | Quad { bsdf: BsdfRef, transform: Transform, emission: Option }, 70 | Mesh { bsdf: BsdfRef, transform: Transform, file: String }, 71 | InfiniteSphere { transform: Transform, emission: String }, 72 | } 73 | 74 | #[derive(Deserialize, Debug)] 75 | #[serde(untagged)] 76 | enum Transform { 77 | LookAt { position: Vec3, look_at: Vec3, up: Vec3 }, 78 | Normal { position: Option, scale: Option, rotation: Option }, 79 | } 80 | 81 | #[derive(Deserialize, Debug)] 82 | struct Camera { 83 | resolution: Resolution, 84 | transform: Transform, 85 | fov: f32, 86 | tonemap: Tonemap, 87 | aperture_size: Option, 88 | focus_distance: Option, 89 | } 90 | 91 | #[derive(Deserialize, Debug)] 92 | #[serde(untagged)] 93 | enum Resolution { 94 | Rect(usize, usize), 95 | Square(usize), 96 | } 97 | 98 | #[derive(Deserialize, Debug)] 99 | #[serde(rename_all = "snake_case")] 100 | enum Tonemap { 101 | Gamma, 102 | Filmic, 103 | } 104 | 105 | impl Vec3 { 106 | fn convert(self) -> math::Vec3 { 107 | match self { 108 | Vec3::Thrice(v) => math::Vec3::thrice(v), 109 | Vec3::Explicit(x, y, z) => math::Vec3 { x, y, z }, 110 | } 111 | } 112 | } 113 | 114 | impl Scene { 115 | fn convert(self, dir: &Path) -> (scene::Scene, camera::Camera) { 116 | let mut bsdfs = HashMap::new(); 117 | for b in self.bsdfs.into_iter() { 118 | let (name, mat) = b.convert(dir, &bsdfs); 119 | bsdfs.insert(name, mat); 120 | } 121 | let mut objects = Vec::new(); 122 | let mut envmap = None; 123 | for p in self.primitives { 124 | p.convert(dir, &bsdfs, &mut objects, &mut envmap); 125 | } 126 | (scene::Scene::new(envmap, objects), self.camera.convert()) 127 | } 128 | } 129 | 130 | impl Bsdf { 131 | fn convert(self, dir: &Path, bsdfs: &HashMap>) -> (String, Arc) { 132 | match self { 133 | Bsdf::Lambert { name, albedo } => { 134 | (name.clone(), Arc::new(material::Diffuse { 135 | albedo: albedo.convert(dir), 136 | })) 137 | } 138 | Bsdf::RoughPlastic { name, albedo, ior, roughness } => { 139 | (name.clone(), Arc::new(material::RoughPlastic::new( 140 | albedo.convert(dir), 141 | ior, 142 | roughness.convert(dir), 143 | ))) 144 | } 145 | Bsdf::Mirror { name, albedo } => { 146 | (name.clone(), Arc::new(material::Mirror { 147 | albedo: albedo.convert(dir), 148 | })) 149 | } 150 | Bsdf::Conductor { name, albedo, material } => { 151 | (name.clone(), material::Conductor::from_symbol(&material, albedo.convert(dir)).unwrap()) 152 | } 153 | Bsdf::RoughConductor { name, albedo, material, roughness } => { 154 | (name.clone(), material::RoughConductor::from_symbol(&material, albedo.convert(dir), roughness.convert(dir)).unwrap()) 155 | } 156 | Bsdf::Plastic { name, albedo, ior } => { 157 | (name.clone(), Arc::new(material::Plastic::new(albedo.convert(dir), ior))) 158 | } 159 | Bsdf::Dielectric { name, albedo, ior } => { 160 | (name.clone(), Arc::new(material::Dielectric { 161 | albedo: albedo.convert(dir), 162 | ior, 163 | })) 164 | } 165 | Bsdf::RoughDielectric { name, albedo, ior, roughness } => { 166 | (name.clone(), Arc::new(material::RoughDielectric { 167 | albedo: albedo.convert(dir), 168 | ior, 169 | roughness: roughness.convert(dir), 170 | })) 171 | } 172 | Bsdf::Thinsheet { name, albedo, ior } => { 173 | // TODO: real thin sheet 174 | (name.clone(), Arc::new(material::Dielectric { 175 | albedo: albedo.convert(dir), 176 | ior, 177 | })) 178 | } 179 | Bsdf::Null { name } => { 180 | // TODO: real null bsdf 181 | (name.clone(), Arc::new(material::Diffuse { 182 | albedo: texture::Texture::Constant(math::Vec3::zero()), 183 | })) 184 | } 185 | 186 | Bsdf::SmoothCoat { name, ior, sigma_a, thickness, substrate } => { 187 | (name.clone(), Arc::new(material::SmoothCoat { 188 | ior, 189 | scaled_sigma_a: sigma_a.convert() * thickness, 190 | substrate: substrate.convert(dir, bsdfs), 191 | })) 192 | } 193 | Bsdf::Transparency { name } => { 194 | // TODO: add material 195 | (name.clone(), Arc::new(material::Diffuse { 196 | albedo: texture::Texture::Constant(math::Vec3::thrice(0.5)), 197 | })) 198 | } 199 | } 200 | } 201 | } 202 | 203 | impl BsdfRef { 204 | fn convert(self, dir: &Path, bsdfs: &HashMap>) -> Arc { 205 | // all diffuse white 206 | //return Arc::new(material::Material { 207 | // texture: texture::Texture::Constant(math::Vec3::thrice(0.5)), 208 | // bsdf: material::BSDF::Diffuse, 209 | //}); 210 | 211 | match self { 212 | BsdfRef::Ref(name) => bsdfs.get(&name).unwrap().clone(), 213 | BsdfRef::Bsdf(bsdf) => bsdf.convert(dir, bsdfs).1, 214 | } 215 | } 216 | } 217 | 218 | impl Texture { 219 | fn convert(self, dir: &Path) -> texture::Texture { 220 | match self { 221 | Texture::Constant(v) => texture::Texture::Constant(v.convert()), 222 | Texture::Procedural(ProceduralTexture::Checker { on_color, off_color, res_u, res_v }) => { 223 | texture::Texture::Checker { 224 | on_color: on_color.convert(), 225 | off_color: off_color.convert(), 226 | resolution: (res_u, res_v) 227 | } 228 | } 229 | Texture::Bitmap(file) => texture::Texture::Bitmap(texture::Image::load_ldr(dir.join(&file))), 230 | } 231 | } 232 | } 233 | 234 | impl Primitive { 235 | fn convert(self, dir: &Path, bsdfs: &HashMap>, objects: &mut Vec, envmap: &mut Option) { 236 | match self { 237 | Primitive::Quad { bsdf, transform, emission: None } => { 238 | objects.push(scene::Object::Scatterer { 239 | surface: Box::new(primitive::Parallelogram::unit_transform(&transform.convert())), 240 | material: bsdf.convert(dir, bsdfs), 241 | }) 242 | } 243 | Primitive::Quad { bsdf: _, transform, emission: Some(v) } => { 244 | objects.push(scene::Object::Emitter(light::AreaLight { 245 | surface: Box::new(primitive::Parallelogram::unit_transform(&transform.convert())), 246 | emission: v.convert(), 247 | })) 248 | } 249 | Primitive::Mesh { bsdf, transform, file } => { 250 | let mat = bsdf.convert(dir, bsdfs); 251 | for (_, mesh) in obj::load(dir.join(&file), &transform.convert()) { 252 | objects.push(scene::Object::Scatterer { 253 | surface: Box::new(mesh), 254 | material: mat.clone(), 255 | }) 256 | } 257 | } 258 | Primitive::InfiniteSphere { transform, emission } => { 259 | let hdr = texture::Image::load_hdr(dir.join(&emission)); 260 | *envmap = Some(light::EnvMap::from_image(hdr, &transform.convert())) 261 | } 262 | } 263 | } 264 | } 265 | 266 | impl Transform { 267 | fn convert(self) -> math::Mat4 { 268 | match self { 269 | Transform::LookAt { position, look_at, up } => { 270 | math::Mat4::look_at(position.convert(), look_at.convert(), up.convert()) 271 | } 272 | Transform::Normal { position, scale, rotation } => { 273 | let mut transform = math::Mat4::identity(); 274 | if let Some(v) = position { 275 | transform = transform * math::Mat4::translate(v.convert()) 276 | } 277 | if let Some(v) = rotation { 278 | transform = transform * math::Mat4::rot_yxz(v.convert()) 279 | } 280 | if let Some(v) = scale { 281 | transform = transform * math::Mat4::scale(v.convert()) 282 | } 283 | transform 284 | } 285 | } 286 | } 287 | } 288 | 289 | impl Camera { 290 | fn convert(self) -> camera::Camera { 291 | camera::Camera::new( 292 | &self.transform.convert(), 293 | self.resolution.convert(), 294 | self.fov, 295 | self.tonemap.convert(), 296 | self.aperture_size, 297 | self.focus_distance, 298 | ) 299 | } 300 | } 301 | 302 | impl Resolution { 303 | fn convert(self) -> (usize, usize) { 304 | match self { 305 | Resolution::Rect(w, h) => (w, h), 306 | Resolution::Square(w) => (w, w), 307 | } 308 | } 309 | } 310 | 311 | impl Tonemap { 312 | fn convert(self) -> camera::Tonemap { 313 | match self { 314 | Tonemap::Gamma => camera::gamma, 315 | Tonemap::Filmic => camera::filmic, 316 | } 317 | } 318 | } 319 | 320 | fn main() { 321 | let args = std::env::args().collect::>(); 322 | 323 | if args.len() < 2 { 324 | eprintln!("usage: {} tungsten_scene.json", args[0]); 325 | std::process::exit(1); 326 | } 327 | 328 | let path = Path::new(&args[1]); 329 | let file = BufReader::new(File::open(&path).unwrap()); 330 | let tungsten_scene: Scene = serde_json::from_reader(file).unwrap(); 331 | //println!("{:?}", &tungsten_scene); 332 | 333 | let (scene, camera) = tungsten_scene.convert(path.parent().unwrap()); 334 | 335 | tracing::render_preview(scene, camera); 336 | //tracing::render(scene, camera, 16); 337 | } 338 | -------------------------------------------------------------------------------- /src/material.rs: -------------------------------------------------------------------------------- 1 | use texture::Texture; 2 | use math::*; 3 | use warp::*; 4 | use std::sync::Arc; 5 | 6 | pub struct BSDFSample { 7 | pub direction: Vec3, 8 | pub pdf: f32, 9 | /// Importance weight of the sample (i.e. the value of the BSDF divided by 10 | /// the probability density) multiplied by the cosine falloff factor with 11 | /// respect to the sampled direction 12 | pub weight: Vec3, 13 | pub is_specular: bool, 14 | } 15 | 16 | const NULL_SAMPLE: BSDFSample = BSDFSample { 17 | direction: Vec3 { x: 0.0, y: 0.0, z: 0.0 }, 18 | pdf: 0.0, 19 | weight: Vec3 { x: 0.0, y: 0.0, z: 0.0 }, 20 | is_specular: false, 21 | }; 22 | 23 | pub trait Material: Sync + Send { 24 | fn sample(&self, _dir_in: Vec3, _uv: (f32, f32), _rnd: Vec3) -> BSDFSample; 25 | 26 | fn eval(&self, _dir_in: Vec3, _dir_out: Vec3, _uv: (f32, f32)) -> Vec3 { 27 | Vec3::zero() 28 | } 29 | 30 | fn pdf(&self, _dir_in: Vec3, _dir_out: Vec3, _uv: (f32, f32)) -> f32 { 31 | 0.0 32 | } 33 | 34 | fn is_purely_specular(&self) -> bool { 35 | true 36 | } 37 | } 38 | 39 | #[derive(Clone, Copy)] 40 | pub struct ComplexIOR { 41 | pub eta: Vec3, 42 | pub k: Vec3, 43 | } 44 | 45 | pub struct Diffuse { 46 | pub albedo: Texture, 47 | } 48 | 49 | impl Material for Diffuse { 50 | fn sample(&self, dir_in: Vec3, uv: (f32, f32), rnd: Vec3) -> BSDFSample { 51 | if -cos_theta(dir_in) <= 0.0 { 52 | return NULL_SAMPLE; 53 | } 54 | 55 | let d = cosine_hemisphere((rnd.x, rnd.y)); 56 | 57 | BSDFSample { 58 | direction: d, 59 | pdf: cosine_hemisphere_pdf(d), 60 | weight: self.albedo.eval(uv), 61 | is_specular: false, 62 | } 63 | } 64 | 65 | fn eval(&self, _dir_in: Vec3, dir_out: Vec3, uv: (f32, f32)) -> Vec3 { 66 | self.albedo.eval(uv) * (INV_PI * cos_theta(dir_out).max(0.0)) 67 | } 68 | 69 | fn pdf(&self, _dir_in: Vec3, dir_out: Vec3, _uv: (f32, f32)) -> f32 { 70 | INV_PI * cos_theta(dir_out).max(0.0) 71 | } 72 | 73 | fn is_purely_specular(&self) -> bool { 74 | false 75 | } 76 | } 77 | 78 | pub struct Mirror { 79 | pub albedo: Texture, 80 | } 81 | 82 | impl Material for Mirror { 83 | fn sample(&self, dir_in: Vec3, uv: (f32, f32), _rnd: Vec3) -> BSDFSample { 84 | if -cos_theta(dir_in) <= 0.0 { 85 | return NULL_SAMPLE; 86 | } 87 | 88 | BSDFSample { 89 | direction: reflect(dir_in), 90 | pdf: 1.0, 91 | weight: self.albedo.eval(uv), 92 | is_specular: true, 93 | } 94 | } 95 | } 96 | 97 | pub struct Dielectric { 98 | pub albedo: Texture, 99 | pub ior: f32, 100 | } 101 | 102 | impl Material for Dielectric { 103 | fn sample(&self, dir_in: Vec3, uv: (f32, f32), rnd: Vec3) -> BSDFSample { 104 | let eta = if cos_theta(dir_in) >= 0.0 { self.ior } else { 1.0 / self.ior }; 105 | let cos_i = cos_theta(dir_in).abs(); 106 | let (reflectance, cos_t) = fresnel::dielectric_reflectance(eta, cos_i); 107 | 108 | let (direction, pdf) = if rnd.z < reflectance { 109 | (reflect(dir_in), reflectance) 110 | } else { 111 | (refract(dir_in, eta, cos_t), 1.0 - reflectance) 112 | }; 113 | 114 | BSDFSample { 115 | direction, 116 | pdf, 117 | weight: self.albedo.eval(uv), 118 | is_specular: true, 119 | } 120 | } 121 | } 122 | 123 | pub struct RoughDielectric { 124 | /// Material albedo; should be set to `1.0` for physical accuracy 125 | pub albedo: Texture, 126 | /// Index of Refraction 127 | pub ior: f32, 128 | pub roughness: Texture, 129 | } 130 | 131 | impl Material for RoughDielectric { 132 | fn sample(&self, dir_in: Vec3, uv: (f32, f32), rnd: Vec3) -> BSDFSample { 133 | let eta = if cos_theta(dir_in) >= 0.0 { self.ior } else { 1.0 / self.ior }; 134 | let cos_i = cos_theta(dir_in).abs(); 135 | 136 | let roughness = self.roughness.eval(uv).avg(); 137 | let sample_roughness = (1.2 - 0.2 * cos_i.sqrt()) * roughness; 138 | 139 | let (h, ggx_pdf) = microfacet::sample(sample_roughness, (rnd.x, rnd.y)); 140 | 141 | if ggx_pdf < 1e-10 { 142 | return NULL_SAMPLE; 143 | } 144 | 145 | let in_dot_h = -Vec3::dot(dir_in, h); 146 | let eta_h = if in_dot_h < 0.0 { self.ior } else { 1.0 / self.ior }; 147 | let (f, cos_t) = fresnel::dielectric_reflectance(eta_h, in_dot_h.abs()); 148 | 149 | let is_reflection = rnd.z < f; 150 | let direction = if is_reflection { 151 | 2.0 * in_dot_h * h + dir_in 152 | } else { 153 | (eta_h * in_dot_h - in_dot_h.signum() * cos_t) * h - eta_h * -dir_in 154 | }.normalized(); 155 | 156 | let reflected = cos_theta(dir_in) * cos_theta(direction) <= 0.0; 157 | if reflected != is_reflection { 158 | return NULL_SAMPLE; 159 | } 160 | 161 | let out_dot_h = Vec3::dot(direction, h); 162 | let g = microfacet::shadowing(roughness, dir_in, direction, h); 163 | let d = microfacet::distribution(roughness, cos_theta(h)); 164 | let weight = Vec3::thrice(in_dot_h.abs() * d * g / (ggx_pdf * cos_i)); 165 | 166 | let pdf = if is_reflection { 167 | ggx_pdf / (4.0 * in_dot_h.abs()) * f 168 | } else { 169 | let x = eta * in_dot_h + out_dot_h; 170 | ggx_pdf * out_dot_h.abs() / (x * x) * (1.0 - f) 171 | }; 172 | 173 | BSDFSample { 174 | direction, 175 | pdf, 176 | weight: weight * self.albedo.eval(uv), 177 | is_specular: false, 178 | } 179 | } 180 | 181 | fn eval(&self, dir_in: Vec3, dir_out: Vec3, uv: (f32, f32)) -> Vec3 { 182 | let cos_i = -cos_theta(dir_in); 183 | let cos_o = cos_theta(dir_out); 184 | 185 | let is_reflection = cos_i * cos_o >= 0.0; 186 | let roughness = self.roughness.eval(uv).avg(); 187 | let eta = if cos_i < 0.0 { self.ior } else { 1.0 / self.ior }; 188 | 189 | let h = if is_reflection { 190 | cos_i.signum() * (-dir_in + dir_out).normalized() 191 | } else { 192 | -(-dir_in*eta + dir_out).normalized() 193 | }; 194 | let in_dot_h = -Vec3::dot(dir_in, h); 195 | let out_dot_h = Vec3::dot(dir_out, h); 196 | let eta_h = if in_dot_h < 0.0 { self.ior } else { 1.0 / self.ior }; 197 | 198 | let (f, _) = fresnel::dielectric_reflectance(eta_h, in_dot_h.abs()); 199 | let g = microfacet::shadowing(roughness, dir_in, dir_out, h); 200 | let d = microfacet::distribution(roughness, cos_theta(h)); 201 | 202 | let r = if is_reflection { 203 | (f * g * d) / (4.0 * cos_i.abs()) 204 | } else { 205 | let x = eta * in_dot_h + out_dot_h; 206 | (in_dot_h * out_dot_h).abs() * (1.0 - f) * g * d / (x * x * cos_i.abs()) 207 | }; 208 | r * self.albedo.eval(uv) 209 | } 210 | 211 | fn pdf(&self, dir_in: Vec3, dir_out: Vec3, uv: (f32, f32)) -> f32 { 212 | let cos_i = -cos_theta(dir_in); 213 | let cos_o = cos_theta(dir_out); 214 | 215 | let is_reflection = cos_i * cos_o >= 0.0; 216 | let roughness = self.roughness.eval(uv).avg(); 217 | let sample_roughness = (1.2 - 0.2 * cos_i.abs().sqrt()) * roughness; 218 | let eta = if cos_i < 0.0 { self.ior } else { 1.0 / self.ior }; 219 | 220 | let h = if is_reflection { 221 | cos_i.signum() * (-dir_in + dir_out).normalized() 222 | } else { 223 | -(-dir_in*eta + dir_out).normalized() 224 | }; 225 | let in_dot_h = -Vec3::dot(dir_in, h); 226 | let out_dot_h = Vec3::dot(dir_out, h); 227 | let eta_h = if in_dot_h < 0.0 { self.ior } else { 1.0 / self.ior }; 228 | 229 | let (f, _) = fresnel::dielectric_reflectance(eta_h, in_dot_h.abs()); 230 | let ggx_pdf = microfacet::pdf(sample_roughness, cos_theta(h)); 231 | 232 | if is_reflection { 233 | ggx_pdf / (4.0 * in_dot_h.abs()) * f 234 | } else { 235 | let x = eta * in_dot_h + out_dot_h; 236 | ggx_pdf * out_dot_h.abs() / (x * x) * (1.0 - f) 237 | } 238 | } 239 | 240 | fn is_purely_specular(&self) -> bool { 241 | false 242 | } 243 | } 244 | 245 | pub struct Plastic { 246 | albedo: Texture, 247 | ior: f32, 248 | } 249 | 250 | impl Plastic { 251 | pub fn new(albedo: Texture, ior: f32) -> Plastic { 252 | Plastic { 253 | albedo, 254 | ior, 255 | } 256 | } 257 | } 258 | 259 | impl Material for Plastic { 260 | fn eval(&self, dir_in: Vec3, dir_out: Vec3, uv: (f32, f32)) -> Vec3 { 261 | let cos_i = -cos_theta(dir_in); 262 | let cos_o = cos_theta(dir_out); 263 | 264 | if cos_i <= 0.0 || cos_o <= 0.0 { 265 | return Vec3::zero(); 266 | } 267 | 268 | let eta = 1.0 / self.ior; 269 | let (f, _) = fresnel::dielectric_reflectance(eta, cos_i); 270 | 271 | self.albedo.eval(uv) * (INV_PI * cos_o * (1.0 - f)) 272 | } 273 | 274 | fn sample(&self, dir_in: Vec3, uv: (f32, f32), rnd: Vec3) -> BSDFSample { 275 | let cos_i = -cos_theta(dir_in); 276 | if cos_i <= 0.0 { 277 | return NULL_SAMPLE; 278 | } 279 | 280 | // Chose between specular and diffuse reflection according to fresnel reflectance. 281 | let eta = 1.0 / self.ior; 282 | let (f, _) = fresnel::dielectric_reflectance(eta, cos_i); 283 | let spec_prob = f; 284 | 285 | if rnd.z < spec_prob { 286 | // TODO: shouldn't we account for the probably of sampling that 287 | // direction with diffuse reflection? 288 | // - Seems to give incorrect results 289 | // - Reference implementations like Mitsuba do not 290 | 291 | BSDFSample { 292 | direction: reflect(dir_in), 293 | pdf: spec_prob, 294 | weight: Vec3::thrice(1.0), 295 | is_specular: true, 296 | } 297 | } else { 298 | let direction = cosine_hemisphere((rnd.x, rnd.y)); 299 | 300 | BSDFSample { 301 | direction, 302 | pdf: cosine_hemisphere_pdf(direction) * (1.0 - spec_prob), 303 | weight: self.albedo.eval(uv), 304 | is_specular: false, 305 | } 306 | } 307 | } 308 | 309 | fn pdf(&self, dir_in: Vec3, dir_out: Vec3, _uv: (f32, f32)) -> f32 { 310 | let cos_i = -cos_theta(dir_in); 311 | let cos_o = cos_theta(dir_out); 312 | 313 | if cos_i <= 0.0 || cos_o <= 0.0 { 314 | return 0.0; 315 | } 316 | 317 | let eta = 1.0 / self.ior; 318 | let (f, _) = fresnel::dielectric_reflectance(eta, cos_i); 319 | let spec_prob = f; 320 | 321 | cosine_hemisphere_pdf(dir_out) * (1.0 - spec_prob) 322 | } 323 | 324 | fn is_purely_specular(&self) -> bool { 325 | false 326 | } 327 | } 328 | 329 | pub struct RoughPlastic { 330 | albedo: Texture, 331 | ior: f32, 332 | roughness: Texture, 333 | } 334 | 335 | impl RoughPlastic { 336 | pub fn new(albedo: Texture, ior: f32, roughness: Texture) -> RoughPlastic { 337 | RoughPlastic { 338 | albedo, 339 | ior, 340 | roughness, 341 | } 342 | } 343 | } 344 | 345 | impl Material for RoughPlastic { 346 | fn eval(&self, dir_in: Vec3, dir_out: Vec3, uv: (f32, f32)) -> Vec3 { 347 | let cos_i = -cos_theta(dir_in); 348 | let cos_o = cos_theta(dir_out); 349 | 350 | if cos_i <= 0.0 || cos_o <= 0.0 { 351 | return Vec3::zero(); 352 | } 353 | 354 | let roughness = self.roughness.eval(uv).avg(); 355 | let h = (-dir_in + dir_out).normalized(); 356 | let eta = 1.0 / self.ior; 357 | 358 | let (f, _) = fresnel::dielectric_reflectance(eta, -Vec3::dot(dir_in, h)); 359 | let d = microfacet::distribution(roughness, cos_theta(h)); 360 | let g = microfacet::shadowing(roughness, dir_in, dir_out, h); 361 | 362 | let spec_brdf = Vec3::thrice((f * d * g) / (4.0 * cos_i)); 363 | let diff_brdf = self.albedo.eval(uv) * (INV_PI * cos_o * (1.0 - f)); 364 | 365 | spec_brdf + diff_brdf 366 | } 367 | 368 | fn sample(&self, dir_in: Vec3, uv: (f32, f32), rnd: Vec3) -> BSDFSample { 369 | let cos_i = -cos_theta(dir_in); 370 | if cos_i <= 0.0 { 371 | return NULL_SAMPLE; 372 | } 373 | 374 | // Uniformly chose between diffuse and glossy reflection. 375 | // For some reason, it seems to give better results than sampling 376 | // according to fresnel reflectance. 377 | let spec_prob = 0.5; 378 | 379 | let direction = if rnd.z < spec_prob { 380 | let roughness = self.roughness.eval(uv).avg(); 381 | let (h, _) = microfacet::sample(roughness, (rnd.x, rnd.y)); 382 | let direction = (2.0 * -Vec3::dot(dir_in, h) * h + dir_in).normalized(); 383 | 384 | if cos_theta(direction) <= 0.0 { 385 | return NULL_SAMPLE; 386 | } 387 | 388 | direction 389 | } else { 390 | cosine_hemisphere((rnd.x, rnd.y)) 391 | }; 392 | 393 | let pdf = self.pdf(dir_in, direction, uv); 394 | 395 | BSDFSample { 396 | direction, 397 | pdf, 398 | weight: self.eval(dir_in, direction, uv) / pdf, 399 | is_specular: false, 400 | } 401 | } 402 | 403 | fn pdf(&self, dir_in: Vec3, dir_out: Vec3, uv: (f32, f32)) -> f32 { 404 | let cos_i = -cos_theta(dir_in); 405 | let cos_o = cos_theta(dir_out); 406 | 407 | if cos_i <= 0.0 || cos_o <= 0.0 { 408 | return 0.0; 409 | } 410 | 411 | let spec_prob = 0.5; 412 | 413 | let roughness = self.roughness.eval(uv).avg(); 414 | let h = (-dir_in + dir_out).normalized(); 415 | 416 | let spec_pdf = microfacet::pdf(roughness, cos_theta(h)) / (4.0 * Vec3::dot(dir_out, h)); 417 | let diff_pdf = cosine_hemisphere_pdf(dir_out); 418 | 419 | spec_pdf * spec_prob + diff_pdf * (1.0 - spec_prob) 420 | } 421 | 422 | fn is_purely_specular(&self) -> bool { 423 | false 424 | } 425 | } 426 | 427 | pub struct Conductor { 428 | pub albedo: Texture, 429 | pub ior: ComplexIOR, 430 | } 431 | 432 | 433 | impl Conductor { 434 | pub fn from_symbol(symbol: &str, albedo: Texture) -> Option> { 435 | let &(_, ior) = CONDUCTORS_IOR.iter().find(|t| t.0 == symbol)?; 436 | Some(Arc::new(Conductor { albedo, ior })) 437 | } 438 | } 439 | 440 | impl Material for Conductor { 441 | fn sample(&self, dir_in: Vec3, uv: (f32, f32), _rnd: Vec3) -> BSDFSample { 442 | let cos_i = -cos_theta(dir_in); 443 | 444 | if cos_i <= 0.0 { 445 | return NULL_SAMPLE; 446 | } 447 | 448 | BSDFSample { 449 | direction: reflect(dir_in), 450 | pdf: 1.0, 451 | weight: self.albedo.eval(uv) * fresnel::conductor_reflectance_rgb(self.ior, cos_i), 452 | is_specular: true, 453 | } 454 | } 455 | } 456 | 457 | pub struct RoughConductor { 458 | pub albedo: Texture, 459 | pub ior: ComplexIOR, 460 | pub roughness: Texture, 461 | } 462 | 463 | impl RoughConductor { 464 | pub fn from_symbol(symbol: &str, albedo: Texture, roughness: Texture) -> Option> { 465 | let &(_, ior) = CONDUCTORS_IOR.iter().find(|t| t.0 == symbol)?; 466 | Some(Arc::new(RoughConductor { albedo, ior, roughness })) 467 | } 468 | } 469 | 470 | impl Material for RoughConductor { 471 | fn sample(&self, dir_in: Vec3, uv: (f32, f32), rnd: Vec3) -> BSDFSample { 472 | let roughness = self.roughness.eval(uv).avg(); 473 | let (h, ggx_pdf) = microfacet::sample(roughness, (rnd.x, rnd.y)); 474 | let in_dot_h = -Vec3::dot(dir_in, h); 475 | let direction = dir_in + (2.0 * in_dot_h) * h; 476 | 477 | let cos_i = -cos_theta(dir_in); 478 | let cos_o = cos_theta(direction); 479 | if cos_i <= 0.0 || cos_o <= 0.0 || in_dot_h <= 0.0 { 480 | return NULL_SAMPLE; 481 | } 482 | 483 | let g = microfacet::shadowing(roughness, dir_in, direction, h); 484 | let d = microfacet::distribution(roughness, cos_theta(h)); 485 | let f = fresnel::conductor_reflectance_rgb(self.ior, in_dot_h); 486 | 487 | let pdf = ggx_pdf / (4.0 * in_dot_h); 488 | let weight = in_dot_h * d * g / (ggx_pdf * cos_i); 489 | let weight = self.albedo.eval(uv) * (f * weight); 490 | 491 | BSDFSample { direction, pdf, weight, is_specular: false } 492 | } 493 | 494 | fn eval(&self, dir_in: Vec3, dir_out: Vec3, uv: (f32, f32)) -> Vec3 { 495 | let cos_i = -cos_theta(dir_in); 496 | let cos_o = cos_theta(dir_out); 497 | if cos_i <= 0.0 && cos_o <= 0.0 { 498 | return Vec3::zero(); 499 | } 500 | 501 | let roughness = self.roughness.eval(uv).avg(); 502 | let h = (-dir_in + dir_out).normalized(); 503 | let f = fresnel::conductor_reflectance_rgb(self.ior, -Vec3::dot(dir_in, h)); 504 | let g = microfacet::shadowing(roughness, dir_in, dir_out, h); 505 | let d = microfacet::distribution(roughness, cos_theta(h)); 506 | let albedo = self.albedo.eval(uv); 507 | albedo * f * (g * d / (4.0 * cos_i)) 508 | } 509 | 510 | fn pdf(&self, dir_in: Vec3, dir_out: Vec3, uv: (f32, f32)) -> f32 { 511 | let cos_i = -cos_theta(dir_in); 512 | let cos_o = cos_theta(dir_out); 513 | if cos_i <= 0.0 && cos_o <= 0.0 { 514 | return 0.0; 515 | } 516 | let roughness = self.roughness.eval(uv).avg(); 517 | let h = (-dir_in + dir_out).normalized(); 518 | microfacet::pdf(roughness, cos_theta(h)) / (4.0 * -Vec3::dot(dir_in, h)) 519 | } 520 | 521 | fn is_purely_specular(&self) -> bool { 522 | false 523 | } 524 | } 525 | 526 | pub struct SmoothCoat { 527 | pub ior: f32, 528 | pub scaled_sigma_a: Vec3, 529 | pub substrate: Arc, 530 | } 531 | 532 | impl Material for SmoothCoat { 533 | fn sample(&self, dir_in: Vec3, uv: (f32, f32), rnd: Vec3) -> BSDFSample { 534 | let eta = 1.0 / self.ior; 535 | let cos_i = -cos_theta(dir_in); 536 | if cos_i <= 0.0 { 537 | return NULL_SAMPLE; 538 | } 539 | let (fi, cos_ti) = fresnel::dielectric_reflectance(eta, cos_i); 540 | 541 | let avg_transmittance = (-2.0 * self.scaled_sigma_a.avg()).exp(); 542 | let sub_weight = avg_transmittance * (1.0 - fi); 543 | let specular_weight = fi; 544 | let specular_prob = specular_weight / (specular_weight + sub_weight); 545 | 546 | if rnd.z < specular_prob { 547 | return BSDFSample { 548 | direction: reflect(dir_in), 549 | pdf: specular_prob, 550 | weight: Vec3::thrice(fi + sub_weight), 551 | is_specular: true, 552 | }; 553 | } 554 | 555 | let dir_in_sub = Vec3::new(dir_in.x * eta, -cos_ti, dir_in.z * eta); 556 | let sub_sample = self.substrate.sample(dir_in_sub, uv, rnd); 557 | if sub_sample.weight == Vec3::zero() { 558 | return NULL_SAMPLE; 559 | } 560 | 561 | let cos_sub = cos_theta(sub_sample.direction); 562 | let (fo, cos_to) = fresnel::dielectric_reflectance(self.ior, cos_sub); 563 | if fo == 1.0 { 564 | return NULL_SAMPLE; 565 | } 566 | 567 | let dir_out_sub = sub_sample.direction; 568 | let direction = Vec3::new(dir_out_sub.x * self.ior, cos_to, dir_out_sub.z * self.ior); 569 | 570 | let mut weight = sub_sample.weight * ((1.0 - fi) * (1.0 - fo)); 571 | if self.scaled_sigma_a.max_elem() > 0.0 { 572 | weight *= (self.scaled_sigma_a * (-1.0 / cos_sub - 1.0 / cos_ti)).map(f32::exp); 573 | } 574 | weight = weight / (1.0 - specular_prob); 575 | let pdf = sub_sample.pdf * (1.0 - specular_prob) * eta*eta*cos_to/cos_sub; 576 | 577 | BSDFSample { direction, pdf, weight, is_specular: sub_sample.is_specular } 578 | } 579 | 580 | fn eval(&self, dir_in: Vec3, dir_out: Vec3, uv: (f32, f32)) -> Vec3 { 581 | let cos_i = -cos_theta(dir_in); 582 | let cos_o = cos_theta(dir_out); 583 | if cos_i <= 0.0 && cos_o <= 0.0 { 584 | return Vec3::zero(); 585 | } 586 | 587 | let eta = 1.0 / self.ior; 588 | let (fi, cos_ti) = fresnel::dielectric_reflectance(eta, cos_i); 589 | let (fo, cos_to) = fresnel::dielectric_reflectance(eta, cos_o); 590 | 591 | let dir_in_sub = Vec3::new(dir_in.x * eta, -cos_ti, dir_in.z * eta); 592 | let dir_out_sub = Vec3::new(dir_out.x * eta, cos_to, dir_out.z * eta); 593 | 594 | let mut sub_eval = self.substrate.eval(dir_in_sub, dir_out_sub, uv); 595 | 596 | if self.scaled_sigma_a.max_elem() > 0.0 { 597 | sub_eval *= (self.scaled_sigma_a * (-1.0 / cos_to - 1.0 / cos_ti)).map(f32::exp); 598 | } 599 | 600 | let l = eta * eta * cos_o / cos_to; 601 | l * (1.0 - fi) * (1.0 - fo) * sub_eval 602 | } 603 | 604 | fn pdf(&self, dir_in: Vec3, dir_out: Vec3, uv: (f32, f32)) -> f32 { 605 | let cos_i = -cos_theta(dir_in); 606 | let cos_o = cos_theta(dir_out); 607 | if cos_i <= 0.0 && cos_o <= 0.0 { 608 | return 0.0; 609 | } 610 | 611 | let eta = 1.0 / self.ior; 612 | let (fi, cos_ti) = fresnel::dielectric_reflectance(eta, cos_i); 613 | let (_, cos_to) = fresnel::dielectric_reflectance(eta, cos_o); 614 | 615 | let dir_in_sub = Vec3::new(dir_in.x * eta, -cos_ti, dir_in.z * eta); 616 | let dir_out_sub = Vec3::new(dir_out.x * eta, cos_to, dir_out.z * eta); 617 | 618 | let avg_transmittance = (-2.0 * self.scaled_sigma_a.avg()).exp(); 619 | let sub_weight = avg_transmittance * (1.0 - fi); 620 | let specular_weight = fi; 621 | let specular_prob = specular_weight / (specular_weight + sub_weight); 622 | let l = eta * eta * (cos_o / cos_to).abs(); 623 | self.substrate.pdf(dir_in_sub, dir_out_sub, uv) * (1.0 - specular_prob) * l 624 | } 625 | 626 | fn is_purely_specular(&self) -> bool { 627 | false 628 | } 629 | } 630 | 631 | fn refract(dir_in: Vec3, eta: f32, cos_t: f32) -> Vec3 { 632 | Vec3::new(dir_in.x * eta, cos_t * cos_theta(dir_in).signum(), dir_in.z * eta) 633 | } 634 | 635 | fn reflect(dir_in: Vec3) -> Vec3 { 636 | Vec3::new(dir_in.x, -dir_in.y, dir_in.z) 637 | } 638 | 639 | fn cos_theta(v: Vec3) -> f32 { 640 | v.y 641 | } 642 | 643 | mod fresnel { 644 | use math::*; 645 | use super::ComplexIOR; 646 | 647 | // return (reflectance, cos_t) 648 | pub fn dielectric_reflectance(eta: f32, cos_i: f32) -> (f32, f32) { 649 | // clamp cos_i before using trigonometric identities 650 | let cos_i = cos_i.min(1.0).max(-1.0); 651 | 652 | let sin_t2 = eta * eta * (1.0 - cos_i * cos_i); 653 | if sin_t2 > 1.0 { 654 | // Total Internal Reflection 655 | return (1.0, 0.0); 656 | } 657 | 658 | let cos_t = (1.0 - sin_t2).sqrt(); 659 | let r_s = (eta * cos_i - cos_t) / (eta * cos_i + cos_t); 660 | let r_p = (eta * cos_t - cos_i) / (eta * cos_t + cos_i); 661 | return ((r_s * r_s + r_p * r_p) * 0.5, cos_t); 662 | } 663 | 664 | fn conductor_reflectance(eta: f32, k: f32, cos_i: f32) -> f32 { 665 | let cos_i2 = cos_i * cos_i; 666 | let sin_i2 = 1.0 - cos_i2; 667 | let sin_i4 = sin_i2 * sin_i2; 668 | 669 | let x = eta * eta - k * k - sin_i2; 670 | let a2_b2 = (x * x + 4.0 * eta * eta * k * k).max(0.0).sqrt(); 671 | let a = ((a2_b2 + x) * 0.5).sqrt(); 672 | 673 | let r_s = ((a2_b2 + cos_i2) - (2.0 * a * cos_i)) / ((a2_b2 + cos_i2) + (2.0 * a * cos_i)); 674 | let r_p = ((cos_i2 * a2_b2 + sin_i4) - (2.0 * a * cos_i * sin_i2)) / ((cos_i2 * a2_b2 + sin_i4) + (2.0 * a * cos_i * sin_i2)); 675 | 676 | return (r_s + r_s * r_p) * 0.5; 677 | } 678 | 679 | pub fn conductor_reflectance_rgb(ior: ComplexIOR, cos_i: f32) -> Vec3 { 680 | // clamp cos_i before using trigonometric identities 681 | let cos_i = cos_i.min(1.0).max(-1.0); 682 | 683 | Vec3 { 684 | x: conductor_reflectance(ior.eta.x, ior.k.x, cos_i), 685 | y: conductor_reflectance(ior.eta.y, ior.k.y, cos_i), 686 | z: conductor_reflectance(ior.eta.z, ior.k.z, cos_i), 687 | } 688 | } 689 | } 690 | 691 | mod microfacet { 692 | use math::*; 693 | use super::cos_theta; 694 | 695 | pub fn distribution(alpha: f32, cos_theta: f32) -> f32 { 696 | if cos_theta <= 0.0 { 697 | return 0.0; 698 | } 699 | 700 | let alpha2 = alpha * alpha; 701 | let cos_theta2 = cos_theta * cos_theta; 702 | let tan_theta2 = (1.0 - cos_theta2) / cos_theta2; 703 | let cos_theta4 = cos_theta2 * cos_theta2; 704 | let x = alpha2 + tan_theta2; 705 | alpha2 / (PI * cos_theta4 * x * x) 706 | } 707 | 708 | fn shadowing_1d(alpha: f32, v: Vec3, h: Vec3) -> f32 { 709 | let cos_theta = cos_theta(v); 710 | if Vec3::dot(v, h) * cos_theta <= 0.0 { 711 | return 0.0; 712 | } 713 | 714 | let alpha2 = alpha * alpha; 715 | let cos_theta2 = cos_theta * cos_theta; 716 | let tan_theta2 = (1.0 - cos_theta2) / cos_theta2; 717 | 2.0 / (1.0 + (1.0 + alpha2 * tan_theta2).sqrt()) 718 | } 719 | 720 | pub fn shadowing(alpha: f32, dir_in: Vec3, dir_out: Vec3, h: Vec3) -> f32 { 721 | shadowing_1d(alpha, -dir_in, h) * shadowing_1d(alpha, dir_out, h) 722 | } 723 | 724 | pub fn sample(alpha: f32, (u, v): (f32, f32)) -> (Vec3, f32) { 725 | let phi = v * 2.0 * PI; 726 | let tan_theta2 = alpha * alpha * u / (1.0 - u); 727 | let cos_theta = 1.0 / (1.0 + tan_theta2).sqrt(); 728 | let r = (1.0 - cos_theta * cos_theta).sqrt(); 729 | (Vec3::new(phi.cos() * r, cos_theta, phi.sin() * r), distribution(alpha, cos_theta) * cos_theta) 730 | } 731 | 732 | pub fn pdf(alpha: f32, cos_theta: f32) -> f32 { 733 | distribution(alpha, cos_theta) * cos_theta 734 | } 735 | } 736 | 737 | // List of complex conductor IOR taken from Tungsten: 738 | // https://github.com/tunabrain/tungsten/blob/master/src/core/bsdfs/ComplexIorData.hpp 739 | pub const CONDUCTORS_IOR: [(&'static str, ComplexIOR); 40] = [ 740 | ("a-C", ComplexIOR { eta: Vec3 { x: 2.9440999183, y: 2.2271502925, z: 1.9681668794 }, k: Vec3 { x: 0.8874329109, y: 0.7993216383, z: 0.8152862927 } }), 741 | ("Ag", ComplexIOR { eta: Vec3 { x: 0.1552646489, y: 0.1167232965, z: 0.1383806959 }, k: Vec3 { x: 4.8283433224, y: 3.1222459278, z: 2.1469504455 } }), 742 | ("Al", ComplexIOR { eta: Vec3 { x: 1.6574599595, y: 0.8803689579, z: 0.5212287346 }, k: Vec3 { x: 9.2238691996, y: 6.2695232477, z: 4.8370012281 } }), 743 | ("AlAs", ComplexIOR { eta: Vec3 { x: 3.6051023902, y: 3.2329365777, z: 2.2175611545 }, k: Vec3 { x: 0.0006670247, y: -0.000499940, z: 0.0074261204 } }), 744 | ("AlSb", ComplexIOR { eta: Vec3 { x: -0.048522570, y: 4.1427547893, z: 4.6697691348 }, k: Vec3 { x: -0.036374191, y: 0.0937665154, z: 1.3007390124 } }), 745 | ("Au", ComplexIOR { eta: Vec3 { x: 0.1431189557, y: 0.3749570432, z: 1.4424785571 }, k: Vec3 { x: 3.9831604247, y: 2.3857207478, z: 1.6032152899 } }), 746 | ("Be", ComplexIOR { eta: Vec3 { x: 4.1850592788, y: 3.1850604423, z: 2.7840913457 }, k: Vec3 { x: 3.8354398268, y: 3.0101260162, z: 2.8690088743 } }), 747 | ("Cr", ComplexIOR { eta: Vec3 { x: 4.3696828663, y: 2.9167024892, z: 1.6547005413 }, k: Vec3 { x: 5.2064337956, y: 4.2313645277, z: 3.7549467933 } }), 748 | ("CsI", ComplexIOR { eta: Vec3 { x: 2.1449030413, y: 1.7023164587, z: 1.6624194173 }, k: Vec3 { x: 0.0000000000, y: 0.0000000000, z: 0.0000000000 } }), 749 | ("Cu", ComplexIOR { eta: Vec3 { x: 0.2004376970, y: 0.9240334304, z: 1.1022119527 }, k: Vec3 { x: 3.9129485033, y: 2.4528477015, z: 2.1421879552 } }), 750 | ("Cu2O", ComplexIOR { eta: Vec3 { x: 3.5492833755, y: 2.9520622449, z: 2.7369202137 }, k: Vec3 { x: 0.1132179294, y: 0.1946659670, z: 0.6001681264 } }), 751 | ("CuO", ComplexIOR { eta: Vec3 { x: 3.2453822204, y: 2.4496293965, z: 2.1974114493 }, k: Vec3 { x: 0.5202739621, y: 0.5707372756, z: 0.7172250613 } }), 752 | ("d-C", ComplexIOR { eta: Vec3 { x: 2.7112524747, y: 2.3185812849, z: 2.2288565009 }, k: Vec3 { x: 0.0000000000, y: 0.0000000000, z: 0.0000000000 } }), 753 | ("Hg", ComplexIOR { eta: Vec3 { x: 2.3989314904, y: 1.4400254917, z: 0.9095512090 }, k: Vec3 { x: 6.3276269444, y: 4.3719414152, z: 3.4217899270 } }), 754 | ("HgTe", ComplexIOR { eta: Vec3 { x: 4.7795267752, y: 3.2309984581, z: 2.6600252401 }, k: Vec3 { x: 1.6319827058, y: 1.5808189339, z: 1.7295753852 } }), 755 | ("Ir", ComplexIOR { eta: Vec3 { x: 3.0864098394, y: 2.0821938440, z: 1.6178866805 }, k: Vec3 { x: 5.5921510077, y: 4.0671757150, z: 3.2672611269 } }), 756 | ("K", ComplexIOR { eta: Vec3 { x: 0.0640493070, y: 0.0464100621, z: 0.0381842017 }, k: Vec3 { x: 2.1042155920, y: 1.3489364357, z: 0.9132113889 } }), 757 | ("Li", ComplexIOR { eta: Vec3 { x: 0.2657871942, y: 0.1956102432, z: 0.2209198538 }, k: Vec3 { x: 3.5401743407, y: 2.3111306542, z: 1.6685930000 } }), 758 | ("MgO", ComplexIOR { eta: Vec3 { x: 2.0895885542, y: 1.6507224525, z: 1.5948759692 }, k: Vec3 { x: 0.0000000000, y: 0.0000000000, z: 0.0000000000 } }), 759 | ("Mo", ComplexIOR { eta: Vec3 { x: 4.4837010280, y: 3.5254578255, z: 2.7760769438 }, k: Vec3 { x: 4.1111307988, y: 3.4208716252, z: 3.1506031404 } }), 760 | ("Na", ComplexIOR { eta: Vec3 { x: 0.0602665320, y: 0.0561412435, z: 0.0619909494 }, k: Vec3 { x: 3.1792906496, y: 2.1124800781, z: 1.5790940266 } }), 761 | ("Nb", ComplexIOR { eta: Vec3 { x: 3.4201353595, y: 2.7901921379, z: 2.3955856658 }, k: Vec3 { x: 3.4413817900, y: 2.7376437930, z: 2.5799132708 } }), 762 | ("Ni", ComplexIOR { eta: Vec3 { x: 2.3672753521, y: 1.6633583302, z: 1.4670554172 }, k: Vec3 { x: 4.4988329911, y: 3.0501643957, z: 2.3454274399 } }), 763 | ("Rh", ComplexIOR { eta: Vec3 { x: 2.5857954933, y: 1.8601866068, z: 1.5544279524 }, k: Vec3 { x: 6.7822927110, y: 4.7029501026, z: 3.9760892461 } }), 764 | ("Se-e", ComplexIOR { eta: Vec3 { x: 5.7242724833, y: 4.1653992967, z: 4.0816099264 }, k: Vec3 { x: 0.8713747439, y: 1.1052845009, z: 1.5647788766 } }), 765 | ("Se", ComplexIOR { eta: Vec3 { x: 4.0592611085, y: 2.8426947380, z: 2.8207582835 }, k: Vec3 { x: 0.7543791750, y: 0.6385150558, z: 0.5215872029 } }), 766 | ("SiC", ComplexIOR { eta: Vec3 { x: 3.1723450205, y: 2.5259677964, z: 2.4793623897 }, k: Vec3 { x: 0.0000007284, y: -0.000000686, z: 0.0000100150 } }), 767 | ("SnTe", ComplexIOR { eta: Vec3 { x: 4.5251865890, y: 1.9811525984, z: 1.2816819226 }, k: Vec3 { x: 0.0000000000, y: 0.0000000000, z: 0.0000000000 } }), 768 | ("Ta", ComplexIOR { eta: Vec3 { x: 2.0625846607, y: 2.3930915569, z: 2.6280684948 }, k: Vec3 { x: 2.4080467973, y: 1.7413705864, z: 1.9470377016 } }), 769 | ("Te-e", ComplexIOR { eta: Vec3 { x: 7.5090397678, y: 4.2964603080, z: 2.3698732430 }, k: Vec3 { x: 5.5842076830, y: 4.9476231084, z: 3.9975145063 } }), 770 | ("Te", ComplexIOR { eta: Vec3 { x: 7.3908396088, y: 4.4821028985, z: 2.6370708478 }, k: Vec3 { x: 3.2561412892, y: 3.5273908133, z: 3.2921683116 } }), 771 | ("ThF4", ComplexIOR { eta: Vec3 { x: 1.8307187117, y: 1.4422274283, z: 1.3876488528 }, k: Vec3 { x: 0.0000000000, y: 0.0000000000, z: 0.0000000000 } }), 772 | ("TiC", ComplexIOR { eta: Vec3 { x: 3.7004673762, y: 2.8374356509, z: 2.5823030278 }, k: Vec3 { x: 3.2656905818, y: 2.3515586388, z: 2.1727857800 } }), 773 | ("TiN", ComplexIOR { eta: Vec3 { x: 1.6484691607, y: 1.1504482522, z: 1.3797795097 }, k: Vec3 { x: 3.3684596226, y: 1.9434888540, z: 1.1020123347 } }), 774 | ("TiO2-e", ComplexIOR { eta: Vec3 { x: 3.1065574823, y: 2.5131551146, z: 2.5823844157 }, k: Vec3 { x: 0.0000289537, y: -0.000025148, z: 0.0001775555 } }), 775 | ("TiO2", ComplexIOR { eta: Vec3 { x: 3.4566203131, y: 2.8017076558, z: 2.9051485020 }, k: Vec3 { x: 0.0001026662, y: -0.000089753, z: 0.0006356902 } }), 776 | ("VC", ComplexIOR { eta: Vec3 { x: 3.6575665991, y: 2.7527298065, z: 2.5326814570 }, k: Vec3 { x: 3.0683516659, y: 2.1986687713, z: 1.9631816252 } }), 777 | ("VN", ComplexIOR { eta: Vec3 { x: 2.8656011588, y: 2.1191817791, z: 1.9400767149 }, k: Vec3 { x: 3.0323264950, y: 2.0561075580, z: 1.6162930914 } }), 778 | ("V", ComplexIOR { eta: Vec3 { x: 4.2775126218, y: 3.5131538236, z: 2.7611257461 }, k: Vec3 { x: 3.4911844504, y: 2.8893580874, z: 3.1116965117 } }), 779 | ("W", ComplexIOR { eta: Vec3 { x: 4.3707029924, y: 3.3002972445, z: 2.9982666528 }, k: Vec3 { x: 3.5006778591, y: 2.6048652781, z: 2.2731930614 } }), 780 | ]; 781 | --------------------------------------------------------------------------------