From a9b2d17183d6372436ee41853ca9aa7dfc614892 Mon Sep 17 00:00:00 2001 From: David Date: Sat, 26 Sep 2026 11:01:10 +0200 Subject: [PATCH] city scene --- README.md | 1 + rust/src/scenes.rs | 106 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/README.md b/README.md index e262fe3..3d034ee 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,7 @@ It uses all cores via rayon, shows a progress bar and (for the `cornell` scene) | `spheres` | Large scene: ~470 random diffuse/mirror/glass spheres under a sky (after *Ray Tracing in One Weekend*). The BVH makes it ~11x faster than testing every object | | `night` | The same field at night, lit by 61 glowing spheres. Each shading point picks one light, weighted by power / distance², so many lights cost no more shadow rays than one | | `classic` | The classic Cornell box: one-sided ceiling panel light, two boxes, colour bleeding from the walls | +| `city` | Massive scale: a procedural 400x400-lot city of ~124,000 objects (buildings with stepped tiers, mirror-glass towers, parks with trees) under a low sun. Renders 800x600 at 64 spp in ~2.5 s; without the BVH it is ~1000x slower | Scenes are defined in [`rust/src/scenes.rs`](rust/src/scenes.rs) with a small builder (`plane`, `sphere`, `cuboid`, `box_light`, `sphere_light`, `rect_light`). Every emitter must be added with a `*_light` method so light sampling knows about it. diff --git a/rust/src/scenes.rs b/rust/src/scenes.rs index fc79d8d..5189576 100644 --- a/rust/src/scenes.rs +++ b/rust/src/scenes.rs @@ -38,6 +38,11 @@ pub const SCENES: &[SceneInfo] = &[ 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> { @@ -218,3 +223,104 @@ fn classic() -> Scene { 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() +}