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:
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
}