Rust port

This commit is contained in:
David 2026-09-26 08:13:33 +02:00
parent 3a1864647b
commit 9502137d77
7 changed files with 1303 additions and 0 deletions

1
rust/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

601
rust/Cargo.lock generated Normal file
View file

@ -0,0 +1,601 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "bitflags"
version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
[[package]]
name = "bumpalo"
version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "bytemuck"
version = "1.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
[[package]]
name = "byteorder-lite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "cfg-if"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
[[package]]
name = "console"
version = "0.15.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
dependencies = [
"encode_unicode",
"libc",
"once_cell",
"unicode-width",
"windows-sys",
]
[[package]]
name = "crc32fast"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78"
dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
[[package]]
name = "either"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
[[package]]
name = "encode_unicode"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]]
name = "fdeflate"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
dependencies = [
"simd-adler32",
]
[[package]]
name = "flate2"
version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
dependencies = [
"crc32fast",
"miniz_oxide 0.9.1",
"zlib-rs",
]
[[package]]
name = "futures-core"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
[[package]]
name = "futures-task"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
[[package]]
name = "futures-util"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "getrandom"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "image"
version = "0.25.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
dependencies = [
"bytemuck",
"byteorder-lite",
"moxcms",
"num-traits",
"png",
]
[[package]]
name = "indicatif"
version = "0.17.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235"
dependencies = [
"console",
"number_prefix",
"portable-atomic",
"unicode-width",
"web-time",
]
[[package]]
name = "js-sys"
version = "0.3.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5"
dependencies = [
"cfg-if",
"futures-util",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "libm"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
"simd-adler32",
]
[[package]]
name = "miniz_oxide"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"
dependencies = [
"adler2",
"simd-adler32",
]
[[package]]
name = "moxcms"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
dependencies = [
"num-traits",
"pxfm",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
"libm",
]
[[package]]
name = "number_prefix"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "pathtracer"
version = "0.1.0"
dependencies = [
"image",
"indicatif",
"rand",
"rand_distr",
"rayon",
]
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "png"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
dependencies = [
"bitflags",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide 0.8.9",
]
[[package]]
name = "portable-atomic"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "pxfm"
version = "0.1.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "rand_distr"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31"
dependencies = [
"num-traits",
"rand",
]
[[package]]
name = "rayon"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "rustversion"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "simd-adler32"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "syn"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
[[package]]
name = "unicode-width"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasm-bindgen"
version = "0.2.129"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.129"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.129"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn 3.0.6",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.129"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6"
dependencies = [
"unicode-ident",
]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "zerocopy"
version = "0.8.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "zlib-rs"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112"

17
rust/Cargo.toml Normal file
View file

@ -0,0 +1,17 @@
[package]
name = "pathtracer"
version = "0.1.0"
edition = "2021"
description = "Cornell Box path tracer (Rust port of raytracer.py)"
[dependencies]
rand = { version = "0.8", features = ["small_rng"] }
rand_distr = "0.4"
rayon = "1"
indicatif = "0.17"
image = { version = "0.25", default-features = false, features = ["png"] }
[profile.release]
opt-level = 3
lto = true
codegen-units = 1

33
rust/src/camera.rs Normal file
View file

@ -0,0 +1,33 @@
use crate::scene::Ray;
use crate::vec3::Vec3;
pub struct Camera {
origin: Vec3,
lower_left_corner: Vec3,
horizontal: Vec3,
vertical: Vec3,
}
impl Camera {
pub fn new(lookfrom: Vec3, lookat: Vec3, vup: Vec3, vfov: f64, aspect: f64, focus_dist: f64) -> Self {
let half_height = (vfov.to_radians() / 2.0).tan();
let half_width = aspect * half_height;
let w = (lookfrom - lookat).normalize();
let u = vup.cross(w).normalize();
let v = w.cross(u);
let horizontal = focus_dist * half_width * u;
let vertical = focus_dist * half_height * v;
let lower_left_corner = lookfrom - horizontal / 2.0 - vertical / 2.0 - focus_dist * w;
Camera {
origin: lookfrom,
lower_left_corner,
horizontal,
vertical,
}
}
pub fn get_ray(&self, s: f64, t: f64) -> Ray {
let direction = self.lower_left_corner + s * self.horizontal + t * self.vertical - self.origin;
Ray::new(self.origin, direction)
}
}

239
rust/src/main.rs Normal file
View file

