//! 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::(), 0.2, c as f64 + 0.9 * rng.gen::(), ); 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::() < 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::())); 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::() < 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::(); let c = Vec3::new( x0 + radius + 0.5 + (LOT - 2.0 * radius - 1.0) * rng.gen::(), radius + 0.8, z0 + radius + 0.5 + (LOT - 2.0 * radius - 1.0) * rng.gen::(), ); let shade = 0.7 + 0.3 * rng.gen::(); 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::().powi(2))).max(3.0); // Tall buildings fill more of their lot. let inset = if height > 40.0 { 0.4 + 0.6 * rng.gen::() } else { 0.8 + 1.4 * rng.gen::() }; 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::() < 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::()) } }; 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::(); let tier_height = (0.15 + 0.25 * rng.gen::()) * 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() }