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
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
Texturetrait whosevalue(u, v, p)method returns a color at a point SolidColor, a constant-color textureCheckerTexture, a three-dimensional checker texture- The addition of
to HitRecord, calculated bySphereandMovingSphere
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
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 to HitRecord
Textures require surface coordinates HitRecord.
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.
Coordinates on a Sphere
Map a point
The inverse solution is
The range of
Implement this as a simple free function.
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.
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 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.
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
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.
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
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
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
Texturetrait,SolidColor, andCheckerTexturetocommon/. - Added
to HitRecordand updatedSphereandMovingSphereto calculate the values withget_sphere_uv. - Changed
Lambertianfrom a material that stores a color to one that stores a texture. Compatibility withLambertian::new(Color)is preserved, andLambertian::with_texture(Arc<dyn Texture>)has been added. - Demonstrated the effect with
r204-checker-groundandr204-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.