7. Antialiasing
Ray Tracing in One Weekend (v3.2.3): 7 Antialiasing
The scene from the previous chapter is complete, but looking closely at the sphere's outline you can see jagged "staircasing" artifacts. This is because each pixel's color is determined by a single ray. This chapter implements supersampling antialiasing: casting multiple randomized rays per pixel and averaging their colors.


Without and with antialiasing
Why Jaggies Occur
When a pixel is computed from a single ray, that ray either hits the object ("object color") or misses it ("background color"). A pixel on the boundary can only take one of two values, which produces the jagged edges.
In a real camera, light from near a boundary is blended together during the exposure, naturally producing a smooth transition. To replicate this, we cast several slightly jittered rays per pixel and average the results.
Preparing the Random Number Generator
C++ random numbers
The original book uses rand() and RAND_MAX to produce values in <random> header is available.
#include <random>
inline double random_double() {
static std::uniform_real_distribution<double> distribution(0.0, 1.0);
static std::mt19937 generator;
return distribution(generator);
}Personally, when it comes to random numbers, I think of the issues in "Culdcept Saga." The pseudo-random number generator (PRNG) used by the developers had a short period and a significant bias in its distribution. Furthermore, the inappropriate implementation of multiple processes referencing random numbers based on the same seed sequence led to undesirable consequences, such as the same results appearing in sequence and a tendency for the player who goes first to have an unfair advantage, which clearly negatively impacted the game experience. It's likely that they were using lower-order bits of a linear congruential generator. In C++11, the <random> library includes PRNGs like the Mersenne Twister, and using the standard library generally avoids significant issues in terms of speed, memory usage, and period for most applications. However, around 2006, when "Culdcept Saga" was being developed, such implementations were only available in the Boost C++ Libraries.
Rust random numbers (Xorshift64)
Rust's standard library contains no random number generator. External crates are an option, but here we implement the Xorshift64 algorithm, which works well in WASM.
Xorshift is a fast pseudo-random number generator built entirely from bitwise operations. Mathematically it is a type of linear-feedback shift register (LFSR). It has good statistical properties and is more than sufficient quality for ray tracing.
use std::cell::Cell;
thread_local! {
static RNG_STATE: Cell<u64> = Cell::new(0x123456789abcdef0);
}
pub fn random_double() -> f64 {
RNG_STATE.with(|state| {
let mut s = state.get();
s ^= s << 13; // XOR
s ^= s >> 7; // Shift
s ^= s << 17; // XOR
state.set(s);
// Convert the top 53 bits to a floating-point value in [0, 1)
(s >> 11) as f64 * (1.0 / (1u64 << 53) as f64)
})
}
pub fn random_double_range(min: f64, max: f64) -> f64 {
min + (max - min) * random_double()
}thread_local! is a macro that declares a thread-local variable. Because WASM is single-threaded, this functions effectively as a global variable. Cell<T> provides interior mutability — it allows the value to be modified even through a shared &self reference.
The Camera Struct
Since chapter 1.4, the camera setup has been inlined directly into each render function. This chapter introduces a dedicated Camera struct.
The C++ implementation:
class camera {
public:
camera() {
double aspect_ratio = 16.0 / 9.0;
double viewport_height = 2.0;
double viewport_width = aspect_ratio * viewport_height;
double focal_length = 1.0;
origin = point3(0, 0, 0);
horizontal = vec3(viewport_width, 0, 0);
vertical = vec3(0, viewport_height, 0);
lower_left_corner = origin - horizontal/2 - vertical/2 - vec3(0, 0, focal_length);
}
ray get_ray(double u, double v) const {
return ray(origin, lower_left_corner + u*horizontal + v*vertical - origin);
}
private:
point3 origin;
point3 lower_left_corner;
vec3 horizontal;
vec3 vertical;
};The Rust implementation:
pub struct Camera {
origin: Point3,
lower_left_corner: Point3,
horizontal: Vec3,
vertical: Vec3,
}
impl Camera {
pub fn new() -> Self {
let aspect_ratio = 16.0_f64 / 9.0;
let viewport_height = 2.0_f64;
let viewport_width = aspect_ratio * viewport_height;
let focal_length = 1.0_f64;
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,
)
}
}Updating write_color
With antialiasing, the accumulated sum of multiple sample colors is passed to write_color, which divides by the sample count to obtain the average. The result is clamped to stay within
On clamp
The C++ version adds the following to rtweekend.h:
inline double clamp(double x, double min, double max) {
if (x < min) return min;
if (x > max) return max;
return x;
}In Rust, clamp is a built-in method on f64:
let r = r.clamp(0.0, 0.999);Old vs. new write_color
The previous implementation was hardcoded to one sample:
// Old (chapters 1.2 through 1.6)
pub fn write_color(pixel_color: Color) -> String {
let ir = (255.999 * pixel_color.x()) as i32;
let ig = (255.999 * pixel_color.y()) as i32;
let ib = (255.999 * pixel_color.z()) as i32;
format!("{} {} {}", ir, ig, ib)
}The new version takes samples_per_pixel and applies scaling and clamping.
The C++ implementation:
void write_color(std::ostream& out, const color& pixel_color, int samples_per_pixel) {
auto r = pixel_color.x();
auto g = pixel_color.y();
auto b = pixel_color.z();
auto scale = 1.0 / samples_per_pixel;
r *= scale;
g *= scale;
b *= scale;
out << static_cast<int>(256 * clamp(r, 0.0, 0.999)) << ' '
<< static_cast<int>(256 * clamp(g, 0.0, 0.999)) << ' '
<< static_cast<int>(256 * clamp(b, 0.0, 0.999)) << '\n';
}The Rust implementation:
/// Averages `pixel_color` over `samples_per_pixel` samples and converts to an "R G B" string.
pub fn write_color(pixel_color: Color, samples_per_pixel: i32) -> String {
let scale = 1.0 / samples_per_pixel as f64;
let r = (pixel_color.x() * scale).clamp(0.0, 0.999);
let g = (pixel_color.y() * scale).clamp(0.0, 0.999);
let b = (pixel_color.z() * scale).clamp(0.0, 0.999);
let ir = (256.0 * r) as i32;
let ig = (256.0 * g) as i32;
let ib = (256.0 * b) as i32;
format!("{} {} {}", ir, ig, ib)
}With samples_per_pixel = 1, the result is identical to the previous implementation.
Because the signature of write_color changes, the crates from earlier chapters (r103-vec3 through r106-hittable-list) must be updated from write_color(pixel_color) to write_color(pixel_color, 1).
Updating the Render Loop
The updated C++ main loop:
int samples_per_pixel = 100;
camera cam;
for (int j = image_height - 1; j >= 0; j--) {
for (int i = 0; i < image_width; i++) {
color pixel_color(0, 0, 0);
for (int s = 0; s < samples_per_pixel; s++) {
auto u = (i + random_double()) / (image_width - 1);
auto v = (j + random_double()) / (image_height - 1);
ray r = cam.get_ray(u, v);
pixel_color += ray_color(r, world);
}
write_color(std::cout, pixel_color, samples_per_pixel);
}
}The Rust implementation:
for j in (0..image_height).rev() {
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 + random_double()) / (image_width - 1) as f64;
let v = (j as f64 + random_double()) / (image_height - 1) as f64;
let r = camera.get_ray(u, v);
pixel_color += ray_color(&r, &world);
}
output.push_str(&format!("{}\n", write_color(pixel_color, samples_per_pixel)));
}
}The sample loop uses _ as the loop variable (for _ in) to tell the Rust compiler that the counter is intentionally unused. Naming an unused variable without the _ prefix produces an unused-variable warning.
Adding to the common Crate
The Camera struct and random-number utilities will be needed in subsequent chapters, so they are added to common.
common/src/camera.rs
use crate::vec3::{Point3, Vec3};
use crate::ray::Ray;
/// A pinhole camera with a fixed 16:9 aspect ratio.
pub struct Camera {
origin: Point3,
lower_left_corner: Point3,
horizontal: Vec3,
vertical: Vec3,
}
impl Camera {
/// Creates a camera with a fixed 16:9 aspect ratio.
pub fn new() -> Self {
let aspect_ratio = 16.0_f64 / 9.0;
let viewport_height = 2.0_f64;
let viewport_width = aspect_ratio * viewport_height;
let focal_length = 1.0_f64;
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 }
}
/// Returns a ray for the given `(u, v)` viewport coordinates in [0, 1].
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,
)
}
}common/src/utils.rs
use std::cell::Cell;
/// Floating-point infinity.
pub const INFINITY: f64 = f64::INFINITY;
/// The mathematical constant π.
pub const PI: f64 = std::f64::consts::PI;
/// Converts degrees to radians.
pub fn degrees_to_radians(degrees: f64) -> f64 {
degrees * PI / 180.0
}
thread_local! {
static RNG_STATE: Cell<u64> = Cell::new(0x123456789abcdef0);
}
/// Returns a random f64 in the range `[0, 1)`.
pub fn random_double() -> f64 {
RNG_STATE.with(|state| {
let mut s = state.get();
s ^= s << 13;
s ^= s >> 7;
s ^= s << 17;
state.set(s);
(s >> 11) as f64 * (1.0 / (1u64 << 53) as f64)
})
}
/// Returns a random f64 in the range `[min, max)`.
pub fn random_double_range(min: f64, max: f64) -> f64 {
min + (max - min) * random_double()
}common/src/lib.rs (updated)
pub mod vec3;
pub use vec3::{Color, Point3, Vec3, cross, dot, unit_vector, write_color};
pub mod ray;
pub use ray::Ray;
pub mod hittable;
pub use hittable::{HitRecord, Hittable};
pub mod sphere;
pub use sphere::Sphere;
pub mod hittable_list;
pub use hittable_list::HittableList;
pub mod camera;
pub use camera::Camera;
pub mod utils;
pub use utils::{INFINITY, PI, degrees_to_radians, random_double, random_double_range};Implementing the r107-antialiasing Crate
Set up r107-antialiasing/Cargo.toml:
[package]
name = "r107-antialiasing"
version = "0.1.0"
edition = "2024"
[dependencies]
common = { workspace = true }Complete implementation of r107-antialiasing/src/lib.rs:
use common::{
Camera, Color, Hittable, HittableList, Point3, Sphere,
unit_vector, write_color, Ray, random_double,
};
fn ray_color(r: &Ray, world: &dyn Hittable) -> Color {
if let Some(rec) = world.hit(r, 0.0, f64::INFINITY) {
return 0.5 * (rec.normal + Color::new(1.0, 1.0, 1.0));
}
let unit_direction = 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)
}
pub fn render_image() -> String {
// Image dimensions
let aspect_ratio = 16.0_f64 / 9.0;
let image_width = 384_i32;
let image_height = (image_width as f64 / aspect_ratio) as i32;
let samples_per_pixel = 100_i32;
// 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 camera = Camera::new();
// Render
let mut output = String::new();
output.push_str("P3\n");
output.push_str(&format!("{} {}\n", image_width, image_height));
output.push_str("255\n");
for j in (0..image_height).rev() {
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 + random_double()) / (image_width - 1) as f64;
let v = (j as f64 + random_double()) / (image_height - 1) as f64;
let r = camera.get_ray(u, v);
pixel_color += ray_color(&r, &world);
}
output.push_str(&format!("{}\n", write_color(pixel_color, samples_per_pixel)));
}
}
output
}WASM Export
Add to raytracing-demos/src/lib.rs:
// Chapter 1.7: Antialiasing
#[wasm_bindgen]
pub fn render_antialiasing() -> String {
r107_antialiasing::render_image()
}