diff --git a/rust/.gitignore b/rust/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/rust/.gitignore @@ -0,0 +1 @@ +/target diff --git a/rust/Cargo.lock b/rust/Cargo.lock new file mode 100644 index 0000000..f91fae4 --- /dev/null +++ b/rust/Cargo.lock @@ -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" diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 0000000..c4b7d7c --- /dev/null +++ b/rust/Cargo.toml @@ -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 diff --git a/rust/src/camera.rs b/rust/src/camera.rs new file mode 100644 index 0000000..7b00d19 --- /dev/null +++ b/rust/src/camera.rs @@ -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) + } +} diff --git a/rust/src/main.rs b/rust/src/main.rs new file mode 100644 index 0000000..74ee3b9 --- /dev/null +++ b/rust/src/main.rs @@ -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(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::() * 6.0) as usize; + let x = lo.x + (hi.x - lo.x) * rng.gen::(); + let y = lo.y + (hi.y - lo.y) * rng.gen::(); + let z = lo.z + (hi.z - lo.z) * rng.gen::(); + 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 { + 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::()) / (width - 1) as f64; + let v = ((height - 1 - j) as f64 + rng.gen::()) / (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 = 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, cols: std::ops::Range) -> 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| 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 = 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); +} diff --git a/rust/src/scene.rs b/rust/src/scene.rs new file mode 100644 index 0000000..2b276a9 --- /dev/null +++ b/rust/src/scene.rs @@ -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(&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::() { + 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, + pub lights: Vec, +} + +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 { + 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 +} diff --git a/rust/src/vec3.rs b/rust/src/vec3.rs new file mode 100644 index 0000000..7396f94 --- /dev/null +++ b/rust/src/vec3.rs @@ -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 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 for Vec3 { + type Output = Vec3; + fn mul(self, s: f64) -> Vec3 { + Vec3::new(self.x * s, self.y * s, self.z * s) + } +} + +impl Mul for f64 { + type Output = Vec3; + fn mul(self, v: Vec3) -> Vec3 { + v * self + } +} + +impl Div for Vec3 { + type Output = Vec3; + fn div(self, s: f64) -> Vec3 { + Vec3::new(self.x / s, self.y / s, self.z / s) + } +} + +impl DivAssign 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 { + 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) +}