ai_pathtracer/rust/src/scenes.rs
2026-09-26 11:01:10 +02:00

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()
}