@ -0,0 +1,239 @@
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);
}

268
rust/src/scene.rs Normal file
View file

@ -0,0 +1,268 @@
use rand::Rng;
use rand_distr::StandardNormal;
use crate::vec3::{reflect, refract, schlick, Vec3};
pub const EPSILON: f64 = 1e-6;
#[derive(Clone, Copy, Debug)]
pub struct Ray {
pub origin: Vec3,
pub direction: Vec3,
}
impl Ray {
pub fn new(origin: Vec3, direction: Vec3) -> Self {
Ray {
origin,
direction: direction.normalize(),
}
}
pub fn at(&self, t: f64) -> Vec3 {
self.origin + t * self.direction
}
}
#[derive(Clone, Copy, Debug)]
pub enum Material {
Diffuse { albedo: Vec3 },
Mirror { albedo: Vec3 },
Dielectric { ref_idx: f64, albedo: Vec3 },
Light { emission: Vec3 },
}
pub struct HitRecord {
pub point: Vec3,
pub normal: Vec3,
pub front_face: bool,
pub material: Material,
}
impl HitRecord {
/// Orients the normal against the incoming ray, as in the Python version.
fn new(r: &Ray, t: f64, outward: Vec3, material: Material) -> Self {
let front_face = r.direction.dot(outward) < 0.0;
HitRecord {
point: r.at(t),
normal: if front_face { outward } else { -outward },
front_face,
material,
}
}
}
impl Material {
pub fn emission(&self) -> Vec3 {
match self {
Material::Light { emission } => *emission,
_ => Vec3::ZERO,
}
}
/// Returns (attenuation, scattered ray), or None if the ray is absorbed.
pub fn scatter<R: Rng>(&self, ray_in: &Ray, rec: &HitRecord, rng: &mut R) -> Option<(Vec3, Ray)> {
match *self {
Material::Diffuse { albedo } => {
let jitter = Vec3::new(
rng.sample(StandardNormal),
rng.sample(StandardNormal),
rng.sample(StandardNormal),
);
let mut target = rec.normal + jitter * 0.5;
if target.length() < 1e-8 {
target = rec.normal;
}
Some((albedo, Ray::new(rec.point, target)))
}
Material::Mirror { albedo } => {
let reflected = reflect(ray_in.direction, rec.normal);
if reflected.dot(rec.normal) <= 0.0 {
return None;
}
Some((albedo, Ray::new(rec.point, reflected)))
}
Material::Dielectric { ref_idx, albedo } => {
let ratio = if rec.front_face { 1.0 / ref_idx } else { ref_idx };
let d = ray_in.direction;
let cos_theta = (-d.dot(rec.normal)).min(1.0);
let sin_theta = (1.0 - cos_theta * cos_theta).max(0.0).sqrt();
let cannot_refract = ratio * sin_theta > 1.0;
let direction = if cannot_refract || schlick(cos_theta, ratio) > rng.gen::<f64>() {
reflect(d, rec.normal)
} else {
refract(d, rec.normal, ratio).unwrap_or_else(|| reflect(d, rec.normal))
};
Some((albedo, Ray::new(rec.point, direction)))
}
Material::Light { .. } => None,
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum Shape {
Sphere { center: Vec3, radius: f64 },
Plane { point: Vec3, normal: Vec3 },
Box { min: Vec3, max: Vec3 },
}
impl Shape {
fn hit(&self, r: &Ray, t_min: f64, t_max: f64) -> Option<(f64, Vec3)> {
match *self {
Shape::Sphere { center, radius } => {
let oc = r.origin - center;
let a = r.direction.dot(r.direction);
let b = oc.dot(r.direction);
let c = oc.dot(oc) - radius * radius;
let disc = b * b - a * c;
if disc < 0.0 {
return None;
}
let sq = disc.sqrt();
let mut root = (-b - sq) / a;
if root < t_min || root > t_max {
root = (-b + sq) / a;
if root < t_min || root > t_max {
return None;
}
}
let outward = (r.at(root) - center).normalize();
Some((root, outward))
}
Shape::Plane { point, normal } => {
let denom = r.direction.dot(normal);
if denom.abs() < 1e-12 {
return None;
}
let t = (point - r.origin).dot(normal) / denom;
if t < t_min || t > t_max {
return None;
}
Some((t, normal))
}
Shape::Box { min, max } => {
let mut t_lo = t_min;
let mut t_hi = t_max;
// Python indexes with -1 when no slab raises t_lo, which means the z axis.
let mut hit_axis = 2;
let mut inv_ds = [0.0; 3];
for axis in 0..3 {
let d = r.direction[axis];
let inv_d = if d.abs() > 1e-12 { 1.0 / d } else { 1e12 };
inv_ds[axis] = inv_d;
let mut t0 = (min[axis] - r.origin[axis]) * inv_d;
let mut t1 = (max[axis] - r.origin[axis]) * inv_d;
if inv_d < 0.0 {
std::mem::swap(&mut t0, &mut t1);
}
if t0 > t_lo {
t_lo = t0;
hit_axis = axis;
}
t_hi = t_hi.min(t1);
if t_hi < t_lo {
return None;
}
}
if t_lo >= t_max || t_lo <= t_min {
return None;
}
let sign = if inv_ds[hit_axis] > 0.0 { 1.0 } else { -1.0 };
let mut n = [0.0; 3];
n[hit_axis] = sign;
Some((t_lo, Vec3::new(n[0], n[1], n[2])))
}
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct Object {
pub shape: Shape,
pub material: Material,
}
/// An axis-aligned box light, sampled explicitly for next event estimation.
#[derive(Clone, Copy, Debug)]
pub struct BoxLight {
pub min: Vec3,
pub max: Vec3,
pub emission: Vec3,
}
#[derive(Default)]
pub struct Scene {
pub objects: Vec<Object>,
pub lights: Vec<BoxLight>,
}
impl Scene {
pub fn add(&mut self, shape: Shape, material: Material) {
self.objects.push(Object { shape, material });
}
#[allow(dead_code)] // see cornell_box()
pub fn add_box_light(&mut self, min: Vec3, max: Vec3, emission: Vec3) {
self.lights.push(BoxLight { min, max, emission });
self.add(Shape::Box { min, max }, Material::Light { emission });
}
pub fn hit(&self, r: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
let mut closest: Option<(f64, Vec3, Material)> = None;
let mut min_t = t_max;
for obj in &self.objects {
if let Some((t, outward)) = obj.shape.hit(r, t_min, min_t) {
if t < min_t {
min_t = t;
closest = Some((t, outward, obj.material));
}
}
}
closest.map(|(t, outward, material)| HitRecord::new(r, t, outward, material))
}
pub fn blocked(&self, origin: Vec3, direction: Vec3, t_max: f64) -> bool {
let r = Ray::new(origin, direction);
self.objects
.iter()
.any(|obj| obj.shape.hit(&r, EPSILON, t_max).is_some())
}
}
pub fn cornell_box() -> Scene {
let mut scene = Scene::default();
let white = Material::Diffuse { albedo: Vec3::new(0.73, 0.73, 0.73) };
let gray = Material::Diffuse { albedo: Vec3::new(0.5, 0.5, 0.5) };
let red = Material::Diffuse { albedo: Vec3::new(0.65, 0.05, 0.05) };
let green = Material::Diffuse { albedo: Vec3::new(0.12, 0.45, 0.15) };
let mirror = Material::Mirror { albedo: Vec3::new(0.95, 0.95, 0.95) };
let glass = Material::Dielectric { ref_idx: 1.5, albedo: Vec3::ONE };
let plane = |p: Vec3, n: Vec3| Shape::Plane { point: p, normal: n.normalize() };
scene.add(plane(Vec3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0)), gray);
scene.add(plane(Vec3::new(0.0, 500.0, 0.0), Vec3::new(0.0, -1.0, 0.0)), white);
scene.add(plane(Vec3::new(0.0, 0.0, 500.0), Vec3::new(0.0, 0.0, -1.0)), white);
scene.add(plane(Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0)), red);
scene.add(plane(Vec3::new(500.0, 0.0, 0.0), Vec3::new(-1.0, 0.0, 0.0)), green);
// Like raytracer.py, the light is added as a plain object rather than via
// add_box_light, so `lights` stays empty and NEE in `trace` never runs.
// Using add_box_light here enables the Python NEE code as-written, which
// over-brightens the image (it double-counts light and its pdf is off).
scene.add(
Shape::Box {
min: Vec3::new(0.0, 180.0, 180.0),
max: Vec3::new(25.0, 220.0, 320.0),
},
Material::Light { emission: Vec3::splat(50.0) },
);
let sphere = |c: Vec3, r: f64| Shape::Sphere { center: c, radius: r };
scene.add(sphere(Vec3::new(250.0, 180.0, 250.0), 80.0), glass);
scene.add(sphere(Vec3::new(150.0, 180.0, 380.0), 80.0), mirror);
scene.add(sphere(Vec3::new(350.0, 160.0, 300.0), 60.0), red);
scene
}

144
rust/src/vec3.rs Normal file
View file

@ -0,0 +1,144 @@
use std::ops::{Add, AddAssign, Div, DivAssign, Index, Mul, Neg, Sub};
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Vec3 {
pub x: f64,
pub y: f64,
pub z: f64,
}
impl Vec3 {
pub const ZERO: Vec3 = Vec3::new(0.0, 0.0, 0.0);
pub const ONE: Vec3 = Vec3::new(1.0, 1.0, 1.0);
pub const fn new(x: f64, y: f64, z: f64) -> Self {
Vec3 { x, y, z }
}
pub fn splat(v: f64) -> Self {
Vec3::new(v, v, v)
}
pub fn dot(self, o: Vec3) -> f64 {
self.x * o.x + self.y * o.y + self.z * o.z
}
pub fn cross(self, o: Vec3) -> Vec3 {
Vec3::new(
self.y * o.z - self.z * o.y,
self.z * o.x - self.x * o.z,
self.x * o.y - self.y * o.x,
)
}
pub fn length(self) -> f64 {
self.dot(self).sqrt()
}
pub fn normalize(self) -> Vec3 {
let n = self.length();
if n > 1e-12 {
self / n
} else {
self
}
}
/// Component-wise product.
pub fn mul_elem(self, o: Vec3) -> Vec3 {
Vec3::new(self.x * o.x, self.y * o.y, self.z * o.z)
}
pub fn map(self, f: impl Fn(f64) -> f64) -> Vec3 {
Vec3::new(f(self.x), f(self.y), f(self.z))
}
pub fn mean(self) -> f64 {
(self.x + self.y + self.z) / 3.0
}
}
impl Index<usize> for Vec3 {
type Output = f64;
fn index(&self, i: usize) -> &f64 {
match i {
0 => &self.x,
1 => &self.y,
_ => &self.z,
}
}
}
impl Add for Vec3 {
type Output = Vec3;
fn add(self, o: Vec3) -> Vec3 {
Vec3::new(self.x + o.x, self.y + o.y, self.z + o.z)
}
}
impl AddAssign for Vec3 {
fn add_assign(&mut self, o: Vec3) {
*self = *self + o;
}
}
impl Sub for Vec3 {
type Output = Vec3;
fn sub(self, o: Vec3) -> Vec3 {
Vec3::new(self.x - o.x, self.y - o.y, self.z - o.z)
}
}
impl Neg for Vec3 {
type Output = Vec3;
fn neg(self) -> Vec3 {
Vec3::new(-self.x, -self.y, -self.z)
}
}
impl Mul<f64> for Vec3 {
type Output = Vec3;
fn mul(self, s: f64) -> Vec3 {
Vec3::new(self.x * s, self.y * s, self.z * s)
}
}
impl Mul<Vec3> for f64 {
type Output = Vec3;
fn mul(self, v: Vec3) -> Vec3 {
v * self
}
}
impl Div<f64> for Vec3 {
type Output = Vec3;
fn div(self, s: f64) -> Vec3 {
Vec3::new(self.x / s, self.y / s, self.z / s)
}
}
impl DivAssign<f64> for Vec3 {
fn div_assign(&mut self, s: f64) {
*self = *self / s;
}
}
pub fn reflect(d: Vec3, n: Vec3) -> Vec3 {
d - 2.0 * d.dot(n) * n
}
pub fn refract(incident: Vec3, normal: Vec3, eta: f64) -> Option<Vec3> {
let cos_i = (-incident.dot(normal)).min(1.0);
let sin_t_sq = eta * eta * (1.0 - cos_i * cos_i);
if sin_t_sq > 1.0 {
return None;
}
let cos_t = (1.0 - sin_t_sq).max(0.0).sqrt();
Some(eta * incident + (eta * cos_i - cos_t) * normal)
}
pub fn schlick(cos: f64, ref_idx: f64) -> f64 {
let r0 = (1.0 - ref_idx) / (1.0 + ref_idx);
let r0 = r0 * r0;
r0 + (1.0 - r0) * (1.0 - cos).powi(5)
}