326 lines
12 KiB
Rust
326 lines
12 KiB
Rust
//! The built-in scenes, selectable with `--scene NAME`.
|
|
|
|
use rand::rngs::SmallRng;
|
|
use rand::{Rng, SeedableRng};
|
|
|
|
use crate::scene::{Background, Material, Scene, SceneBuilder, View};
|
|
use crate::vec3::Vec3;
|
|
|
|
pub struct SceneInfo {
|
|
pub name: &'static str,
|
|
pub description: &'static str,
|
|
pub build: fn() -> Scene,
|
|
}
|
|
|
|
pub const SCENES: &[SceneInfo] = &[
|
|
SceneInfo {
|
|
name: "cornell",
|
|
description: "Cornell box from raytracer.py: glass, mirror and diffuse spheres, wall light",
|
|
build: cornell,
|
|
},
|
|
SceneInfo {
|
|
name: "caustics",
|
|
description: "Clear, amber and blue glass plus a mirror under one light, focusing caustics onto the floor",
|
|
build: caustics,
|
|
},
|
|
SceneInfo {
|
|
name: "spheres",
|
|
description: "Large scene: ~480 random spheres under a sky (exercises the BVH)",
|
|
build: spheres,
|
|
},
|
|
SceneInfo {
|
|
name: "night",
|
|
description: "The same sphere field at night, lit by dozens of glowing spheres (many-light sampling)",
|
|
build: night,
|
|
},
|
|
SceneInfo {
|
|
name: "classic",
|
|
description: "The classic Cornell box: ceiling light, two boxes, colour bleeding",
|
|
build: classic,
|
|
},
|
|
SceneInfo {
|
|
name: "city",
|
|
description: "Massive scale: a procedural city of ~125,000 objects under a low sun",
|
|
build: city,
|
|
},
|
|
];
|
|
|
|
pub fn find(name: &str) -> Option<&'static SceneInfo> {
|
|
SCENES.iter().find(|s| s.name == name)
|
|
}
|
|
|
|
fn diffuse(r: f64, g: f64, b: f64) -> Material {
|
|
Material::Diffuse { albedo: Vec3::new(r, g, b) }
|
|
}
|
|
|
|
fn mirror(r: f64, g: f64, b: f64) -> Material {
|
|
Material::Mirror { albedo: Vec3::new(r, g, b) }
|
|
}
|
|
|
|
/// Glass; `tint` is applied at every reflection/refraction, so colour
|
|
/// deepens with each pass through the surface.
|
|
fn glass(tint: Vec3) -> Material {
|
|
Material::Dielectric { ref_idx: 1.5, albedo: tint }
|
|
}
|
|
|
|
fn cornell() -> Scene {
|
|
let mut b = SceneBuilder::new(
|
|
View {
|
|
lookfrom: Vec3::new(278.0, 278.0, -800.0),
|
|
lookat: Vec3::new(278.0, 278.0, 278.0),
|
|
vfov: 40.0,
|
|
},
|
|
Background::Sky,
|
|
);
|
|
|
|
let white = diffuse(0.73, 0.73, 0.73);
|
|
b.plane(Vec3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0), diffuse(0.5, 0.5, 0.5));
|
|
b.plane(Vec3::new(0.0, 500.0, 0.0), Vec3::new(0.0, -1.0, 0.0), white);
|
|
b.plane(Vec3::new(0.0, 0.0, 500.0), Vec3::new(0.0, 0.0, -1.0), white);
|
|
b.plane(Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0), diffuse(0.65, 0.05, 0.05));
|
|
b.plane(Vec3::new(500.0, 0.0, 0.0), Vec3::new(-1.0, 0.0, 0.0), diffuse(0.12, 0.45, 0.15));
|
|
|
|
b.box_light(Vec3::new(0.0, 180.0, 180.0), Vec3::new(25.0, 220.0, 320.0), Vec3::splat(50.0));
|
|
|
|
b.sphere(Vec3::new(250.0, 180.0, 250.0), 80.0, glass(Vec3::ONE));
|
|
b.sphere(Vec3::new(150.0, 180.0, 380.0), 80.0, mirror(0.95, 0.95, 0.95));
|
|
b.sphere(Vec3::new(350.0, 160.0, 300.0), 60.0, diffuse(0.65, 0.05, 0.05));
|
|
b.build()
|
|
}
|
|
|
|
fn caustics() -> Scene {
|
|
let mut b = SceneBuilder::new(
|
|
View {
|
|
lookfrom: Vec3::new(0.0, 160.0, -280.0),
|
|
lookat: Vec3::new(20.0, 30.0, 120.0),
|
|
vfov: 38.0,
|
|
},
|
|
Background::Color(Vec3::ZERO),
|
|
);
|
|
|
|
b.plane(Vec3::ZERO, Vec3::new(0.0, 1.0, 0.0), diffuse(0.8, 0.8, 0.8));
|
|
b.plane(Vec3::new(0.0, 0.0, 520.0), Vec3::new(0.0, 0.0, -1.0), diffuse(0.6, 0.6, 0.6));
|
|
|
|
// One downward-facing panel above and well behind the spheres. A glass
|
|
// sphere focuses light about 1.5 radii from its centre; with the light
|
|
// ~48 degrees from vertical that focus lands on the floor just in front
|
|
// of each sphere, facing the camera. A smaller panel gives sharper
|
|
// caustics but makes them slower to converge.
|
|
b.rect_light(
|
|
Vec3::new(-30.0, 250.0, 330.0),
|
|
Vec3::new(30.0, 250.0, 390.0),
|
|
Vec3::new(0.0, -1.0, 0.0),
|
|
Vec3::splat(90.0),
|
|
);
|
|
|
|
b.sphere(Vec3::new(-75.0, 45.0, 110.0), 45.0, glass(Vec3::ONE));
|
|
b.sphere(Vec3::new(40.0, 38.0, 80.0), 38.0, glass(Vec3::new(1.0, 0.7, 0.3)));
|
|
b.sphere(Vec3::new(125.0, 30.0, 150.0), 30.0, glass(Vec3::new(0.4, 0.65, 1.0)));
|
|
b.sphere(Vec3::new(-10.0, 35.0, 210.0), 35.0, mirror(0.9, 0.9, 0.9));
|
|
b.build()
|
|
}
|
|
|
|
/// A grid of small random spheres around three large feature spheres (after
|
|
/// "Ray Tracing in One Weekend"). A `light_fraction` of the small spheres glow.
|
|
fn sphere_field(b: &mut SceneBuilder, light_fraction: f64) {
|
|
let mut rng = SmallRng::seed_from_u64(7);
|
|
b.plane(Vec3::ZERO, Vec3::new(0.0, 1.0, 0.0), diffuse(0.5, 0.5, 0.5));
|
|
|
|
let big = [
|
|
(Vec3::new(0.0, 1.0, 0.0), glass(Vec3::ONE)),
|
|
(Vec3::new(-4.0, 1.0, 0.0), diffuse(0.4, 0.2, 0.1)),
|
|
(Vec3::new(4.0, 1.0, 0.0), mirror(0.7, 0.6, 0.5)),
|
|
];
|
|
for (center, material) in big {
|
|
b.sphere(center, 1.0, material);
|
|
}
|
|
|
|
for a in -11..11 {
|
|
for c in -11..11 {
|
|
let center = Vec3::new(
|
|
a as f64 + 0.9 * rng.gen::<f64>(),
|
|
0.2,
|
|
c as f64 + 0.9 * rng.gen::<f64>(),
|
|
);
|
|
if big.iter().any(|(bc, _)| (center - Vec3::new(bc.x, 0.2, bc.z)).length() < 1.2) {
|
|
continue;
|
|
}
|
|
let rnd_color = |rng: &mut SmallRng| Vec3::new(rng.gen(), rng.gen(), rng.gen());
|
|
if rng.gen::<f64>() < light_fraction {
|
|
// Warm-to-white glow of varying strength.
|
|
let warmth: f64 = rng.gen();
|
|
let color = Vec3::new(1.0, 0.55 + 0.4 * warmth, 0.2 + 0.6 * warmth);
|
|
b.sphere_light(center, 0.2, color * (6.0 + 10.0 * rng.gen::<f64>()));
|
|
continue;
|
|
}
|
|
let choice: f64 = rng.gen();
|
|
let material = if choice < 0.75 {
|
|
let albedo = rnd_color(&mut rng).mul_elem(rnd_color(&mut rng));
|
|
Material::Diffuse { albedo }
|
|
} else if choice < 0.9 {
|
|
Material::Mirror { albedo: Vec3::splat(0.5) + 0.5 * rnd_color(&mut rng) }
|
|
} else {
|
|
glass(Vec3::ONE)
|
|
};
|
|
b.sphere(center, 0.2, material);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn spheres() -> Scene {
|
|
let mut b = SceneBuilder::new(
|
|
View {
|
|
lookfrom: Vec3::new(13.0, 2.0, 3.0),
|
|
lookat: Vec3::ZERO,
|
|
vfov: 20.0,
|
|
},
|
|
Background::Sky,
|
|
);
|
|
sphere_field(&mut b, 0.0);
|
|
b.build()
|
|
}
|
|
|
|
fn night() -> Scene {
|
|
let mut b = SceneBuilder::new(
|
|
View {
|
|
lookfrom: Vec3::new(13.0, 2.0, 3.0),
|
|
lookat: Vec3::ZERO,
|
|
vfov: 20.0,
|
|
},
|
|
Background::Color(Vec3::new(0.004, 0.006, 0.015)),
|
|
);
|
|
sphere_field(&mut b, 0.12);
|
|
b.build()
|
|
}
|
|
|
|
fn classic() -> Scene {
|
|
let mut b = SceneBuilder::new(
|
|
View {
|
|
lookfrom: Vec3::new(278.0, 278.0, -800.0),
|
|
lookat: Vec3::new(278.0, 278.0, 0.0),
|
|
vfov: 40.0,
|
|
},
|
|
Background::Color(Vec3::ZERO),
|
|
);
|
|
|
|
let white = diffuse(0.73, 0.73, 0.73);
|
|
b.plane(Vec3::ZERO, Vec3::new(0.0, 1.0, 0.0), white);
|
|
b.plane(Vec3::new(0.0, 555.0, 0.0), Vec3::new(0.0, -1.0, 0.0), white);
|
|
b.plane(Vec3::new(0.0, 0.0, 555.0), Vec3::new(0.0, 0.0, -1.0), white);
|
|
// The camera looks down +z with +x to its left, so red ends up on the left as usual.
|
|
b.plane(Vec3::new(555.0, 0.0, 0.0), Vec3::new(-1.0, 0.0, 0.0), diffuse(0.65, 0.05, 0.05));
|
|
b.plane(Vec3::ZERO, Vec3::new(1.0, 0.0, 0.0), diffuse(0.12, 0.45, 0.15));
|
|
|
|
// A flat panel just under the ceiling, shining down only.
|
|
b.rect_light(
|
|
Vec3::new(213.0, 554.0, 227.0),
|
|
Vec3::new(343.0, 554.0, 332.0),
|
|
Vec3::new(0.0, -1.0, 0.0),
|
|
Vec3::splat(15.0),
|
|
);
|
|
|
|
b.cuboid(Vec3::new(130.0, 0.0, 65.0), Vec3::new(295.0, 165.0, 230.0), white);
|
|
b.cuboid(Vec3::new(265.0, 0.0, 295.0), Vec3::new(430.0, 330.0, 460.0), white);
|
|
b.build()
|
|
}
|
|
|
|
/// A procedural city on a GRID x GRID lot layout: buildings (with stepped
|
|
/// tiers on the tall ones) that rise towards downtown, some mirror-glass
|
|
/// towers, and parks with sphere trees. Lit by a low sun and the sky.
|
|
fn city() -> Scene {
|
|
const GRID: usize = 400;
|
|
const LOT: f64 = 10.0;
|
|
// Every BLOCK-th row/column of lots is left empty as an avenue.
|
|
const BLOCK: usize = 5;
|
|
let extent = GRID as f64 * LOT;
|
|
let downtown = Vec3::new(0.5 * extent, 0.0, 0.5 * extent);
|
|
|
|
let mut b = SceneBuilder::new(
|
|
View {
|
|
lookfrom: Vec3::new(0.35 * extent, 120.0, 0.29 * extent),
|
|
lookat: Vec3::new(0.5 * extent, 0.0, 0.5 * extent),
|
|
vfov: 42.0,
|
|
},
|
|
Background::Sky,
|
|
);
|
|
let mut rng = SmallRng::seed_from_u64(2024);
|
|
|
|
b.plane(Vec3::ZERO, Vec3::new(0.0, 1.0, 0.0), diffuse(0.22, 0.22, 0.23));
|
|
|
|
// The sun: a distant sphere light 1.5 degrees in radius, low and off to
|
|
// the side of the view so the buildings cast long shadows across it.
|
|
let sun_dir = Vec3::new(0.85, 0.26, -0.35).normalize();
|
|
let sun_dist = 50_000.0;
|
|
b.sphere_light(
|
|
downtown + sun_dir * sun_dist,
|
|
sun_dist * 1.5_f64.to_radians().tan(),
|
|
Vec3::new(1.0, 0.88, 0.72) * 1600.0,
|
|
);
|
|
|
|
let concrete = [
|
|
Vec3::new(0.72, 0.70, 0.66),
|
|
Vec3::new(0.56, 0.56, 0.60),
|
|
Vec3::new(0.76, 0.70, 0.60),
|
|
Vec3::new(0.46, 0.43, 0.41),
|
|
Vec3::new(0.82, 0.80, 0.77),
|
|
Vec3::new(0.62, 0.52, 0.46),
|
|
];
|
|
let grass = diffuse(0.18, 0.36, 0.12);
|
|
|
|
for gx in 0..GRID {
|
|
for gz in 0..GRID {
|
|
if gx % BLOCK == BLOCK - 1 || gz % BLOCK == BLOCK - 1 {
|
|
continue;
|
|
}
|
|
let x0 = gx as f64 * LOT;
|
|
let z0 = gz as f64 * LOT;
|
|
let lot_center = Vec3::new(x0 + 0.5 * LOT, 0.0, z0 + 0.5 * LOT);
|
|
let r = (lot_center - downtown).length() / extent;
|
|
|
|
if rng.gen::<f64>() < 0.05 {
|
|
// Park: a lawn with a few round trees.
|
|
b.cuboid(Vec3::new(x0 + 0.5, 0.0, z0 + 0.5), Vec3::new(x0 + LOT - 0.5, 0.15, z0 + LOT - 0.5), grass);
|
|
for _ in 0..rng.gen_range(2..6) {
|
|
let radius = 1.0 + 1.2 * rng.gen::<f64>();
|
|
let c = Vec3::new(
|
|
x0 + radius + 0.5 + (LOT - 2.0 * radius - 1.0) * rng.gen::<f64>(),
|
|
radius + 0.8,
|
|
z0 + radius + 0.5 + (LOT - 2.0 * radius - 1.0) * rng.gen::<f64>(),
|
|
);
|
|
let shade = 0.7 + 0.3 * rng.gen::<f64>();
|
|
b.sphere(c, radius, diffuse(0.12 * shade, 0.32 * shade, 0.08 * shade));
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Height falls off from downtown, with a long tail of towers.
|
|
let skyline = 7.0 + 95.0 * (-(r * r) / 0.0068).exp();
|
|
let height = (skyline * (0.35 + 0.9 * rng.gen::<f64>().powi(2))).max(3.0);
|
|
// Tall buildings fill more of their lot.
|
|
let inset = if height > 40.0 { 0.4 + 0.6 * rng.gen::<f64>() } else { 0.8 + 1.4 * rng.gen::<f64>() };
|
|
let (mut lo, mut hi) = (
|
|
Vec3::new(x0 + inset, 0.0, z0 + inset),
|
|
Vec3::new(x0 + LOT - inset, height, z0 + LOT - inset),
|
|
);
|
|
let material = if height > 45.0 && rng.gen::<f64>() < 0.15 {
|
|
mirror(0.35, 0.45, 0.55)
|
|
} else {
|
|
let c = concrete[rng.gen_range(0..concrete.len())];
|
|
Material::Diffuse { albedo: c * (0.85 + 0.15 * rng.gen::<f64>()) }
|
|
};
|
|
b.cuboid(lo, hi, material);
|
|
|
|
// Tall buildings step in with up to three narrower tiers.
|
|
let mut tiers = if height > 30.0 { rng.gen_range(1..4) } else { 0 };
|
|
while tiers > 0 && hi.x - lo.x > 2.5 {
|
|
let step = 0.6 + 0.6 * rng.gen::<f64>();
|
|
let tier_height = (0.15 + 0.25 * rng.gen::<f64>()) * height;
|
|
lo = Vec3::new(lo.x + step, hi.y, lo.z + step);
|
|
hi = Vec3::new(hi.x - step, hi.y + tier_height, hi.z - step);
|
|
b.cuboid(lo, hi, material);
|
|
tiers -= 1;
|
|
}
|
|
}
|
|
}
|
|
b.build()
|
|
}
|