Skip to content
Copied!
published on 2026-09-06

4. Solid Textures ​

Ray Tracing: The Next Week (v3.2.3): 4 Solid Textures / 2.4 Solid Textures

In computer graphics, a texture is any function that determines the color of an object's surface procedurally or by referring to an image. A solid texture uses only world-space coordinates (x,y,z) as its input, making it well suited to representing materials such as wood or marble whose patterns appear to extend through their interiors.

In this chapter, I promote colors to textures so that a material can refer to an arbitrary texture rather than only a constant color. Four new elements are required:

  • A Texture trait whose value(u, v, p) method returns a color at a point
  • SolidColor, a constant-color texture
  • CheckerTexture, a three-dimensional checker texture
  • The addition of (u,v) to HitRecord, calculated by Sphere and MovingSphere

I change the member stored by Lambertian from Color to Arc<dyn Texture>, while retaining the compatible Lambertian::new(Color) signature by wrapping the color in SolidColor internally. This allows the r1XX crates from Book 1 and the r201 and r203 crates from Book 2 to continue building unchanged.

The Texture Trait and SolidColor ​

common/src/texture.rs
rust
pub trait Texture: Send + Sync {
    fn value(&self, u: f64, v: f64, p: Point3) -> Color;
}

pub struct SolidColor {
    pub color_value: Color,
}

impl SolidColor {
    pub fn new(color_value: Color) -> Self {
        SolidColor { color_value }
    }
}

impl Texture for SolidColor {
    fn value(&self, _u: f64, _v: f64, _p: Point3) -> Color {
        self.color_value
    }
}

The Send + Sync bounds leave room for passing an Arc<dyn Texture> to another thread or extending the renderer with parallelism in the future. This follows the same convention as the Material trait.

Adding (u,v) to HitRecord ​

Textures require surface coordinates (u,v), so I store these two values in HitRecord.

common/src/hittable.rs
rust
pub struct HitRecord {
    pub p: Point3,
    pub normal: Vec3,
    pub t: f64,
    pub u: f64,   // Added.
    pub v: f64,   // Added.
    pub front_face: bool,
    pub mat: Option<Arc<dyn Material>>,
}

Only Sphere::hit and MovingSphere::hit construct HitRecord values, so both implementations must be updated.

(u,v) Coordinates on a Sphere ​

Map a point p on the unit sphere to latitude and longitude, then normalize the result to [0,1]2. For the point p=(x,y,z) on the sphere, let

y=−cos⁡θx=−cos⁡ϕsin⁡θz=−sin⁡ϕsin⁡θ

The inverse solution is

θ=arccos⁡(−y),ϕ=atan2(−z,x)+π

The range of atan2 is (−π,π]. Add π to shift it to [0,2π], then normalize the coordinates as

u=ϕ2π,v=θπ.

Implement this as a simple free function.

common/src/sphere.rs
rust
pub fn get_sphere_uv(p: Point3) -> (f64, f64) {
    // p.y() is in [-1, 1]; theta is in [0, pi]; phi is in [0, 2*pi].
    let theta = (-p.y()).acos();
    let phi = (-p.z()).atan2(p.x()) + PI;
    (phi / (2.0 * PI), theta / PI)
}

In Sphere::hit, outward_normal is the unit vector from the center to the surface and therefore already represents a point on the unit sphere. It can be passed directly to the function.

common/src/sphere.rs
rust
let outward_normal = (p - self.center) / self.radius;
let (u, v) = get_sphere_uv(outward_normal);
let mut rec = HitRecord {
    t: root,
    u, v,
    p,
    normal: outward_normal,
    front_face: false,
    mat: self.material.clone(),
};

MovingSphere::hit likewise calculates (u,v) from outward_normal, which lies on the unit sphere after division by the radius.

Differences Between C++ and Rust ​

The original C++ defines get_sphere_uv as a private static method on the sphere class and returns its values through the output parameters double& u and double& v. The Rust implementation uses a free function that returns the tuple (f64, f64) and makes it reusable from MovingSphere through pub use. Output parameters are uncommon in Rust; returning values is the standard style.

Converting Lambertian to Use Textures ​

Lambertian now stores a texture rather than a color.

common/src/material.rs
rust
pub struct Lambertian {
    pub albedo: Arc<dyn Texture>,
}

impl Lambertian {
    /// Compatibility constructor: wraps a constant color in `SolidColor`.
    pub fn new(albedo: Color) -> Self {
        Lambertian {
            albedo: Arc::new(SolidColor::new(albedo)),
        }
    }

    /// Variant that accepts any texture directly.
    pub fn with_texture(albedo: Arc<dyn Texture>) -> Self {
        Lambertian { albedo }
    }
}

impl Material for Lambertian {
    fn scatter(&self, r_in: &Ray, rec: &HitRecord) -> Option<(Color, Ray)> {
        let mut scatter_direction = rec.normal + random_unit_vector();
        if scatter_direction.near_zero() {
            scatter_direction = rec.normal;
        }
        let scattered = Ray::with_time(rec.p, scatter_direction, r_in.time());
        let attenuation = self.albedo.value(rec.u, rec.v, rec.p);
        Some((attenuation, scattered))
    }
}

