Modeling Light Scatter and Reflectance
Here and throughout these books we will use the term albedo (Latin for “whiteness”). Albedo is a precise technical term in some disciplines, but in all cases it is used to define some form of fractional reflectance. Albedo will vary with material color and (as we will later implement for glass materials) can also vary with incident viewing direction (the direction of the incoming ray).
Lambertian (diffuse) reflectance can either always scatter and attenuate light according to its reflectance \( \mathbf{R} \), or it can sometimes scatter (with probability \( (1 - \mathbf{R}) \)) with no attenuation (where a ray that isn't scattered is just absorbed into the material). It could also be a mixture of both those strategies. We will choose to always scatter, so implementing Lambertian materials becomes a simple task:
diff --git a/src/material.rs b/src/material.rs
index 13b34c3..5702b9c 100644
--- a/src/material.rs
+++ b/src/material.rs
@@ -1,7 +1,28 @@
use crate::{hittable::HitRecord, prelude::*};
pub trait Material {
fn scatter(&self, _r_in: Ray, _rec: HitRecord) -> Option<(Ray, Color)> {
None
}
}
+
+#[derive(Debug, Default, Clone, Copy)]
+pub struct Lambertian {
+ albedo: Color,
+}
+
+impl Lambertian {
+ pub fn new(albedo: Color) -> Self {
+ Self { albedo }
+ }
+}
+
+impl Material for Lambertian {
+ fn scatter(&self, _r_in: Ray, rec: HitRecord) -> Option<(Ray, Color)> {
+ let scatter_direction = rec.normal + random_unit_vector();
+ let scattered = Ray::new(rec.p, scatter_direction);
+ let attenuation = self.albedo;
+
+ Some((scattered, attenuation))
+ }
+}
Listing 61: [material.rs] The new lambertian material class
Note the third option: we could scatter with some fixed probability \( p \) and have attenuation be \( albedo/p \). Your choice.
If you read the code above carefully, you'll notice a small chance of mischief. If the random unit vector we generate is exactly opposite the normal vector, the two will sum to zero, which will result in a zero scatter direction vector. This leads to bad scenarios later on (infinities and NaNs), so we need to intercept the condition before we pass it on.
In service of this, we'll create a new vector method — vec3::near_zero()
— that returns true if the vector is very close to zero in all dimensions.
The following changes will use the C++ standard library function std::fabs
, which returns the absolute value of its input. 1
diff --git a/src/vec3.rs b/src/vec3.rs
index 4cb969b..3348fef 100644
--- a/src/vec3.rs
+++ b/src/vec3.rs
@@ -1,223 +1,229 @@
use std::{
fmt::Display,
ops::{Add, AddAssign, Div, DivAssign, Index, IndexMut, Mul, MulAssign, Neg, Sub},
};
#[derive(Debug, Default, Clone, Copy)]
pub struct Vec3 {
pub e: [f64; 3],
}
pub type Point3 = Vec3;
impl Vec3 {
pub fn new(e0: f64, e1: f64, e2: f64) -> Self {
Self { e: [e0, e1, e2] }
}
pub fn x(&self) -> f64 {
self.e[0]
}
pub fn y(&self) -> f64 {
self.e[1]
}
pub fn z(&self) -> f64 {
self.e[2]
}
pub fn length(&self) -> f64 {
f64::sqrt(self.length_squared())
}
pub fn length_squared(&self) -> f64 {
self.e[0] * self.e[0] + self.e[1] * self.e[1] + self.e[2] * self.e[2]
}
+ pub fn near_zero(&self) -> bool {
+ // Return true if the vector is close to zero in all dimensions.
+ const S: f64 = 1e-8;
+ self.e[0].abs() < S && self.e[1].abs() < S && self.e[2].abs() < S
+ }
+
pub fn random() -> Self {
Vec3 { e: rand::random() }
}
pub fn random_range(min: f64, max: f64) -> Self {
Vec3::new(
rand::random_range(min..max),
rand::random_range(min..max),
rand::random_range(min..max),
)
}
}
impl Neg for Vec3 {
type Output = Self;
fn neg(self) -> Self::Output {
Self::Output {
e: self.e.map(|e| -e),
}
}
}
impl Index<usize> for Vec3 {
type Output = f64;
fn index(&self, index: usize) -> &Self::Output {
&self.e[index]
}
}
impl IndexMut<usize> for Vec3 {
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
&mut self.e[index]
}
}
impl AddAssign for Vec3 {
fn add_assign(&mut self, rhs: Self) {
self.e[0] += rhs.e[0];
self.e[1] += rhs.e[1];
self.e[2] += rhs.e[2];
}
}
impl MulAssign<f64> for Vec3 {
fn mul_assign(&mut self, rhs: f64) {
self.e[0] *= rhs;
self.e[1] *= rhs;
self.e[2] *= rhs;
}
}
impl DivAssign<f64> for Vec3 {
fn div_assign(&mut self, rhs: f64) {
self.mul_assign(1.0 / rhs);
}
}
impl Display for Vec3 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} {} {}", self.e[0], self.e[1], self.e[2])
}
}
impl Add for Vec3 {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self::Output {
e: [
self.e[0] + rhs.e[0],
self.e[1] + rhs.e[1],
self.e[2] + rhs.e[2],
],
}
}
}
impl Sub for Vec3 {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
Self::Output {
e: [
self.e[0] - rhs.e[0],
self.e[1] - rhs.e[1],
self.e[2] - rhs.e[2],
],
}
}
}
impl Mul for Vec3 {
type Output = Self;
fn mul(self, rhs: Self) -> Self::Output {
Self::Output {
e: [
self.e[0] * rhs.e[0],
self.e[1] * rhs.e[1],
self.e[2] * rhs.e[2],
],
}
}
}
impl Mul<f64> for Vec3 {
type Output = Self;
fn mul(self, rhs: f64) -> Self::Output {
Self::Output {
e: [self.e[0] * rhs, self.e[1] * rhs, self.e[2] * rhs],
}
}
}
impl Mul<Vec3> for f64 {
type Output = Vec3;
fn mul(self, rhs: Vec3) -> Self::Output {
rhs.mul(self)
}
}
impl Div<f64> for Vec3 {
type Output = Self;
fn div(self, rhs: f64) -> Self::Output {
self * (1.0 / rhs)
}
}
#[inline]
pub fn dot(u: Vec3, v: Vec3) -> f64 {
u.e[0] * v.e[0] + u.e[1] * v.e[1] + u.e[2] * v.e[2]
}
#[inline]
pub fn cross(u: Vec3, v: Vec3) -> Vec3 {
Vec3::new(
u.e[1] * v.e[2] - u.e[2] * v.e[1],
u.e[2] * v.e[0] - u.e[0] * v.e[2],
u.e[0] * v.e[1] - u.e[1] * v.e[0],
)
}
#[inline]
pub fn unit_vector(v: Vec3) -> Vec3 {
v / v.length()
}
#[inline]
pub fn random_unit_vector() -> Vec3 {
loop {
let p = Vec3::random_range(-1.0, 1.0);
let lensq = p.length_squared();
if 1e-160 < lensq && lensq <= 1.0 {
return p / f64::sqrt(lensq);
}
}
}
#[inline]
pub fn random_on_hemisphere(normal: Vec3) -> Vec3 {
let on_unit_sphere = random_unit_vector();
if dot(on_unit_sphere, normal) > 0.0 {
on_unit_sphere
} else {
-on_unit_sphere
}
}
#[inline]
pub fn random_in_unit_disk() -> Vec3 {
loop {
let p = Vec3::new(
rand::random_range(-1.0..1.0),
rand::random_range(-1.0..1.0),
0.0,
);
if p.length_squared() < 1.0 {
return p;
}
}
}
Listing 62: [vec3.rs] The vec3::near_zero() method
diff --git a/src/material.rs b/src/material.rs
index 5702b9c..1b49e15 100644
--- a/src/material.rs
+++ b/src/material.rs
@@ -1,28 +1,34 @@
use crate::{hittable::HitRecord, prelude::*};
pub trait Material {
fn scatter(&self, _r_in: Ray, _rec: HitRecord) -> Option<(Ray, Color)> {
None
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct Lambertian {
albedo: Color,
}
impl Lambertian {
pub fn new(albedo: Color) -> Self {
Self { albedo }
}
}
impl Material for Lambertian {
fn scatter(&self, _r_in: Ray, rec: HitRecord) -> Option<(Ray, Color)> {
- let scatter_direction = rec.normal + random_unit_vector();
+ let mut scatter_direction = rec.normal + random_unit_vector();
+
+ // Catch degenerate scatter direction
+ if scatter_direction.near_zero() {
+ scatter_direction = rec.normal;
+ }
+
let scattered = Ray::new(rec.p, scatter_direction);
let attenuation = self.albedo;
Some((scattered, attenuation))
}
}
Listing 63: [material.rs] Lambertian scatter, bullet-proof
-
Rust version is
f64::abs
. ↩