├── .gitignore ├── LICENSE ├── README.md ├── pre-commit └── raytracer ├── .gitignore ├── Cargo.toml ├── data ├── beach.jpg ├── cover_scene.json ├── earth.jpg ├── moon.jpg └── test_scene.json ├── output ├── anim.mp4 ├── cover.png ├── dark.mp4 └── out.png └── src ├── camera.rs ├── config.rs ├── lib.rs ├── main.rs ├── materials.rs ├── point3d.rs ├── ray.rs ├── raytracer.rs └── sphere.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | .DS_Store 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2021, David Singleton 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rust-raytracer 2 | 3 | An implementation of a very simple raytracer based on [Ray Tracing in One Weekend 4 | by Peter Shirley](https://raytracing.github.io/books/RayTracingInOneWeekend.html) in Rust. I used this project to *learn* Rust from scratch - the code may not be perfectly idiomatic, or even good, but it does make pretty pictures. 5 | 6 | Additional features beyond Shirley's course: 7 | * Texture mapping (e.g. earth and moon textures below) 8 | * Lighting 9 | * Parallel rendering - will use all CPU cores for best performance 10 | * Read scene data from JSON file 11 | * Render a sky texture 12 | 13 | ## Example output 14 | ![Latest output](raytracer/output/cover.png) 15 | 16 | https://user-images.githubusercontent.com/237355/147687883-4e9ca4fc-7c3b-4adb-85d7-6b08d1bc69f7.mp4 17 | 18 | ## Example usage 19 | ``` 20 | $ cargo build --release 21 | Compiling raytracer v0.1.0 (/Users/dps/proj/rust-raytracer/raytracer) 22 | Finished release [optimized] target(s) in 2.57s 23 | 24 | $ ./target/release/raytracer data/test_scene.json out.png 25 | 26 | Rendering out.png 27 | Frame time: 2840ms 28 | 29 | $ ./target/release/raytracer data/cover_scene.json cover.png 30 | 31 | Rendering cover.png 32 | Frame time: 27146ms 33 | ``` 34 | 35 | ### Texture mapping 36 | ![cover_alt](https://user-images.githubusercontent.com/237355/147840674-38dd846f-1d4d-40a8-a573-e626a454f55a.png) 37 | 38 | ### Lighting 39 | ![lighting-recast-final](https://user-images.githubusercontent.com/237355/147840677-8e895fe5-1d25-428e-a847-6120af3ecfec.png) 40 | 41 | ### Parallel rendering - will use all CPU cores for best performance 42 | 43 | #### Original 44 | ``` 45 | 🚀 ./target/release/raytracer anim/frame 46 | Compiling raytracer v0.1.0 (/Users/dps/proj/rust-raytracer/raytracer) 47 | Finished release [optimized] target(s) in 2.21s 48 | 49 | Rendering anim/frame_000.png 50 | ............................................................ 51 | Frame time: 21s 52 | ``` 53 | #### Using rayon 54 | ``` 55 | Rendering anim/frame_000.png 56 | Frame time: 2573ms 57 | ``` 58 | ### Render a sky texture 59 | ![sky_textures](https://user-images.githubusercontent.com/237355/147840693-355a75da-a473-4c44-b712-842129450306.gif) 60 | 61 | ### Read scene data from JSON file 62 | 63 | #### Example 64 | ``` 65 | { 66 | "width": 800, 67 | "height": 600, 68 | "samples_per_pixel": 128, 69 | "max_depth": 50, 70 | "sky": { 71 | "texture":"data/beach.jpg" 72 | }, 73 | "camera": { 74 | "look_from": { "x": -2.0, "y": 0.5, "z": 1.0 }, 75 | "look_at": { "x": 0.0, "y": 0.0, "z": -1.0 }, 76 | "vup": { "x": 0.0, "y": 1.0, "z": 0.0 }, 77 | "vfov": 50.0, 78 | "aspect": 1.3333333333333333 79 | }, 80 | "objects": [ 81 | { 82 | "center": { "x": 0.0, "y": 0.0, "z": -1.0 }, 83 | "radius": 0.5, 84 | "material": { 85 | "Texture": { 86 | "albedo": [ 87 | 1.0, 88 | 1.0, 89 | 1.0 90 | ], 91 | "pixels": "data/earth.jpg", 92 | "width": 2048, 93 | "height": 1024, 94 | "h_offset": 0.75 95 | } 96 | } 97 | } 98 | ] 99 | } 100 | ``` 101 | 102 | ### Make animation 103 | ``` 104 | 🚀 ffmpeg -f image2 -framerate 15 -i anim/frame_%03d.png -loop -0 anim.gif 105 | ``` 106 | 107 | ### Credits 108 | Earth and moon textures from https://www.solarsystemscope.com/textures/ 109 | 110 | ### Extreme lighting example 111 | ![147705264-c6f439df-f61b-4bcf-b5e6-c2c755b35b1c](https://user-images.githubusercontent.com/237355/147706272-7e35f213-914f-43dd-9b8b-4d3e7628cc19.png) 112 | 113 | ### Progressive max_depth animation 114 | ![max_depth_anim](https://user-images.githubusercontent.com/237355/148159509-aa492f3b-2805-45fe-94a6-3588fbf69bb2.gif) 115 | 116 | -------------------------------------------------------------------------------- /pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | check_char='\xE2\x9C\x93' 3 | cross_char='\xE2\x9D\x8C' 4 | green='\033[0;32m' 5 | nc='\033[0m' 6 | check="$green$check_char$nc" 7 | errors=0 8 | 9 | cd raytracer 10 | echo -n "Checking formatting... " 11 | if result=$(cargo fmt -- --check); then 12 | echo -e "$check" 13 | else 14 | echo -e "$cross_char\n$result" 15 | errors=1 16 | fi 17 | 18 | echo -n "Running tests... " 19 | if result=$(cargo test --color always 2>&1); then 20 | echo -e "$check" 21 | else 22 | echo "$result" 23 | errors=1 24 | fi 25 | 26 | if [ "$errors" != 0 ]; then 27 | echo "Failed" 28 | exit 1 29 | else 30 | echo "OK" 31 | fi -------------------------------------------------------------------------------- /raytracer/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | anim/ 12 | -------------------------------------------------------------------------------- /raytracer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "raytracer" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | image = "0.13.0" 10 | palette = "0.6.0" 11 | assert_approx_eq = "1.1.0" 12 | rand = "0.8.4" 13 | jpeg-decoder = "0.2.1" 14 | crossbeam = "0.8" 15 | rayon = "1" 16 | serde = { version = "1.0", features = ["derive"] } 17 | serde_json = "1.0" 18 | serde_with = "1.9.4" -------------------------------------------------------------------------------- /raytracer/data/beach.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dps/rust-raytracer/606f85fa0353710875158fc9dc3c8cd2d29572c3/raytracer/data/beach.jpg -------------------------------------------------------------------------------- /raytracer/data/earth.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dps/rust-raytracer/606f85fa0353710875158fc9dc3c8cd2d29572c3/raytracer/data/earth.jpg -------------------------------------------------------------------------------- /raytracer/data/moon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dps/rust-raytracer/606f85fa0353710875158fc9dc3c8cd2d29572c3/raytracer/data/moon.jpg -------------------------------------------------------------------------------- /raytracer/data/test_scene.json: -------------------------------------------------------------------------------- 1 | { 2 | "width": 800, 3 | "height": 600, 4 | "samples_per_pixel": 128, 5 | "max_depth": 50, 6 | "sky": { 7 | "texture":"data/beach.jpg" 8 | }, 9 | "camera": { 10 | "look_from": { 11 | "x": -2.0, 12 | "y": 0.5, 13 | "z": 1.0 14 | }, 15 | "look_at": { 16 | "x": 0.0, 17 | "y": 0.0, 18 | "z": -1.0 19 | }, 20 | "vup": { 21 | "x": 0.0, 22 | "y": 1.0, 23 | "z": 0.0 24 | }, 25 | "vfov": 50.0, 26 | "aspect": 1.3333333333333333 27 | }, 28 | "objects": [ 29 | { 30 | "center": { 31 | "x": 0.0, 32 | "y": 0.0, 33 | "z": -1.0 34 | }, 35 | "radius": 0.5, 36 | "material": { 37 | "Texture": { 38 | "albedo": [ 39 | 1.0, 40 | 1.0, 41 | 1.0 42 | ], 43 | "pixels": "data/earth.jpg", 44 | "width": 2048, 45 | "height": 1024, 46 | "h_offset": 0.75 47 | } 48 | } 49 | }, 50 | { 51 | "center": { 52 | "x": -1.0, 53 | "y": 0.2, 54 | "z": -1.0 55 | }, 56 | "radius": 0.1, 57 | "material": { 58 | "Texture": { 59 | "albedo": [ 60 | 1.0, 61 | 1.0, 62 | 1.0 63 | ], 64 | "pixels": "data/moon.jpg", 65 | "width": 2048, 66 | "height": 1024, 67 | "h_offset": 0.75 68 | } 69 | } 70 | }, 71 | { 72 | "center": { 73 | "x": 0.0, 74 | "y": -100.5, 75 | "z": -1.0 76 | }, 77 | "radius": 100.0, 78 | "material": { 79 | "Metal": { 80 | "albedo": [ 81 | 0.8, 82 | 0.8, 83 | 0.8 84 | ], 85 | "fuzz": 0.0 86 | } 87 | } 88 | }, 89 | { 90 | "center": { 91 | "x": 0.0, 92 | "y": 16.0, 93 | "z": 20.0 94 | }, 95 | "radius": 15.0, 96 | "material": { 97 | "Light": {} 98 | } 99 | }, 100 | { 101 | "center": { 102 | "x": 1.0, 103 | "y": 0.5, 104 | "z": -1.0 105 | }, 106 | "radius": 0.5, 107 | "material": { 108 | "Metal": { 109 | "albedo": [ 110 | 0.8, 111 | 0.6, 112 | 0.2 113 | ], 114 | "fuzz": 0.1 115 | } 116 | } 117 | }, 118 | { 119 | "center": { 120 | "x": -1.2, 121 | "y": 0.0, 122 | "z": -1.0 123 | }, 124 | "radius": 0.5, 125 | "material": { 126 | "Glass": { 127 | "index_of_refraction": 1.5 128 | } 129 | } 130 | }, 131 | { 132 | "center": { 133 | "x": -1.2, 134 | "y": 0.0, 135 | "z": -1.0 136 | }, 137 | "radius": -0.45, 138 | "material": { 139 | "Glass": { 140 | "index_of_refraction": 1.5 141 | } 142 | } 143 | } 144 | ] 145 | } -------------------------------------------------------------------------------- /raytracer/output/anim.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dps/rust-raytracer/606f85fa0353710875158fc9dc3c8cd2d29572c3/raytracer/output/anim.mp4 -------------------------------------------------------------------------------- /raytracer/output/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dps/rust-raytracer/606f85fa0353710875158fc9dc3c8cd2d29572c3/raytracer/output/cover.png -------------------------------------------------------------------------------- /raytracer/output/dark.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dps/rust-raytracer/606f85fa0353710875158fc9dc3c8cd2d29572c3/raytracer/output/dark.mp4 -------------------------------------------------------------------------------- /raytracer/output/out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dps/rust-raytracer/606f85fa0353710875158fc9dc3c8cd2d29572c3/raytracer/output/out.png -------------------------------------------------------------------------------- /raytracer/src/camera.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::point3d::Point3D; 4 | use crate::ray::Ray; 5 | 6 | #[cfg(test)] 7 | use assert_approx_eq::assert_approx_eq; 8 | 9 | #[derive(Debug, Clone, Copy, Serialize, Deserialize)] 10 | #[serde(from = "CameraParams")] 11 | pub struct Camera { 12 | #[serde(skip_serializing)] 13 | pub origin: Point3D, // Note, don't serialize any of the computed fields. 14 | #[serde(skip_serializing)] 15 | pub lower_left_corner: Point3D, 16 | #[serde(skip_serializing)] 17 | pub focal_length: f64, 18 | #[serde(skip_serializing)] 19 | pub horizontal: Point3D, 20 | #[serde(skip_serializing)] 21 | pub vertical: Point3D, 22 | look_from: Point3D, 23 | look_at: Point3D, 24 | vup: Point3D, 25 | vfov: f64, // vertical field-of-view in degrees 26 | aspect: f64, 27 | } 28 | 29 | #[derive(Debug, Clone, Copy, Serialize, Deserialize)] 30 | pub struct CameraParams { 31 | pub look_from: Point3D, 32 | pub look_at: Point3D, 33 | pub vup: Point3D, 34 | pub vfov: f64, // vertical field-of-view in degrees 35 | pub aspect: f64, 36 | } 37 | 38 | impl From for Camera { 39 | fn from(p: CameraParams) -> Self { 40 | Camera::new(p.look_from, p.look_at, p.vup, p.vfov, p.aspect) 41 | } 42 | } 43 | 44 | impl Camera { 45 | pub fn new( 46 | look_from: Point3D, 47 | look_at: Point3D, 48 | vup: Point3D, 49 | vfov: f64, // vertical field-of-view in degrees 50 | aspect: f64, 51 | ) -> Camera { 52 | let theta = vfov.to_radians(); 53 | let half_height = (theta / 2.0).tan(); 54 | let half_width = aspect * half_height; 55 | 56 | let w = (look_from - look_at).unit_vector(); 57 | let u = vup.cross(&w).unit_vector(); 58 | let v = w.cross(&u); 59 | 60 | let origin = look_from; 61 | let lower_left_corner = origin - (u * half_width) - (v * half_height) - w; 62 | let horizontal = u * 2.0 * half_width; 63 | let vertical = v * 2.0 * half_height; 64 | 65 | Camera { 66 | origin, 67 | lower_left_corner, 68 | focal_length: (look_from - look_at).length(), 69 | horizontal, 70 | vertical, 71 | look_from, 72 | look_at, 73 | vup, 74 | vfov, 75 | aspect, 76 | } 77 | } 78 | 79 | pub fn get_ray(&self, u: f64, v: f64) -> Ray { 80 | Ray::new( 81 | self.origin, 82 | self.lower_left_corner + (self.horizontal * u) + (self.vertical * v) - self.origin, 83 | ) 84 | } 85 | } 86 | 87 | #[test] 88 | fn test_camera() { 89 | let camera = Camera::new( 90 | Point3D::new(0.0, 0.0, 0.0), 91 | Point3D::new(0.0, 0.0, -1.0), 92 | Point3D::new(0.0, 1.0, 0.0), 93 | 90.0, 94 | (800.0 / 600.0) as f64, 95 | ); 96 | assert_eq!(camera.origin.x(), 0.0); 97 | assert_eq!(camera.origin.y(), 0.0); 98 | assert_eq!(camera.origin.z(), 0.0); 99 | 100 | assert_approx_eq!(camera.lower_left_corner.x(), -(1.0 + (1.0 / 3.0))); 101 | assert_approx_eq!(camera.lower_left_corner.y(), -1.0); 102 | assert_approx_eq!(camera.lower_left_corner.z(), -1.0); 103 | } 104 | 105 | #[test] 106 | fn test_camera_get_ray() { 107 | let camera = Camera::new( 108 | Point3D::new(-4.0, 4.0, 1.0), 109 | Point3D::new(0.0, 0.0, -1.0), 110 | Point3D::new(0.0, 1.0, 0.0), 111 | 160.0, 112 | (800 / 600) as f64, 113 | ); 114 | let ray = camera.get_ray(0.5, 0.5); 115 | assert_eq!(ray.origin.x(), -4.0); 116 | assert_eq!(ray.origin.y(), 4.0); 117 | assert_eq!(ray.origin.z(), 1.0); 118 | 119 | assert_approx_eq!(ray.direction.x(), (2.0 / 3.0)); 120 | assert_approx_eq!(ray.direction.y(), -(2.0 / 3.0)); 121 | assert_approx_eq!(ray.direction.z(), -(1.0 / 3.0)); 122 | } 123 | 124 | #[test] 125 | fn test_to_json() { 126 | let camera = Camera::new( 127 | Point3D::new(-4.0, 4.0, 1.0), 128 | Point3D::new(0.0, 0.0, -1.0), 129 | Point3D::new(0.0, 1.0, 0.0), 130 | 160.0, 131 | (800 / 600) as f64, 132 | ); 133 | let serialized = serde_json::to_string(&camera).unwrap(); 134 | assert_eq!("{\"look_from\":{\"x\":-4.0,\"y\":4.0,\"z\":1.0},\"look_at\":{\"x\":0.0,\"y\":0.0,\"z\":-1.0},\"vup\":{\"x\":0.0,\"y\":1.0,\"z\":0.0},\"vfov\":160.0,\"aspect\":1.0}", serialized); 135 | let c = serde_json::from_str::(&serialized).unwrap(); 136 | assert_eq!(camera.origin, c.origin); 137 | assert_eq!(camera.lower_left_corner, c.lower_left_corner); 138 | assert_eq!(camera.focal_length, c.focal_length); 139 | assert_eq!(camera.horizontal, c.horizontal); 140 | assert_eq!(camera.vertical, c.vertical); 141 | } 142 | -------------------------------------------------------------------------------- /raytracer/src/config.rs: -------------------------------------------------------------------------------- 1 | use jpeg_decoder::Decoder; 2 | use palette::Srgb; 3 | use rand::Rng; 4 | use serde::{Deserialize, Serialize}; 5 | use serde_with::serde_as; 6 | use std::fs::File; 7 | use std::io::BufReader; 8 | 9 | use crate::camera::Camera; 10 | use crate::materials::Glass; 11 | use crate::materials::Lambertian; 12 | use crate::materials::Material; 13 | use crate::materials::Metal; 14 | use crate::point3d::Point3D; 15 | use crate::sphere::Sphere; 16 | 17 | #[cfg(test)] 18 | use std::fs; 19 | 20 | #[serde_with::serde_as] 21 | #[derive(Debug, Serialize, Deserialize)] 22 | pub struct Sky { 23 | // If provided, the sky will be rendered using the equirectangular 24 | // projected texture loaded from an image file at this path. Else, 25 | // a light blue colored sky will be used. 26 | #[serde_as(as = "TextureOptionPixelsAsPath")] 27 | pub texture: Option<(Vec, usize, usize, String)>, 28 | } 29 | 30 | impl Sky { 31 | pub fn new_default_sky() -> Sky { 32 | Sky { texture: None } 33 | } 34 | } 35 | 36 | fn load_texture_image(path: &str) -> (Vec, usize, usize, String) { 37 | let file = File::open(path).expect(path); 38 | let mut decoder = Decoder::new(BufReader::new(file)); 39 | let pixels = decoder.decode().expect("failed to decode image"); 40 | let metadata = decoder.info().unwrap(); 41 | ( 42 | pixels, 43 | metadata.width as usize, 44 | metadata.height as usize, 45 | path.to_string(), 46 | ) 47 | } 48 | 49 | serde_with::serde_conv!( 50 | TextureOptionPixelsAsPath, 51 | Option<(Vec, usize, usize, String)>, 52 | |texture: &Option<(Vec, usize, usize, String)>| { 53 | match texture { 54 | Some(tuple) => tuple.3.clone(), 55 | None => "".to_string(), 56 | } 57 | }, 58 | |value: &str| -> Result<_, std::convert::Infallible> { 59 | match value { 60 | "" => Ok(None), 61 | _ => Ok(Some(load_texture_image(value))), 62 | } 63 | } 64 | ); 65 | 66 | #[derive(Debug, Serialize, Deserialize)] 67 | pub struct Config { 68 | pub width: usize, 69 | pub height: usize, 70 | pub samples_per_pixel: u32, 71 | pub max_depth: usize, 72 | pub sky: Option, 73 | pub camera: Camera, 74 | pub objects: Vec, 75 | } 76 | 77 | #[test] 78 | fn test_to_json() { 79 | let config = Config { 80 | width: 100, 81 | height: 100, 82 | samples_per_pixel: 1, 83 | max_depth: 1, 84 | sky: Some(Sky::new_default_sky()), 85 | camera: Camera::new( 86 | Point3D::new(0.0, 0.0, 0.0), 87 | Point3D::new(0.0, 0.0, -1.0), 88 | Point3D::new(0.0, 1.0, 0.0), 89 | 90.0, 90 | 1.0, 91 | ), 92 | objects: vec![Sphere::new( 93 | Point3D::new(0.0, 0.0, -1.0), 94 | 0.5, 95 | Material::Lambertian(Lambertian::new(Srgb::new( 96 | 0.8 as f32, 0.3 as f32, 0.3 as f32, 97 | ))), 98 | )], 99 | }; 100 | let serialized = serde_json::to_string(&config).unwrap(); 101 | assert_eq!("{\"width\":100,\"height\":100,\"samples_per_pixel\":1,\"max_depth\":1,\"sky\":{\"texture\":\"\"},\"camera\":{\"look_from\":{\"x\":0.0,\"y\":0.0,\"z\":0.0},\"look_at\":{\"x\":0.0,\"y\":0.0,\"z\":-1.0},\"vup\":{\"x\":0.0,\"y\":1.0,\"z\":0.0},\"vfov\":90.0,\"aspect\":1.0},\"objects\":[{\"center\":{\"x\":0.0,\"y\":0.0,\"z\":-1.0},\"radius\":0.5,\"material\":{\"Lambertian\":{\"albedo\":[0.8,0.3,0.3]}}}]}", serialized); 102 | } 103 | 104 | #[test] 105 | fn test_sky_perms_to_from_json() { 106 | let config = Config { 107 | width: 100, 108 | height: 100, 109 | samples_per_pixel: 1, 110 | max_depth: 1, 111 | sky: None, 112 | camera: Camera::new( 113 | Point3D::new(0.0, 0.0, 0.0), 114 | Point3D::new(0.0, 0.0, -1.0), 115 | Point3D::new(0.0, 1.0, 0.0), 116 | 90.0, 117 | 1.0, 118 | ), 119 | objects: vec![Sphere::new( 120 | Point3D::new(0.0, 0.0, -1.0), 121 | 0.5, 122 | Material::Lambertian(Lambertian::new(Srgb::new( 123 | 0.8 as f32, 0.3 as f32, 0.3 as f32, 124 | ))), 125 | )], 126 | }; 127 | let serialized = serde_json::to_string(&config).unwrap(); 128 | assert_eq!("{\"width\":100,\"height\":100,\"samples_per_pixel\":1,\"max_depth\":1,\"sky\":null,\"camera\":{\"look_from\":{\"x\":0.0,\"y\":0.0,\"z\":0.0},\"look_at\":{\"x\":0.0,\"y\":0.0,\"z\":-1.0},\"vup\":{\"x\":0.0,\"y\":1.0,\"z\":0.0},\"vfov\":90.0,\"aspect\":1.0},\"objects\":[{\"center\":{\"x\":0.0,\"y\":0.0,\"z\":-1.0},\"radius\":0.5,\"material\":{\"Lambertian\":{\"albedo\":[0.8,0.3,0.3]}}}]}", serialized); 129 | let _ = serde_json::from_str::(&serialized).expect("Unable to parse json"); 130 | 131 | // This scene contains a sky texture at data/earth,jpg 132 | let scene_json = "{\"width\":100,\"height\":100,\"samples_per_pixel\":1,\"max_depth\":1,\"sky\":{\"texture\":\"data/earth.jpg\"},\"camera\":{\"look_from\":{\"x\":0.0,\"y\":0.0,\"z\":0.0},\"look_at\":{\"x\":0.0,\"y\":0.0,\"z\":-1.0},\"vup\":{\"x\":0.0,\"y\":1.0,\"z\":0.0},\"vfov\":90.0,\"aspect\":1.0},\"objects\":[{\"center\":{\"x\":0.0,\"y\":0.0,\"z\":-1.0},\"radius\":0.5,\"material\":{\"Lambertian\":{\"albedo\":[0.8,0.3,0.3]}}}]}"; 133 | let scene = serde_json::from_str::(&scene_json).expect("Unable to parse json"); 134 | 135 | assert_eq!( 136 | match scene.sky { 137 | Some(sky) => { 138 | match sky.texture { 139 | Some(tuple) => (tuple.1, tuple.2, tuple.3), 140 | _ => (0, 0, "".to_string()), 141 | } 142 | } 143 | _ => (0, 0, "".to_string()), 144 | }, 145 | (2048, 1024, "data/earth.jpg".to_string()) 146 | ) 147 | } 148 | 149 | fn _make_cover_world() -> Vec { 150 | let mut world = Vec::new(); 151 | 152 | world.push(Sphere::new( 153 | Point3D::new(0.0, -1000.0, 0.0), 154 | 1000.0, 155 | Material::Lambertian(Lambertian::new(Srgb::new(0.5, 0.5, 0.5))), 156 | )); 157 | 158 | let mut rng = rand::thread_rng(); 159 | 160 | for a in -11..11 { 161 | for b in -11..11 { 162 | let choose_mat = rng.gen::(); 163 | let center = Point3D::new( 164 | a as f64 + 0.9 * rng.gen::(), 165 | 0.2, 166 | b as f64 + 0.9 * rng.gen::(), 167 | ); 168 | 169 | if ((center - Point3D::new(4.0, 0.2, 0.0)).length()) < 0.9 { 170 | continue; 171 | } 172 | 173 | if choose_mat < 0.8 { 174 | // diffuse 175 | world.push(Sphere::new( 176 | center, 177 | 0.2, 178 | Material::Lambertian(Lambertian::new(Srgb::new( 179 | rng.gen::() * rng.gen::(), 180 | rng.gen::() * rng.gen::(), 181 | rng.gen::() * rng.gen::(), 182 | ))), 183 | )); 184 | } else if choose_mat < 0.95 { 185 | // metal 186 | world.push(Sphere::new( 187 | center, 188 | 0.2, 189 | Material::Metal(Metal::new( 190 | Srgb::new( 191 | 0.5 * (1.0 + rng.gen::()), 192 | 0.5 * (1.0 + rng.gen::()), 193 | 0.5 * (1.0 + rng.gen::()), 194 | ), 195 | 0.5 * rng.gen::(), 196 | )), 197 | )); 198 | } else { 199 | // glass 200 | world.push(Sphere::new(center, 0.2, Material::Glass(Glass::new(1.5)))); 201 | } 202 | } 203 | } 204 | 205 | world.push(Sphere::new( 206 | Point3D::new(0.0, 1.0, 0.0), 207 | 1.0, 208 | Material::Glass(Glass::new(1.5)), 209 | )); 210 | world.push(Sphere::new( 211 | Point3D::new(-4.0, 1.0, 0.0), 212 | 1.0, 213 | Material::Lambertian(Lambertian::new(Srgb::new( 214 | 0.4 as f32, 0.2 as f32, 0.1 as f32, 215 | ))), 216 | )); 217 | world.push(Sphere::new( 218 | Point3D::new(4.0, 1.0, 0.0), 219 | 1.0, 220 | Material::Metal(Metal::new( 221 | Srgb::new(0.7 as f32, 0.6 as f32, 0.5 as f32), 222 | 0.0, 223 | )), 224 | )); 225 | world 226 | } 227 | 228 | #[test] 229 | fn test_cover_scene_to_json() { 230 | let config = Config { 231 | width: 800, 232 | height: 600, 233 | samples_per_pixel: 64, 234 | max_depth: 50, 235 | sky: Some(Sky::new_default_sky()), 236 | camera: Camera::new( 237 | Point3D::new(13.0, 2.0, 3.0), 238 | Point3D::new(0.0, 0.0, 0.0), 239 | Point3D::new(0.0, 1.0, 0.0), 240 | 20.0, 241 | (800.0 / 600.0) as f64, 242 | ), 243 | objects: _make_cover_world(), 244 | }; 245 | let serialized = serde_json::to_string_pretty(&config).unwrap(); 246 | fs::write("/tmp/cover_scene.json", serialized).unwrap(); 247 | } 248 | 249 | #[test] 250 | fn test_from_file() { 251 | let json = fs::read("data/test_scene.json").expect("Unable to read file"); 252 | let scene = serde_json::from_slice::(&json).expect("Unable to parse json"); 253 | assert_eq!(scene.width, 800); 254 | assert_eq!(scene.height, 600); 255 | } 256 | -------------------------------------------------------------------------------- /raytracer/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod camera; 2 | pub mod config; 3 | pub mod materials; 4 | pub mod point3d; 5 | pub mod ray; 6 | pub mod raytracer; 7 | pub mod sphere; 8 | -------------------------------------------------------------------------------- /raytracer/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::fs; 3 | 4 | use raytracer::config::Config; 5 | use raytracer::raytracer::render; 6 | 7 | fn main() { 8 | let args: Vec = env::args().collect(); 9 | if args.len() != 3 { 10 | println!("Usage: {} ", args[0]); 11 | return; 12 | } 13 | 14 | let json = fs::read(&args[1]).expect("Unable to read config file."); 15 | let scene = serde_json::from_slice::(&json).expect("Unable to parse config json"); 16 | 17 | let filename = &args[2]; //format!("{}_{:0>3}.png", args[2], i); 18 | println!("\nRendering {}", filename); 19 | render(&filename, scene); 20 | } 21 | -------------------------------------------------------------------------------- /raytracer/src/materials.rs: -------------------------------------------------------------------------------- 1 | use jpeg_decoder::Decoder; 2 | use palette::Srgb; 3 | use rand::Rng; 4 | use serde::{Deserialize, Serialize}; 5 | use serde_with::serde_as; 6 | use std::fs::File; 7 | use std::io::BufReader; 8 | 9 | use crate::point3d::Point3D; 10 | use crate::ray::HitRecord; 11 | use crate::ray::Ray; 12 | 13 | pub trait Scatterable { 14 | fn scatter(&self, ray: &Ray, hit_record: &HitRecord) -> Option<(Option, Srgb)>; 15 | } 16 | 17 | // https://docs.rs/serde_with/1.9.4/serde_with/macro.serde_conv.html 18 | serde_with::serde_conv!( 19 | SrgbAsArray, 20 | Srgb, 21 | |srgb: &Srgb| [srgb.red, srgb.green, srgb.blue], 22 | |value: [f32; 3]| -> Result<_, std::convert::Infallible> { 23 | Ok(Srgb::new(value[0], value[1], value[2])) 24 | } 25 | ); 26 | 27 | // TODO: replace this with the more elegant implementation in config.rs 28 | serde_with::serde_conv!( 29 | TexturePixelsAsPath, 30 | Vec, 31 | |_pixels: &Vec| "/tmp/texture.jpg", 32 | |value: &str| -> Result<_, std::convert::Infallible> { Ok(load_texture_image(value).0) } 33 | ); 34 | 35 | #[derive(Debug, Clone, Deserialize, Serialize)] 36 | pub enum Material { 37 | Lambertian(Lambertian), 38 | Metal(Metal), 39 | Glass(Glass), 40 | Texture(Texture), 41 | Light(Light), 42 | } 43 | 44 | impl Scatterable for Material { 45 | fn scatter(&self, ray: &Ray, hit_record: &HitRecord) -> Option<(Option, Srgb)> { 46 | match self { 47 | Material::Lambertian(l) => l.scatter(ray, hit_record), 48 | Material::Metal(m) => m.scatter(ray, hit_record), 49 | Material::Glass(g) => g.scatter(ray, hit_record), 50 | Material::Texture(t) => t.scatter(ray, hit_record), 51 | Material::Light(l) => l.scatter(ray, hit_record), 52 | } 53 | } 54 | } 55 | 56 | #[derive(Debug, Clone, Copy, Deserialize, Serialize)] 57 | pub struct Light {} 58 | 59 | impl Light { 60 | pub fn new() -> Light { 61 | Light {} 62 | } 63 | } 64 | 65 | impl Scatterable for Light { 66 | fn scatter(&self, _ray: &Ray, _hit_record: &HitRecord) -> Option<(Option, Srgb)> { 67 | Some((None, Srgb::new(1.0, 1.0, 1.0))) 68 | } 69 | } 70 | 71 | #[serde_with::serde_as] 72 | #[derive(Debug, Clone, Copy, Deserialize, Serialize)] 73 | pub struct Lambertian { 74 | #[serde_as(as = "SrgbAsArray")] 75 | pub albedo: Srgb, 76 | } 77 | 78 | impl Lambertian { 79 | pub fn new(albedo: Srgb) -> Lambertian { 80 | Lambertian { albedo } 81 | } 82 | } 83 | 84 | impl Scatterable for Lambertian { 85 | fn scatter(&self, _ray: &Ray, hit_record: &HitRecord) -> Option<(Option, Srgb)> { 86 | let mut scatter_direction = hit_record.normal + Point3D::random_in_unit_sphere(); 87 | if scatter_direction.near_zero() { 88 | scatter_direction = hit_record.normal; 89 | } 90 | let target = hit_record.point + scatter_direction; 91 | let scattered = Ray::new(hit_record.point, target - hit_record.point); 92 | let attenuation = self.albedo; 93 | Some((Some(scattered), attenuation)) 94 | } 95 | } 96 | 97 | #[serde_with::serde_as] 98 | #[derive(Debug, Clone, Copy, Deserialize, Serialize)] 99 | pub struct Metal { 100 | #[serde_as(as = "SrgbAsArray")] 101 | pub albedo: Srgb, 102 | pub fuzz: f64, 103 | } 104 | 105 | impl Metal { 106 | pub fn new(albedo: Srgb, fuzz: f64) -> Metal { 107 | Metal { albedo, fuzz } 108 | } 109 | } 110 | 111 | fn reflect(v: &Point3D, n: &Point3D) -> Point3D { 112 | *v - *n * (2.0 * v.dot(n)) 113 | } 114 | 115 | impl Scatterable for Metal { 116 | fn scatter(&self, ray: &Ray, hit_record: &HitRecord) -> Option<(Option, Srgb)> { 117 | let reflected = reflect(&ray.direction, &hit_record.normal); 118 | let scattered = Ray::new( 119 | hit_record.point, 120 | reflected + Point3D::random_in_unit_sphere() * self.fuzz, 121 | ); 122 | let attenuation = self.albedo; 123 | if scattered.direction.dot(&hit_record.normal) > 0.0 { 124 | Some((Some(scattered), attenuation)) 125 | } else { 126 | None 127 | } 128 | } 129 | } 130 | 131 | #[derive(Debug, Clone, Copy, Deserialize, Serialize)] 132 | pub struct Glass { 133 | pub index_of_refraction: f64, 134 | } 135 | 136 | impl Glass { 137 | pub fn new(index_of_refraction: f64) -> Glass { 138 | Glass { 139 | index_of_refraction, 140 | } 141 | } 142 | } 143 | 144 | fn refract(uv: &Point3D, n: &Point3D, etai_over_etat: f64) -> Point3D { 145 | let cos_theta = ((-*uv).dot(n)).min(1.0); 146 | let r_out_perp = (*uv + *n * cos_theta) * etai_over_etat; 147 | let r_out_parallel = *n * (-1.0 * (1.0 - r_out_perp.length_squared()).abs().sqrt()); 148 | r_out_perp + r_out_parallel 149 | } 150 | 151 | fn reflectance(cosine: f64, ref_idx: f64) -> f64 { 152 | let mut r0 = (1.0 - ref_idx) / (1.0 + ref_idx); 153 | r0 = r0 * r0; 154 | r0 + (1.0 - r0) * (1.0 - cosine).powi(5) 155 | } 156 | 157 | #[test] 158 | fn test_refract() { 159 | let uv = Point3D::new(1.0, 1.0, 0.0); 160 | let n = Point3D::new(-1.0, 0.0, 0.0); 161 | let etai_over_etat = 1.0; 162 | let expected = Point3D::new(0.0, 1.0, 0.0); 163 | let actual = refract(&uv, &n, etai_over_etat); 164 | assert_eq!(actual, expected); 165 | } 166 | 167 | #[test] 168 | fn test_reflectance() { 169 | let cosine = 0.0; 170 | let ref_idx = 1.5; 171 | let expected = 1.0; 172 | let actual = reflectance(cosine, ref_idx); 173 | assert_eq!(actual, expected); 174 | } 175 | 176 | impl Scatterable for Glass { 177 | fn scatter(&self, ray: &Ray, hit_record: &HitRecord) -> Option<(Option, Srgb)> { 178 | let mut rng = rand::thread_rng(); 179 | let attenuation = Srgb::new(1.0 as f32, 1.0 as f32, 1.0 as f32); 180 | let refraction_ratio = if hit_record.front_face { 181 | 1.0 / self.index_of_refraction 182 | } else { 183 | self.index_of_refraction 184 | }; 185 | let unit_direction = ray.direction.unit_vector(); 186 | let cos_theta = (-unit_direction).dot(&hit_record.normal).min(1.0); 187 | let sin_theta = (1.0 - cos_theta * cos_theta).sqrt(); 188 | let cannot_refract = refraction_ratio * sin_theta > 1.0; 189 | if cannot_refract || reflectance(cos_theta, refraction_ratio) > rng.gen::() { 190 | let reflected = reflect(&unit_direction, &hit_record.normal); 191 | let scattered = Ray::new(hit_record.point, reflected); 192 | Some((Some(scattered), attenuation)) 193 | } else { 194 | let direction = refract(&unit_direction, &hit_record.normal, refraction_ratio); 195 | let scattered = Ray::new(hit_record.point, direction); 196 | Some((Some(scattered), attenuation)) 197 | } 198 | } 199 | } 200 | 201 | #[serde_with::serde_as] 202 | #[derive(Debug, Clone, Deserialize, Serialize)] 203 | pub struct Texture { 204 | #[serde_as(as = "SrgbAsArray")] 205 | pub albedo: Srgb, 206 | #[serde_as(as = "TexturePixelsAsPath")] 207 | pub pixels: Vec, 208 | width: u64, 209 | height: u64, 210 | h_offset: f64, 211 | } 212 | 213 | fn load_texture_image(path: &str) -> (Vec, u64, u64) { 214 | let file = File::open(path).expect(path); 215 | let mut decoder = Decoder::new(BufReader::new(file)); 216 | let pixels = decoder.decode().expect("failed to decode image"); 217 | let metadata = decoder.info().unwrap(); 218 | (pixels, metadata.width as u64, metadata.height as u64) 219 | } 220 | 221 | impl Texture { 222 | pub fn new(albedo: Srgb, texture_path: &str, rot: f64) -> Texture { 223 | let file = File::open(texture_path).expect("failed to open texture file"); 224 | let mut decoder = Decoder::new(BufReader::new(file)); 225 | let pixels = decoder.decode().expect("failed to decode image"); 226 | let metadata = decoder.info().unwrap(); 227 | Texture { 228 | albedo, 229 | pixels, 230 | width: metadata.width as u64, 231 | height: metadata.height as u64, 232 | h_offset: rot, 233 | } 234 | } 235 | 236 | pub fn get_albedo(&self, u: f64, v: f64) -> Srgb { 237 | let mut rot = u + self.h_offset; 238 | if rot > 1.0 { 239 | rot = rot - 1.0; 240 | } 241 | let uu = rot * (self.width) as f64; 242 | let vv = (1.0 - v) * (self.height - 1) as f64; 243 | let base_pixel = 244 | (3 * ((vv.floor() as u64) * self.width as u64 + (uu.floor() as u64))) as usize; 245 | let pixel_r = self.pixels[base_pixel]; 246 | let pixel_g = self.pixels[base_pixel + 1]; 247 | let pixel_b = self.pixels[base_pixel + 2]; 248 | Srgb::new( 249 | pixel_r as f32 / 255.0, 250 | pixel_g as f32 / 255.0, 251 | pixel_b as f32 / 255.0, 252 | ) 253 | } 254 | } 255 | 256 | impl Scatterable for Texture { 257 | fn scatter(&self, _ray: &Ray, hit_record: &HitRecord) -> Option<(Option, Srgb)> { 258 | let mut scatter_direction = hit_record.normal + Point3D::random_in_unit_sphere(); 259 | if scatter_direction.near_zero() { 260 | scatter_direction = hit_record.normal; 261 | } 262 | let target = hit_record.point + scatter_direction; 263 | let scattered = Ray::new(hit_record.point, target - hit_record.point); 264 | let attenuation = self.get_albedo(hit_record.u, hit_record.v); 265 | Some((Some(scattered), attenuation)) 266 | } 267 | } 268 | 269 | #[test] 270 | fn test_texture() { 271 | let _world = Material::Texture(Texture::new( 272 | Srgb::new(1.0, 1.0, 1.0), 273 | "data/earth.jpg", 274 | 0.0, 275 | )); 276 | } 277 | 278 | #[test] 279 | fn test_to_json() { 280 | let m = Metal::new(Srgb::new(0.8, 0.8, 0.8), 2.0); 281 | let serialized = serde_json::to_string(&m).unwrap(); 282 | assert_eq!(r#"{"albedo":[0.8,0.8,0.8],"fuzz":2.0}"#, serialized,); 283 | } 284 | -------------------------------------------------------------------------------- /raytracer/src/point3d.rs: -------------------------------------------------------------------------------- 1 | use rand::Rng; 2 | use serde::{Deserialize, Serialize}; 3 | use std::cmp::PartialEq; 4 | use std::f64; 5 | use std::ops::{Add, Div, Mul, Neg, Sub}; 6 | 7 | #[cfg(test)] 8 | use assert_approx_eq::assert_approx_eq; 9 | 10 | #[derive(Debug, Clone, Copy, Deserialize, Serialize)] 11 | pub struct Point3D { 12 | x: f64, 13 | y: f64, 14 | z: f64, 15 | } 16 | 17 | impl Point3D { 18 | pub fn new(x: f64, y: f64, z: f64) -> Point3D { 19 | Point3D { x, y, z } 20 | } 21 | 22 | pub fn random(min: f64, max: f64) -> Point3D { 23 | let mut rng = rand::thread_rng(); 24 | Point3D::new( 25 | rng.gen_range(min..max), 26 | rng.gen_range(min..max), 27 | rng.gen_range(min..max), 28 | ) 29 | } 30 | 31 | pub fn random_in_unit_sphere() -> Point3D { 32 | loop { 33 | let p = Point3D::random(-1.0, 1.0); 34 | if p.length_squared() < 1.0 { 35 | return p; 36 | } 37 | } 38 | } 39 | 40 | pub fn x(&self) -> f64 { 41 | self.x 42 | } 43 | 44 | pub fn y(&self) -> f64 { 45 | self.y 46 | } 47 | 48 | pub fn z(&self) -> f64 { 49 | self.z 50 | } 51 | 52 | pub fn distance(&self, other: &Point3D) -> f64 { 53 | let dx = self.x - other.x(); 54 | let dy = self.y - other.y(); 55 | let dz = self.z - other.z(); 56 | (dx * dx + dy * dy + dz * dz).sqrt() 57 | } 58 | 59 | pub fn length_squared(&self) -> f64 { 60 | self.x * self.x + self.y * self.y + self.z * self.z 61 | } 62 | 63 | pub fn length(&self) -> f64 { 64 | self.distance(&Point3D::new(0.0, 0.0, 0.0)) 65 | } 66 | 67 | pub fn unit_vector(&self) -> Point3D { 68 | let length = self.length(); 69 | Point3D::new(self.x / length, self.y / length, self.z / length) 70 | } 71 | 72 | pub fn dot(&self, other: &Point3D) -> f64 { 73 | self.x * other.x + self.y * other.y + self.z * other.z 74 | } 75 | 76 | pub fn cross(&self, other: &Point3D) -> Point3D { 77 | Point3D::new( 78 | self.y * other.z - self.z * other.y, 79 | self.z * other.x - self.x * other.z, 80 | self.x * other.y - self.y * other.x, 81 | ) 82 | } 83 | 84 | pub fn near_zero(&self) -> bool { 85 | self.x.abs() < f64::EPSILON && self.y.abs() < f64::EPSILON && self.z.abs() < f64::EPSILON 86 | } 87 | } 88 | 89 | impl Add for Point3D { 90 | type Output = Point3D; 91 | 92 | fn add(self, other: Point3D) -> Point3D { 93 | Point3D { 94 | x: self.x + other.x(), 95 | y: self.y + other.y(), 96 | z: self.z + other.z(), 97 | } 98 | } 99 | } 100 | 101 | impl Sub for Point3D { 102 | type Output = Point3D; 103 | 104 | fn sub(self, other: Point3D) -> Point3D { 105 | Point3D { 106 | x: self.x - other.x(), 107 | y: self.y - other.y(), 108 | z: self.z - other.z(), 109 | } 110 | } 111 | } 112 | 113 | impl Neg for Point3D { 114 | type Output = Point3D; 115 | 116 | fn neg(self) -> Point3D { 117 | Point3D { 118 | x: -self.x, 119 | y: -self.y, 120 | z: -self.z, 121 | } 122 | } 123 | } 124 | 125 | impl Mul for Point3D { 126 | type Output = Point3D; 127 | 128 | fn mul(self, other: Point3D) -> Point3D { 129 | Point3D { 130 | x: self.x * other.x(), 131 | y: self.y * other.y(), 132 | z: self.z * other.z(), 133 | } 134 | } 135 | } 136 | 137 | impl Mul for Point3D { 138 | type Output = Point3D; 139 | 140 | fn mul(self, other: f64) -> Point3D { 141 | Point3D { 142 | x: self.x * other, 143 | y: self.y * other, 144 | z: self.z * other, 145 | } 146 | } 147 | } 148 | 149 | impl Div for Point3D { 150 | type Output = Point3D; 151 | 152 | fn div(self, other: Point3D) -> Point3D { 153 | Point3D { 154 | x: self.x / other.x(), 155 | y: self.y / other.y(), 156 | z: self.z / other.z(), 157 | } 158 | } 159 | } 160 | 161 | impl Div for Point3D { 162 | type Output = Point3D; 163 | 164 | fn div(self, other: f64) -> Point3D { 165 | Point3D { 166 | x: self.x / other, 167 | y: self.y / other, 168 | z: self.z / other, 169 | } 170 | } 171 | } 172 | 173 | impl PartialEq for Point3D { 174 | fn eq(&self, other: &Point3D) -> bool { 175 | self.x == other.x() && self.y == other.y() && self.z == other.z() 176 | } 177 | } 178 | 179 | #[test] 180 | fn test_gen() { 181 | let p = Point3D { 182 | x: 0.1, 183 | y: 0.2, 184 | z: 0.3, 185 | }; 186 | assert_eq!(p.x(), 0.1); 187 | assert_eq!(p.y(), 0.2); 188 | assert_eq!(p.z(), 0.3); 189 | 190 | let q = Point3D::new(0.2, 0.3, 0.4); 191 | assert_eq!(q.x(), 0.2); 192 | assert_eq!(q.y(), 0.3); 193 | assert_eq!(q.z(), 0.4); 194 | } 195 | 196 | #[test] 197 | fn test_add() { 198 | let p = Point3D::new(0.1, 0.2, 0.3); 199 | let q = Point3D::new(0.2, 0.3, 0.4); 200 | let r = p + q; 201 | assert_approx_eq!(r.x(), 0.3); 202 | assert_approx_eq!(r.y(), 0.5); 203 | assert_approx_eq!(r.z(), 0.7); 204 | } 205 | 206 | #[test] 207 | fn test_sub() { 208 | let p = Point3D::new(0.1, 0.2, 0.3); 209 | let q = Point3D::new(0.2, 0.3, 0.4); 210 | let r = p - q; 211 | assert_approx_eq!(r.x(), -0.1); 212 | assert_approx_eq!(r.y(), -0.1); 213 | assert_approx_eq!(r.z(), -0.1); 214 | } 215 | 216 | #[test] 217 | fn test_neg() { 218 | let p = Point3D::new(0.1, 0.2, 0.3); 219 | let q = -p; 220 | assert_approx_eq!(q.x(), -0.1); 221 | assert_approx_eq!(q.y(), -0.2); 222 | assert_approx_eq!(q.z(), -0.3); 223 | } 224 | 225 | #[test] 226 | fn test_mul() { 227 | let p = Point3D::new(0.1, 0.2, 0.3); 228 | let q = Point3D::new(0.2, 0.3, 0.4); 229 | let r = p * q; 230 | assert_approx_eq!(r.x(), 0.02); 231 | assert_approx_eq!(r.y(), 0.06); 232 | assert_approx_eq!(r.z(), 0.12); 233 | } 234 | 235 | #[test] 236 | fn test_div() { 237 | let p = Point3D::new(0.1, 0.2, 0.3); 238 | let q = Point3D::new(0.2, 0.3, 0.4); 239 | let r = p / q; 240 | assert_approx_eq!(r.x(), 0.5); 241 | assert_approx_eq!(r.y(), 0.6666666666666666); 242 | assert_approx_eq!(r.z(), 0.3 / 0.4); 243 | } 244 | 245 | #[test] 246 | fn test_dot() { 247 | let p = Point3D::new(0.1, 0.2, 0.3); 248 | let q = Point3D::new(0.2, 0.3, 0.4); 249 | assert_approx_eq!(p.dot(&q), 0.2); 250 | } 251 | 252 | #[test] 253 | fn test_length_squared() { 254 | let p = Point3D::new(0.1, 0.2, 0.3); 255 | assert_approx_eq!(p.length_squared(), 0.14); 256 | } 257 | 258 | #[test] 259 | fn test_random() { 260 | let p = Point3D::random(-1.0, 1.0); 261 | assert!(p.x() >= -1.0 && p.x() <= 1.0); 262 | assert!(p.y() >= -1.0 && p.y() <= 1.0); 263 | assert!(p.z() >= -1.0 && p.z() <= 1.0); 264 | } 265 | 266 | #[test] 267 | fn test_near_zero() { 268 | let p = Point3D::new(0.1, 0.2, 0.3); 269 | assert!(!p.near_zero()); 270 | let p = Point3D::new(0.0, 0.0, 0.0); 271 | assert!(p.near_zero()); 272 | } 273 | -------------------------------------------------------------------------------- /raytracer/src/ray.rs: -------------------------------------------------------------------------------- 1 | use crate::materials::Material; 2 | use crate::point3d::Point3D; 3 | 4 | #[cfg(test)] 5 | use assert_approx_eq::assert_approx_eq; 6 | 7 | #[derive(Debug, Clone, Copy)] 8 | pub struct Ray { 9 | pub origin: Point3D, 10 | pub direction: Point3D, 11 | } 12 | 13 | impl Ray { 14 | pub fn new(origin: Point3D, direction: Point3D) -> Ray { 15 | Ray { origin, direction } 16 | } 17 | 18 | pub fn at(&self, t: f64) -> Point3D { 19 | self.origin + self.direction * t 20 | } 21 | } 22 | 23 | pub struct HitRecord<'material> { 24 | pub t: f64, 25 | pub point: Point3D, 26 | pub normal: Point3D, 27 | pub front_face: bool, 28 | pub material: &'material Material, 29 | pub u: f64, 30 | pub v: f64, 31 | } 32 | 33 | pub trait Hittable { 34 | fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option; 35 | } 36 | 37 | #[test] 38 | fn test_ray() { 39 | let p = Point3D::new(0.1, 0.2, 0.3); 40 | let q = Point3D::new(0.2, 0.3, 0.4); 41 | 42 | let r = Ray::new(p, q); 43 | 44 | assert_approx_eq!(r.origin.x(), 0.1); 45 | assert_approx_eq!(r.origin.y(), 0.2); 46 | assert_approx_eq!(r.origin.z(), 0.3); 47 | assert_approx_eq!(r.direction.x(), 0.2); 48 | assert_approx_eq!(r.direction.y(), 0.3); 49 | assert_approx_eq!(r.direction.z(), 0.4); 50 | } 51 | 52 | #[test] 53 | fn test_ray_at() { 54 | let p = Point3D::new(0.0, 0.0, 0.0); 55 | let q = Point3D::new(1.0, 2.0, 3.0); 56 | 57 | let r = Ray::new(p, q); 58 | let s = r.at(0.5); 59 | 60 | assert_approx_eq!(s.x(), 0.5); 61 | assert_approx_eq!(s.y(), 1.0); 62 | assert_approx_eq!(s.z(), 1.5); 63 | } 64 | -------------------------------------------------------------------------------- /raytracer/src/raytracer.rs: -------------------------------------------------------------------------------- 1 | use image::png::PNGEncoder; 2 | use image::ColorType; 3 | use palette::Pixel; 4 | use palette::Srgb; 5 | use rand::Rng; 6 | use rayon::prelude::*; 7 | use std::fs::File; 8 | use std::time::Instant; 9 | 10 | use crate::config::Config; 11 | use crate::materials::Material; 12 | use crate::materials::Scatterable; 13 | use crate::ray::HitRecord; 14 | use crate::ray::Hittable; 15 | use crate::ray::Ray; 16 | use crate::sphere::Sphere; 17 | 18 | #[cfg(test)] 19 | use std::fs; 20 | 21 | #[cfg(test)] 22 | use crate::point3d::Point3D; 23 | 24 | #[cfg(test)] 25 | use crate::camera::Camera; 26 | #[cfg(test)] 27 | use crate::config::Sky; 28 | #[cfg(test)] 29 | use crate::materials::Lambertian; 30 | #[cfg(test)] 31 | use crate::materials::Light; 32 | 33 | fn write_image( 34 | filename: &str, 35 | pixels: &[u8], 36 | bounds: (usize, usize), 37 | ) -> Result<(), std::io::Error> { 38 | let output = File::create(filename)?; 39 | let encoder = PNGEncoder::new(output); 40 | encoder.encode(pixels, bounds.0 as u32, bounds.1 as u32, ColorType::RGB(8))?; 41 | Ok(()) 42 | } 43 | 44 | fn hit_world<'material>( 45 | world: &'material Vec, 46 | r: &Ray, 47 | t_min: f64, 48 | t_max: f64, 49 | ) -> Option> { 50 | let mut closest_so_far = t_max; 51 | let mut hit_record = None; 52 | for sphere in world { 53 | if let Some(hit) = sphere.hit(r, t_min, closest_so_far) { 54 | closest_so_far = hit.t; 55 | hit_record = Some(hit); 56 | } 57 | } 58 | hit_record 59 | } 60 | 61 | fn clamp(value: f32) -> f32 { 62 | if value < 0.0 { 63 | 0.0 64 | } else if value > 1.0 { 65 | 1.0 66 | } else { 67 | value 68 | } 69 | } 70 | 71 | fn ray_color( 72 | ray: &Ray, 73 | scene: &Config, 74 | lights: &Vec, 75 | max_depth: usize, 76 | depth: usize, 77 | ) -> Srgb { 78 | let mut rng = rand::thread_rng(); 79 | 80 | if depth <= 0 { 81 | return Srgb::new(0.0, 0.0, 0.0); 82 | } 83 | let hit = hit_world(&scene.objects, ray, 0.001, std::f64::MAX); 84 | match hit { 85 | Some(hit_record) => { 86 | let scattered = hit_record.material.scatter(ray, &hit_record); 87 | match scattered { 88 | Some((scattered_ray, albedo)) => { 89 | let mut light_red = 0.0; 90 | let mut light_green = 0.0; 91 | let mut light_blue = 0.0; 92 | let mut prob = 0.1; 93 | match hit_record.material { 94 | Material::Glass(_) => { 95 | prob = 0.05; 96 | } 97 | _ => {} 98 | } 99 | if lights.len() > 0 100 | && rng.gen::() > (1.0 - lights.len() as f64 * prob) 101 | && depth > (max_depth - 2) 102 | { 103 | for light in lights { 104 | let light_ray = 105 | Ray::new(hit_record.point, light.center - hit_record.point); 106 | let target_color = ray_color(&light_ray, scene, lights, 2, 1); 107 | light_red += albedo.red * target_color.red; 108 | light_green += albedo.green * target_color.green; 109 | light_blue += albedo.blue * target_color.blue; 110 | } 111 | light_red /= lights.len() as f32; 112 | light_green /= lights.len() as f32; 113 | light_blue /= lights.len() as f32; 114 | } 115 | match scattered_ray { 116 | Some(sr) => { 117 | let target_color = ray_color(&sr, scene, lights, max_depth, depth - 1); 118 | return Srgb::new( 119 | clamp(light_red + albedo.red * target_color.red), 120 | clamp(light_green + albedo.green * target_color.green), 121 | clamp(light_blue + albedo.blue * target_color.blue), 122 | ); 123 | } 124 | None => albedo, 125 | } 126 | } 127 | None => { 128 | // don't bother bouncing absorbed rays towards lights 129 | // (they would be absorbed in the opposite direction). 130 | return Srgb::new(0.0, 0.0, 0.0); 131 | } 132 | } 133 | } 134 | None => { 135 | let t: f32 = clamp(0.5 * (ray.direction.unit_vector().y() as f32 + 1.0)); 136 | let u: f32 = clamp(0.5 * (ray.direction.unit_vector().x() as f32 + 1.0)); 137 | match &scene.sky { 138 | None => { 139 | return Srgb::new(0.0, 0.0, 0.0); 140 | } 141 | Some(sky) => match &sky.texture { 142 | None => { 143 | return Srgb::new( 144 | (1.0 - t) * 1.0 + t * 0.5, 145 | (1.0 - t) * 1.0 + t * 0.7, 146 | (1.0 - t) * 1.0 + t * 1.0, 147 | ); 148 | } 149 | Some((pixels, width, height, _)) => { 150 | let x = (u * (*width - 1) as f32) as usize; 151 | let y = ((1.0 - t) * (*height - 1) as f32) as usize; 152 | let pixel_red = &pixels[(y * *width + x) * 3]; 153 | let pixel_green = &pixels[(y * *width + x) * 3 + 1]; 154 | let pixel_blue = &pixels[(y * *width + x) * 3 + 2]; 155 | return Srgb::new( 156 | 0.7 * *pixel_red as f32 / 255.0, 157 | 0.7 * *pixel_green as f32 / 255.0, 158 | 0.7 * *pixel_blue as f32 / 255.0, 159 | ); 160 | } 161 | }, 162 | } 163 | } 164 | } 165 | } 166 | 167 | #[test] 168 | fn test_ray_color() { 169 | let p = Point3D::new(0.0, 0.0, 0.0); 170 | let q = Point3D::new(1.0, 0.0, 0.0); 171 | let r = Ray::new(p, q); 172 | let scene = Config { 173 | width: 80, 174 | height: 60, 175 | samples_per_pixel: 1, 176 | max_depth: 2, 177 | sky: Some(Sky::new_default_sky()), 178 | camera: Camera::new( 179 | Point3D::new(0.0, 0.0, -3.0), 180 | Point3D::new(0.0, 0.0, 0.0), 181 | Point3D::new(0.0, 1.0, 0.0), 182 | 20.0, 183 | 1.333, 184 | ), 185 | objects: Vec::new(), 186 | }; 187 | let l = Vec::new(); 188 | assert_eq!(ray_color(&r, &scene, &l, 2, 2), Srgb::new(0.75, 0.85, 1.0)); 189 | } 190 | 191 | fn render_line(pixels: &mut [u8], scene: &Config, lights: &Vec, y: usize) { 192 | let mut rng = rand::thread_rng(); 193 | 194 | let bounds = (scene.width, scene.height); 195 | 196 | for x in 0..bounds.0 { 197 | let mut pixel_colors: Vec = vec![0.0; 3]; 198 | for _s in 0..scene.samples_per_pixel { 199 | let u = (x as f64 + rng.gen::()) / (bounds.0 as f64 - 1.0); 200 | let v = (bounds.1 as f64 - (y as f64 + rng.gen::())) / (bounds.1 as f64 - 1.0); 201 | let r = scene.camera.get_ray(u, v); 202 | let c = ray_color(&r, scene, lights, scene.max_depth, scene.max_depth); 203 | pixel_colors[0] += c.red; 204 | pixel_colors[1] += c.green; 205 | pixel_colors[2] += c.blue; 206 | } 207 | let scale = 1.0 / scene.samples_per_pixel as f32; 208 | let color = Srgb::new( 209 | (scale * pixel_colors[0]).sqrt(), 210 | (scale * pixel_colors[1]).sqrt(), 211 | (scale * pixel_colors[2]).sqrt(), 212 | ); 213 | let pixel: [u8; 3] = color.into_format().into_raw(); 214 | pixels[x * 3] = pixel[0]; 215 | pixels[x * 3 + 1] = pixel[1]; 216 | pixels[x * 3 + 2] = pixel[2]; 217 | } 218 | } 219 | 220 | fn find_lights(world: &Vec) -> Vec { 221 | world 222 | .iter() 223 | .filter(|s| match s.material { 224 | Material::Light(_) => true, 225 | _ => false, 226 | }) 227 | .cloned() 228 | .collect() 229 | } 230 | 231 | #[test] 232 | fn test_find_lights() { 233 | let world = vec![ 234 | Sphere::new( 235 | Point3D::new(0.0, 0.0, -1.0), 236 | 0.5, 237 | Material::Light(Light::new()), 238 | ), 239 | Sphere::new( 240 | Point3D::new(0.0, 0.0, -1.0), 241 | 0.5, 242 | Material::Lambertian(Lambertian::new(Srgb::new( 243 | 0.5 as f32, 0.5 as f32, 0.5 as f32, 244 | ))), 245 | ), 246 | ]; 247 | assert_eq!(find_lights(&world).len(), 1); 248 | } 249 | 250 | pub fn render(filename: &str, scene: Config) { 251 | let image_width = scene.width; 252 | let image_height = scene.height; 253 | 254 | let mut pixels = vec![0; image_width * image_height * 3]; 255 | let bands: Vec<(usize, &mut [u8])> = pixels.chunks_mut(image_width * 3).enumerate().collect(); 256 | 257 | let lights = find_lights(&scene.objects); 258 | 259 | let start = Instant::now(); 260 | bands.into_par_iter().for_each(|(i, band)| { 261 | render_line(band, &scene, &lights, i); 262 | }); 263 | println!("Frame time: {}ms", start.elapsed().as_millis()); 264 | 265 | write_image(filename, &pixels, (image_width, image_height)).expect("error writing image"); 266 | } 267 | 268 | #[test] 269 | fn test_render_full_test_scene() { 270 | let json = fs::read("data/test_scene.json").expect("Unable to read file"); 271 | let mut scene = serde_json::from_slice::(&json).expect("Unable to parse json"); 272 | scene.width = 80; 273 | scene.height = 60; 274 | render("/tmp/test_scene.png", scene); 275 | } 276 | 277 | #[test] 278 | fn test_render_full_cover_scene() { 279 | let json = fs::read("data/cover_scene.json").expect("Unable to read file"); 280 | let mut scene = serde_json::from_slice::(&json).expect("Unable to parse json"); 281 | scene.width = 40; 282 | scene.height = 30; 283 | render("/tmp/cover_scene.png", scene); 284 | } 285 | -------------------------------------------------------------------------------- /raytracer/src/sphere.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::materials::Material; 4 | use crate::point3d::Point3D; 5 | use crate::ray::HitRecord; 6 | use crate::ray::Hittable; 7 | use crate::ray::Ray; 8 | 9 | #[cfg(test)] 10 | use crate::materials::Glass; 11 | #[cfg(test)] 12 | use crate::materials::Lambertian; 13 | #[cfg(test)] 14 | use crate::materials::Texture; 15 | #[cfg(test)] 16 | use palette::Srgb; 17 | 18 | #[derive(Debug, Clone, Deserialize, Serialize)] 19 | pub struct Sphere { 20 | pub center: Point3D, 21 | pub radius: f64, 22 | pub material: Material, 23 | } 24 | 25 | impl Sphere { 26 | pub fn new(center: Point3D, radius: f64, material: Material) -> Sphere { 27 | Sphere { 28 | center, 29 | radius, 30 | material, 31 | } 32 | } 33 | } 34 | 35 | fn u_v_from_sphere_hit_point(hit_point_on_sphere: Point3D) -> (f64, f64) { 36 | let n = hit_point_on_sphere.unit_vector(); 37 | let x = n.x(); 38 | let y = n.y(); 39 | let z = n.z(); 40 | let u = (x.atan2(z) / (2.0 * std::f64::consts::PI)) + 0.5; 41 | let v = y * 0.5 + 0.5; 42 | (u, v) 43 | } 44 | 45 | impl Hittable for Sphere { 46 | fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option { 47 | let oc = ray.origin - self.center; 48 | let a = ray.direction.length_squared(); 49 | let half_b = oc.dot(&ray.direction); 50 | let c = oc.length_squared() - self.radius * self.radius; 51 | let discriminant = (half_b * half_b) - (a * c); 52 | 53 | if discriminant >= 0.0 { 54 | let sqrtd = discriminant.sqrt(); 55 | let root_a = ((-half_b) - sqrtd) / a; 56 | let root_b = ((-half_b) + sqrtd) / a; 57 | for root in [root_a, root_b].iter() { 58 | if *root < t_max && *root > t_min { 59 | let p = ray.at(*root); 60 | let normal = (p - self.center) / self.radius; 61 | let front_face = ray.direction.dot(&normal) < 0.0; 62 | 63 | let (u, v) = u_v_from_sphere_hit_point(p - self.center); 64 | 65 | return Some(HitRecord { 66 | t: *root, 67 | point: p, 68 | normal: if front_face { normal } else { -normal }, 69 | front_face, 70 | material: &self.material, 71 | u, 72 | v, 73 | }); 74 | } 75 | } 76 | } 77 | None 78 | } 79 | } 80 | 81 | #[test] 82 | fn test_sphere_hit() { 83 | let center = Point3D::new(0.0, 0.0, 0.0); 84 | let sphere = Sphere::new(center, 1.0, Material::Glass(Glass::new(1.5))); 85 | let ray = Ray::new(Point3D::new(0.0, 0.0, -5.0), Point3D::new(0.0, 0.0, 1.0)); 86 | let hit = sphere.hit(&ray, 0.0, f64::INFINITY); 87 | assert_eq!(hit.unwrap().t, 4.0); 88 | } 89 | 90 | #[test] 91 | fn test_to_json() { 92 | let sphere = Sphere::new( 93 | Point3D::new(0.0, 0.0, 0.0), 94 | 1.0, 95 | Material::Lambertian(Lambertian::new(Srgb::new( 96 | 0.5 as f32, 0.5 as f32, 0.5 as f32, 97 | ))), 98 | ); 99 | let serialized = serde_json::to_string(&sphere).unwrap(); 100 | assert_eq!( 101 | "{\"center\":{\"x\":0.0,\"y\":0.0,\"z\":0.0},\"radius\":1.0,\"material\":{\"Lambertian\":{\"albedo\":[0.5,0.5,0.5]}}}", 102 | serialized, 103 | ); 104 | let s = serde_json::from_str::(&serialized).unwrap(); 105 | assert_eq!(sphere.center, s.center); 106 | assert_eq!(sphere.radius, s.radius); 107 | 108 | let textured_sphere = Sphere::new( 109 | Point3D::new(0.0, 0.0, 0.0), 110 | 1.0, 111 | Material::Texture(Texture::new( 112 | Srgb::new(0.5 as f32, 0.5 as f32, 0.5 as f32), 113 | "data/earth.jpg", 114 | 0.0, 115 | )), 116 | ); 117 | 118 | let tserialized = serde_json::to_string(&textured_sphere).unwrap(); 119 | assert_eq!( 120 | "{\"center\":{\"x\":0.0,\"y\":0.0,\"z\":0.0},\"radius\":1.0,\"material\":{\"Texture\":{\"albedo\":[0.5,0.5,0.5],\"pixels\":\"/tmp/texture.jpg\",\"width\":2048,\"height\":1024,\"h_offset\":0.0}}}", 121 | tserialized, 122 | ); 123 | 124 | let tex = Texture::new( 125 | Srgb::new(0.5 as f32, 0.5 as f32, 0.5 as f32), 126 | "data/earth.jpg", 127 | 0.0, 128 | ); 129 | let tloadable = "{\"center\":{\"x\":0.0,\"y\":0.0,\"z\":0.0},\"radius\":1.0,\"material\":{\"Texture\":{\"albedo\":[0.5,0.5,0.5],\"pixels\":\"data/earth.jpg\",\"width\":2048,\"height\":1024,\"h_offset\":0.0}}}"; 130 | let loaded = serde_json::from_str::(&tloadable).unwrap(); 131 | match loaded.material { 132 | Material::Texture(ref t) => { 133 | assert_eq!(t.pixels, tex.pixels); 134 | } 135 | _ => panic!("Wrong material type"), 136 | } 137 | } 138 | --------------------------------------------------------------------------------