Every existing call to Lambertian::new(Color) continues to work, while new code that needs a texture calls Lambertian::with_texture(...). The crates from Book 1 and the first part of Book 2 therefore continue to build unchanged.

Differences Between C++ and Rust ​

The original C++ changes the lambertian member to shared_ptr<texture> albedo and overloads two constructors. Rust supports neither constructors nor function overloading, so I follow the Vec::new and Vec::with_capacity convention from the previous chapter and distinguish new from with_texture by name. The call to albedo->value(...) inside Material::scatter follows the same flow as the original.

Metal and Dielectric do not currently need textures. Metal uses a solid-color albedo, while a dielectric has no albedo at all, so neither type changes.

Checker Textures ​

The sign of the product sin⁡(10x)sin⁡(10y)sin⁡(10z) forms a three-dimensional checker pattern with a period of π/10. I use its sign to select between two child textures.

common/src/texture.rs
rust
pub struct CheckerTexture {
    pub even: Arc<dyn Texture>,
    pub odd: Arc<dyn Texture>,
}

impl CheckerTexture {
    pub fn new(even: Arc<dyn Texture>, odd: Arc<dyn Texture>) -> Self {
        CheckerTexture { even, odd }
    }

    /// Convenience constructor from two solid colors.
    pub fn from_colors(c1: Color, c2: Color) -> Self {
        CheckerTexture {
            even: Arc::new(SolidColor::new(c1)),
            odd: Arc::new(SolidColor::new(c2)),
        }
    }
}

impl Texture for CheckerTexture {
    fn value(&self, u: f64, v: f64, p: Point3) -> Color {
        let sines = (10.0 * p.x()).sin() * (10.0 * p.y()).sin() * (10.0 * p.z()).sin();
        if sines < 0.0 {
            self.odd.value(u, v, p)
        } else {
            self.even.value(u, v, p)
        }
    }
}

Because even and odd are both Arc<dyn Texture>, a child may be another CheckerTexture, or a future Perlin noise or image texture. This is the idea behind the shader network proposed by Pat Hanrahan in the 1980s.

Demo 1: r204-checker-ground ​

In the random_scene() from Chapter 3, replace only the ground sphere, the gray sphere with radius 1000, with a checker texture created by CheckerTexture::from_colors.

r204-checker-ground/src/lib.rs
rust
let checker = Arc::new(CheckerTexture::from_colors(
    Color::new(0.2, 0.3, 0.1),
    Color::new(0.9, 0.9, 0.9),
));
world.add(Box::new(Sphere::with_material(
    Point3::new(0.0, -1000.0, 0.0),
    1000.0,
    Arc::new(Lambertian::with_texture(checker)),
)));

The rest of the scene, including the small spheres, the three large spheres above them, the BVH, and the shutter interval, remains the same as in Chapter 3. Thanks to the BVH, rendering at 100 samples per pixel still completes in a reasonable time. The WASM demo at the top of the page shows the result. Because the ground is a sphere, the checker pattern appears strongly curved.

Demo 2: r204-checker-spheres ​

I now implement the two_spheres() scene corresponding to Image 3 in the original. It is a simple scene containing two spheres of radius 10, centered at (0,10,0) and (0,−10,0), that share the same checker texture.

r204-checker-spheres/src/lib.rs
rust
fn two_spheres() -> HittableList {
    let mut objects = HittableList::new();

    let checker = Arc::new(CheckerTexture::from_colors(
        Color::new(0.2, 0.3, 0.1),
        Color::new(0.9, 0.9, 0.9),
    ));
    objects.add(Box::new(Sphere::with_material(
        Point3::new(0.0, -10.0, 0.0),
        10.0,
        Arc::new(Lambertian::with_texture(checker.clone())),
    )));
    objects.add(Box::new(Sphere::with_material(
        Point3::new(0.0, 10.0, 0.0),
        10.0,
        Arc::new(Lambertian::with_texture(checker)),
    )));

    objects
}

The camera parameters are lookfrom = (13, 2, 3), lookat = (0, 0, 0), vfov = 20°, an aperture of 0 with no depth of field, and a shutter interval of [0,1]. Because the scene contains no moving objects, no motion blur appears.

Calling checker.clone() shares the same Arc between the two Lambertian materials so that only one texture value exists. This increments the reference count without duplicating the texture itself.

Summary ​

  • Added the Texture trait, SolidColor, and CheckerTexture to common/.
  • Added (u,v) to HitRecord and updated Sphere and MovingSphere to calculate the values with get_sphere_uv.
  • Changed Lambertian from a material that stores a color to one that stores a texture. Compatibility with Lambertian::new(Color) is preserved, and Lambertian::with_texture(Arc<dyn Texture>) has been added.
  • Demonstrated the effect with r204-checker-ground and r204-checker-spheres.

The abstraction of colors as textures is now in place. The next chapter builds Perlin noise on this foundation to generate natural patterns resembling wood and marble.