ai_pathtracer/rust/src/main.rs
2026-09-26 08:13:33 +02:00

239 lines
8.3 KiB
Rust

mod camera;
mod scene;
mod vec3;
use std::f64::consts::PI;
use std::time::Instant;
use indicatif::{ProgressBar, ProgressStyle};
use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng};
use rayon::prelude::*;
use camera::Camera;
use scene::{cornell_box, Material, Ray, Scene, EPSILON};
use vec3::Vec3;
fn trace<R: Rng>(scene: &Scene, ray: &Ray, depth: u32, max_depth: u32, rng: &mut R) -> Vec3 {
if depth >= max_depth {
return Vec3::ZERO;
}
let Some(rec) = scene.hit(ray, EPSILON, f64::INFINITY) else {
let t = 0.5 * (ray.direction.y + 1.0);
return Vec3::ONE * (1.0 - t) + Vec3::new(0.5, 0.7, 1.0) * t;
};
let emitted = rec.material.emission();
if let Material::Light { .. } = rec.material {
return emitted;
}
let Some((attenuation, scattered)) = rec.material.scatter(ray, &rec, rng) else {
return emitted;
};
let indirect = trace(scene, &scattered, depth + 1, max_depth, rng);
let mut color = emitted + attenuation.mul_elem(indirect);
if let Material::Diffuse { albedo } = rec.material {
let mut direct = Vec3::ZERO;
for light in &scene.lights {
let (lo, hi) = (light.min, light.max);
for _ in 0..2 {
let face = (rng.gen::<f64>() * 6.0) as usize;
let x = lo.x + (hi.x - lo.x) * rng.gen::<f64>();
let y = lo.y + (hi.y - lo.y) * rng.gen::<f64>();
let z = lo.z + (hi.z - lo.z) * rng.gen::<f64>();
let lp = match face {
0 => Vec3::new(lo.x, y, z),
1 => Vec3::new(hi.x, y, z),
2 => Vec3::new(x, lo.y, z),
3 => Vec3::new(x, hi.y, z),
4 => Vec3::new(x, y, lo.z),
_ => Vec3::new(x, y, hi.z),
};
let to_light = lp - rec.point;
let dist_sq = to_light.dot(to_light);
if dist_sq < 1e-8 {
continue;
}
let dist = dist_sq.sqrt();
let d_to_light = to_light / dist;
let cos_at_rec = rec.normal.dot(d_to_light);
if cos_at_rec <= 0.0 {
continue;
}
if scene.blocked(rec.point + EPSILON * rec.normal, d_to_light, dist - EPSILON) {
continue;
}
let light_area = 6.0 * (hi.x - lo.x) * (hi.y - lo.y) * (hi.z - lo.z);
let pdf = light_area / (dist_sq * cos_at_rec);
if pdf < 1e-12 {
continue;
}
let brdf = albedo / PI;
direct += light.emission.mul_elem(brdf) * (cos_at_rec / pdf);
break;
}
}
if direct.dot(direct) > 0.0 {
color = emitted + attenuation.mul_elem(indirect + direct);
}
}
color
}
struct Settings {
width: usize,
height: usize,
spp: u32,
max_depth: u32,
output: String,
}
fn render(scene: &Scene, s: &Settings) -> Vec<Vec3> {
let (width, height) = (s.width, s.height);
let cam = Camera::new(
Vec3::new(278.0, 278.0, -800.0),
Vec3::new(278.0, 278.0, 278.0),
Vec3::new(0.0, 1.0, 0.0),
40.0,
width as f64 / height as f64,
1078.0,
);
let pb = ProgressBar::new(height as u64);
pb.set_style(
ProgressStyle::with_template(
"{spinner:.green} Rendering [{bar:50.bright_green/blue}] {percent:>3}% {pos}/{len} rows {elapsed_precise} ETA {eta_precise}",
)
.unwrap()
.progress_chars("=> "),
);
let mut image = vec![Vec3::ZERO; width * height];
image
.par_chunks_mut(width)
.enumerate()
.for_each(|(j, row)| {
for (i, px) in row.iter_mut().enumerate() {
let mut rng = SmallRng::seed_from_u64((j * width + i) as u64 ^ 0x9E37_79B9_7F4A_7C15);
let mut color = Vec3::ZERO;
for _ in 0..s.spp {
let u = (i as f64 + rng.gen::<f64>()) / (width - 1) as f64;
let v = ((height - 1 - j) as f64 + rng.gen::<f64>()) / (height - 1) as f64;
let ray = cam.get_ray(u, v);
color += trace(scene, &ray, 0, s.max_depth, &mut rng);
}
color /= s.spp as f64;
*px = color.map(|c| c.max(0.0).sqrt().min(1.0));
}
pb.inc(1);
});
pb.finish();
image
}
fn save_png(image: &[Vec3], width: usize, height: usize, path: &str) -> image::ImageResult<()> {
let bytes: Vec<u8> = image
.iter()
.flat_map(|c| [c.x, c.y, c.z])
.map(|c| (c.clamp(0.0, 1.0) * 255.0) as u8)
.collect();
image::save_buffer(path, &bytes, width as u32, height as u32, image::ColorType::Rgb8)
}
fn region_mean(image: &[Vec3], width: usize, rows: std::ops::Range<usize>, cols: std::ops::Range<usize>) -> f64 {
let n = rows.len() * cols.len();
if n == 0 {
return 0.0;
}
let sum: f64 = rows
.flat_map(|j| cols.clone().map(move |i| image[j * width + i].mean()))
.sum();
sum / n as f64
}
fn print_region_table(image: &[Vec3], width: usize, height: usize) {
// Same regions as the Python version (which slices the Floor/Ceiling/Center
// columns by height-based indices).
let (h_start, h_end) = (height / 3, 2 * height / 3);
let clamp_cols = |r: std::ops::Range<usize>| r.start.min(width)..r.end.min(width);
let regions = [
("Left Wall", 0..height, 0..width / 4),
("Right Wall", 0..height, 3 * width / 4..width),
("Floor", 2 * height / 3..height, clamp_cols(h_start..h_end)),
("Ceiling", 0..height / 3, clamp_cols(h_start..h_end)),
("Center", h_start..h_end, clamp_cols(h_start..h_end)),
];
println!("Region Brightness Analysis");
println!("{:<12} {:>10} Visual", "Region", "Brightness");
for (name, rows, cols) in regions {
let brightness = region_mean(image, width, rows, cols) * 255.0;
let filled = ((brightness / 255.0 * 20.0) as usize).min(20);
let bar = format!("{}{}", "#".repeat(filled), "-".repeat(20 - filled));
let color = if brightness > 80.0 { 32 } else if brightness > 40.0 { 33 } else { 31 };
println!("{name:<12} {brightness:>10.1} \x1b[{color}m{bar}\x1b[0m");
}
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let arg = |i: usize, default: &str| args.get(i).cloned().unwrap_or_else(|| default.to_string());
let parse = |i: usize, default: &str| -> usize {
arg(i, default).parse().unwrap_or_else(|_| {
eprintln!("usage: pathtracer [width] [height] [samples_per_pixel] [max_bounces] [output]");
std::process::exit(2);
})
};
let settings = Settings {
width: parse(1, "400"),
height: parse(2, "300"),
spp: parse(3, "50") as u32,
max_depth: parse(4, "15") as u32,
output: arg(5, "cornell_box.png"),
};
if settings.width < 2 || settings.height < 2 || settings.spp == 0 {
eprintln!("width and height must be >= 2 and samples_per_pixel >= 1");
std::process::exit(2);
}
println!("=== Cornell Box Path Tracer (Rust) ===");
println!("Features: reflections, refraction/caustics, multiple bounces, NEE");
println!();
println!("Resolution: {}x{}", settings.width, settings.height);
println!("Samples per pixel: {}", settings.spp);
println!("Max bounces: {}", settings.max_depth);
println!("Threads: {}", rayon::current_num_threads());
println!("Output: {}", settings.output);
println!();
let scene = cornell_box();
let start = Instant::now();
let image = render(&scene, &settings);
let elapsed = start.elapsed().as_secs_f64();
if let Err(e) = save_png(&image, settings.width, settings.height, &settings.output) {
eprintln!("failed to write {}: {e}", settings.output);
std::process::exit(1);
}
println!();
println!("Rendered {}", settings.output);
println!(
"Total time: {:.2}s | Speed: {:.2} MPix*samp/s",
elapsed,
(settings.width * settings.height) as f64 * settings.spp as f64 / elapsed / 1e6
);
println!();
print_region_table(&image, settings.width, settings.height);
}