Antialiasing
When a real camera takes a picture, there are usually no jaggies along edges because the edge pixels are a blend of some foreground and some background. We can get the same effect by averaging a bunch of samples inside each pixel. We abstract the camera class a bit so we can make a cooler camera later.
Some Random Number Utilities
One thing we need is a random number generator that returns real random numbers. We need a function that returns a canonical random number which by convention returns a random real in the range . The “less than” before the 1 is important as we will sometimes take advantage of that.
Rust does not have a built-in random number generator in the standard library. However, we can add an external library using Cargo. We can run below in the terminal:
cargo add rand
Cargo will install the rand
crate and update the dependency in Cargo.toml
.
We add the following code snippet:
use rand::Rng;
// Constants
pub use std::f64::consts::PI;
pub use std::f64::INFINITY;
// Utility functions
pub fn degrees_to_radians(degrees: f64) -> f64 {
degrees * PI / 180.0
}
pub fn random_double() -> f64 {
// Return a random real in [0.0, 1.0)
rand::thread_rng().gen()
}
pub fn random_double_range(min: f64, max: f64) -> f64 {
// Return a random real in [min, max)
min + (max - min) * random_double()
}
Note that the gen()
(opens in a new tab) function has a generic type in the return position. This means that the caller gets to choose which type gen()
will return. In this case, the compiler infers that we want f64
from the definition of the enclosing random_double()
function.
Generating Pixels with Multiple Samples
For a given pixel we have several samples within that pixel and send rays through each of the samples. The colors of these rays are then averaged:
Now’s a good time to create a Camera
struct to manage our virtual camera and the related tasks of scene sampling. The following struct implements a simple camera using the axis-aligned camera from before:
use crate::ray::Ray;
use crate::vec3::{Point3, Vec3};
pub struct Camera {
origin: Point3,
lower_left_corner: Point3,
horizontal: Vec3,
vertical: Vec3,
}
impl Camera {
pub fn new() -> Camera {
let aspect_ratio = 16.0 / 9.0;
let viewport_height = 2.0;
let viewport_width = aspect_ratio * viewport_height;
let focal_length = 1.0;
let origin = Point3::new(0.0, 0.0, 0.0);
let horizontal = Vec3::new(viewport_width, 0.0, 0.0);
let vertical = Vec3::new(0.0, viewport_height, 0.0);
let lower_left_corner =
origin - horizontal / 2.0 - vertical / 2.0 - Vec3::new(0.0, 0.0, focal_length);
Camera {
origin,
lower_left_corner,
horizontal,
vertical,
}
}
pub fn get_ray(&self, u: f64, v: f64) -> Ray {
Ray::new(
self.origin,
self.lower_left_corner + u * self.horizontal + v * self.vertical - self.origin,
)
}
}
To handle the multi-sampled color computation, we’ll update the write_color()
function. Rather than adding in a fractional contribution each time we accumulate more light to the color, just add the full color each iteration, and then perform a single divide at the end (by the number of samples) when writing out the color. In addition, we’ll add a handy utility function to the common
module: clamp(x, min, max)
, which clamps the value x
to the range [min, max]:
use rand::Rng;
// Constants
pub use std::f64::consts::PI;
pub use std::f64::INFINITY;
// Utility functions
pub fn degrees_to_radians(degrees: f64) -> f64 {
degrees * PI / 180.0
}
pub fn random_double() -> f64 {
// Return a random real in [0.0, 1.0)
rand::thread_rng().gen()
}
pub fn random_double_range(min: f64, max: f64) -> f64 {
// Return a random real in [min, max)
min + (max - min) * random_double()
}
pub fn clamp(x: f64, min: f64, max: f64) -> f64 {
if x < min {
return min;
}
if x > max {
return max;
}
x
}
use std::io::Write;
use crate::common;
use crate::vec3::Vec3;
// Type alias
pub type Color = Vec3;
pub fn write_color(out: &mut impl Write, pixel_color: Color, samples_per_pixel: i32) {
let mut r = pixel_color.x();
let mut g = pixel_color.y();
let mut b = pixel_color.z();
// Divide the color by the number of samples
let scale = 1.0 / samples_per_pixel as f64;
r *= scale;
g *= scale;
b *= scale;
// Write the translated [0, 255] value of each color component
writeln!(
out,
"{} {} {}",
(256.0 * common::clamp(r, 0.0, 0.999)) as i32,
(256.0 * common::clamp(g, 0.0, 0.999)) as i32,
(256.0 * common::clamp(b, 0.0, 0.999)) as i32,
)
.expect("writing color");
}
Main is also changed:
mod camera;
mod color;
mod common;
mod hittable;
mod hittable_list;
mod ray;
mod sphere;
mod vec3;
use std::io;
use camera::Camera;
use color::Color;
use hittable::{HitRecord, Hittable};
use hittable_list::HittableList;
use ray::Ray;
use sphere::Sphere;
use vec3::Point3;
fn ray_color(r: &Ray, world: &dyn Hittable) -> Color {
let mut rec = HitRecord::new();
if world.hit(r, 0.0, common::INFINITY, &mut rec) {
return 0.5 * (rec.normal + Color::new(1.0, 1.0, 1.0));
}
let unit_direction = vec3::unit_vector(r.direction());
let t = 0.5 * (unit_direction.y() + 1.0);
(1.0 - t) * Color::new(1.0, 1.0, 1.0) + t * Color::new(0.5, 0.7, 1.0)
}
fn main() {
// Image
const ASPECT_RATIO: f64 = 16.0 / 9.0;
const IMAGE_WIDTH: i32 = 400;
const IMAGE_HEIGHT: i32 = (IMAGE_WIDTH as f64 / ASPECT_RATIO) as i32;
const SAMPLES_PER_PIXEL: i32 = 100;
// World
let mut world = HittableList::new();
world.add(Box::new(Sphere::new(Point3::new(0.0, 0.0, -1.0), 0.5)));
world.add(Box::new(Sphere::new(Point3::new(0.0, -100.5, -1.0), 100.0)));
// Camera
let cam = Camera::new();
// Render
print!("P3\n{} {}\n255\n", IMAGE_WIDTH, IMAGE_HEIGHT);
for j in (0..IMAGE_HEIGHT).rev() {
eprint!("\rScanlines remaining: {} ", j);
for i in 0..IMAGE_WIDTH {
let mut pixel_color = Color::new(0.0, 0.0, 0.0);
for _ in 0..SAMPLES_PER_PIXEL {
let u = (i as f64 + common::random_double()) / (IMAGE_WIDTH - 1) as f64;
let v = (j as f64 + common::random_double()) / (IMAGE_HEIGHT - 1) as f64;
let r = cam.get_ray(u, v);
pixel_color += ray_color(&r, &world);
}
color::write_color(&mut io::stdout(), pixel_color, SAMPLES_PER_PIXEL);
}
}
eprint!("\nDone.\n");
}
Zooming into the image that is produced, we can see the difference in edge pixels:
By default, Cargo builds and runs the program in debug mode, which may be slow. You can try using cargo run --release
to build and run in release mode. It will be much faster.