stocking

git clone https://git.bradleygannon.com/stocking.git
Log | Files | Refs

commit 5f7f9eb5019bbe128d49eba9cfc63171023f38da
parent 4baaf9340cd1096b1b86a9b3fe5cbd681ba7be61
Author: Bradley Gannon <bradley@bradleygannon.com>
Date:   Mon, 15 Dec 2025 18:36:56 -0500

Add diffuse reflection, bounce limit, and gamma correction

Complete Chapter 9

Diffstat:
Msrc/camera.rs | 45+++++++++++++++++++++++++++++----------------
Msrc/color.rs | 30+++++++++++++++++++++++++++++-
Msrc/hittable.rs | 7+++++--
Msrc/main.rs | 2+-
Msrc/random.rs | 2+-
Msrc/vec3.rs | 29+++++++++++++++++++++++++++++
6 files changed, 94 insertions(+), 21 deletions(-)

diff --git a/src/camera.rs b/src/camera.rs @@ -1,5 +1,9 @@ use crate::{ - color::Color, hittable::Hittable, interval::Interval, random::RandomNumberGenerator, ray::Ray, + color::{Color, ColorSpace}, + hittable::Hittable, + interval::Interval, + random::RandomNumberGenerator, + ray::Ray, vec3::Vec3, }; @@ -25,11 +29,12 @@ impl Image { output += "255\n"; for row_data in self.data.iter() { for color in row_data.iter() { + let gamma_color = color.as_gamma(); output += &format!( "{} {} {}\n", - scale_f64_to_u8(color.red()), - scale_f64_to_u8(color.green()), - scale_f64_to_u8(color.blue()) + scale_f64_to_u8(gamma_color.red()), + scale_f64_to_u8(gamma_color.green()), + scale_f64_to_u8(gamma_color.blue()) ); } } @@ -45,6 +50,7 @@ pub struct Camera { pixel_origin: Vec3, samples_per_pixel: usize, rng: RandomNumberGenerator, + max_bounces: usize, } impl Camera { @@ -52,7 +58,12 @@ impl Camera { const CAMERA_CENTER: Vec3 = Vec3::new(0., 0., 0.); const RNG_SEED: u64 = 42; - pub fn new(image_width: usize, aspect_ratio: f64, samples_per_pixel: usize) -> Self { + pub fn new( + image_width: usize, + aspect_ratio: f64, + samples_per_pixel: usize, + max_bounces: usize, + ) -> Self { let image_height = ((image_width as f64 / aspect_ratio) as usize).max(1); let viewport_height = 2.; @@ -78,6 +89,7 @@ impl Camera { pixel_origin, samples_per_pixel, rng, + max_bounces, } } @@ -85,10 +97,11 @@ impl Camera { let mut image = Image::new(self.image_width, self.image_height); for i in 0..self.image_width { for j in 0..self.image_height { - let mut pixel_color = Color::new(0., 0., 0.); + let mut pixel_color = Color::new(0., 0., 0., ColorSpace::Linear); for _ in 0..self.samples_per_pixel { let r = self.get_ray(i, j); - pixel_color += self.ray_color(&r, world) / self.samples_per_pixel as f64; + pixel_color += + self.ray_color(&r, world, self.max_bounces) / self.samples_per_pixel as f64; } image.data[j][i] = pixel_color; } @@ -96,18 +109,18 @@ impl Camera { println!("{}", image.to_ppm_string()); } - fn ray_color(&self, r: &Ray, world: &[Box<dyn Hittable>]) -> Color { - if let Some(hit) = world.hit(r, Interval::new(0., f64::INFINITY)) { - let normal = hit.normal(); - Color::new( - normal.x() * 0.5 + 0.5, - normal.y() * 0.5 + 0.5, - normal.z() * 0.5 + 0.5, - ) + fn ray_color(&mut self, r: &Ray, world: &[Box<dyn Hittable>], depth: usize) -> Color { + if depth == 0 { + return Color::new(0., 0., 0., ColorSpace::Linear); + } + if let Some(hit) = world.hit(r, Interval::new(0.001, f64::INFINITY)) { + let direction = hit.normal() + Vec3::random_unit(&mut self.rng); + 0.5 * self.ray_color(&Ray::new(hit.point(), direction), world, depth - 1) } else { let unit_direction = r.direction().unit(); let a = 0.5 * (unit_direction.y() + 1.); - (1. - a) * Color::new(1., 1., 1.) + a * Color::new(0.5, 0.7, 1.) + (1. - a) * Color::new(1., 1., 1., ColorSpace::Linear) + + a * Color::new(0.5, 0.7, 1., ColorSpace::Linear) } } diff --git a/src/color.rs b/src/color.rs @@ -1,21 +1,47 @@ use std::ops::{Add, AddAssign, Div, Mul}; #[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)] +pub enum ColorSpace { + #[default] + Linear, + Gamma, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)] pub struct Color { red: f64, green: f64, blue: f64, + space: ColorSpace, } impl Color { const MIN: f64 = 0.; const MAX: f64 = 1.; - pub const fn new(red: f64, green: f64, blue: f64) -> Self { + pub const fn new(red: f64, green: f64, blue: f64, space: ColorSpace) -> Self { Color { red: red.clamp(Color::MIN, Color::MAX), green: green.clamp(Color::MIN, Color::MAX), blue: blue.clamp(Color::MIN, Color::MAX), + space, + } + } + + pub fn as_gamma(&self) -> Self { + if let ColorSpace::Linear = self.space { + Color { + red: if self.red > 0. { self.red.sqrt() } else { 0. }, + green: if self.green > 0. { + self.green.sqrt() + } else { + 0. + }, + blue: if self.blue > 0. { self.blue.sqrt() } else { 0. }, + space: ColorSpace::Gamma, + } + } else { + *self } } @@ -40,6 +66,7 @@ impl Add<Color> for Color { red: (self.red + rhs.red).clamp(Color::MIN, Color::MAX), green: (self.green + rhs.green).clamp(Color::MIN, Color::MAX), blue: (self.blue + rhs.blue).clamp(Color::MIN, Color::MAX), + space: self.space, } } } @@ -60,6 +87,7 @@ impl Mul<f64> for Color { red: (self.red * rhs).clamp(Color::MIN, Color::MAX), green: (self.green * rhs).clamp(Color::MIN, Color::MAX), blue: (self.blue * rhs).clamp(Color::MIN, Color::MAX), + space: self.space, } } } diff --git a/src/hittable.rs b/src/hittable.rs @@ -1,7 +1,7 @@ use crate::{interval::Interval, ray::Ray, vec3::Vec3}; pub struct HitRecord { - _point: Vec3, + point: Vec3, normal: Vec3, t: f64, _front_face: bool, @@ -18,7 +18,7 @@ impl HitRecord { -outward_normal_unit }; HitRecord { - _point: point, + point, normal, t, _front_face: front_face, @@ -27,6 +27,9 @@ impl HitRecord { pub fn normal(&self) -> Vec3 { self.normal } + pub fn point(&self) -> Vec3 { + self.point + } } pub trait Hittable { diff --git a/src/main.rs b/src/main.rs @@ -17,6 +17,6 @@ fn main() { Box::new(Sphere::new(Vec3::new(0., 0., -1.), 0.5)), Box::new(Sphere::new(Vec3::new(0., -100.5, -1.), 100.)), ]; - let mut camera = Camera::new(400, 16. / 9., 100); + let mut camera = Camera::new(400, 16. / 9., 100, 50); camera.render(&world); } diff --git a/src/random.rs b/src/random.rs @@ -44,7 +44,7 @@ impl RandomNumberGenerator { (r as f64) * (1.0 / ((1u64 << 53) as f64)) } - pub fn _next_f64_in_range(&mut self, min: f64, max: f64) -> f64 { + pub fn next_f64_in_range(&mut self, min: f64, max: f64) -> f64 { min + (max - min) * self.next_f64() } } diff --git a/src/vec3.rs b/src/vec3.rs @@ -3,6 +3,8 @@ use std::{ ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub}, }; +use crate::random::RandomNumberGenerator; + #[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)] pub struct Vec3 { x: f64, @@ -16,6 +18,33 @@ impl Vec3 { Vec3 { x, y, z } } + pub fn random(rng: &mut RandomNumberGenerator, min: f64, max: f64) -> Self { + Vec3::new( + rng.next_f64_in_range(min, max), + rng.next_f64_in_range(min, max), + rng.next_f64_in_range(min, max), + ) + } + + pub fn random_unit(rng: &mut RandomNumberGenerator) -> Self { + loop { + let p = Vec3::random(rng, -1., 1.); + let length_squared = p.length_squared(); + if 1e-160 < length_squared && length_squared <= 1. { + return p / length_squared.sqrt(); + } + } + } + + pub fn random_on_hemisphere(rng: &mut RandomNumberGenerator, normal: &Vec3) -> Self { + let on_unit_sphere = Vec3::random_unit(rng); + if on_unit_sphere.dot(normal) > 0. { + on_unit_sphere + } else { + -on_unit_sphere + } + } + pub const fn x(&self) -> f64 { self.x }