diff --git a/assignment-1c/.gitignore b/assignment-1c/.gitignore new file mode 100644 index 000000000..6987804 --- /dev/null +++ b/assignment-1c/.gitignore @@ -0,0 +1,10 @@ +/target +/assignment-1b +/raytracer1b +/examples/*.png +*.ppm +*.zip +*.pdf +perf.data* +flamegraph.svg +showcase.png diff --git a/assignment-1c/ASSIGNMENT.md b/assignment-1c/ASSIGNMENT.md new file mode 100644 index 000000000..e69de29 diff --git a/assignment-1c/Cargo.lock b/assignment-1c/Cargo.lock new file mode 100644 index 000000000..bc1bc4f --- /dev/null +++ b/assignment-1c/Cargo.lock @@ -0,0 +1,838 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anyhow" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "assignment-1b" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64", + "clap", + "derivative", + "nalgebra", + "num", + "ordered-float", + "rand", + "rayon", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bytemuck" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c041d3eab048880cb0b86b256447da3f18859a163c3b8d8893f4e6368abe6393" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f13b9c79b5d1dd500d20ef541215a6423c75829ef43117e1b4d17fd8af0b5d76" +dependencies = [ + "bitflags", + "clap_derive", + "clap_lex", + "is-terminal", + "once_cell", + "strsim", + "termcolor", +] + +[[package]] +name = "clap_derive" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "684a277d672e91966334af371f1a7b5833f9aa00b07c84e92fbce95e00208ce8" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "783fe232adfca04f90f56201b26d79682d4cd2625e0bc7290b95123afe558ade" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7d6c6f8c91b4b9ed43484ad1a938e393caf35960fce7f82a040497207bd8e9e" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "is-terminal" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dfb6c8100ccc63462345b67d1bbc3679177c75ee4bf59bf29c8b1d110b8189" +dependencies = [ + "hermit-abi", + "io-lifetimes", + "rustix", + "windows-sys", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "linux-raw-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "matrixmultiply" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add85d4dd35074e6fedc608f8c8f513a3548619a9024b751949ef0e8e45a4d84" +dependencies = [ + "rawpointer", +] + +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "nalgebra" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6515c882ebfddccaa73ead7320ca28036c4bc84c9bcca3cc0cbba8efe89223a" +dependencies = [ + "approx", + "matrixmultiply", + "nalgebra-macros", + "num-complex", + "num-rational", + "num-traits", + "simba", + "typenum", +] + +[[package]] +name = "nalgebra-macros" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d232c68884c0c99810a5a4d333ef7e47689cfd0edc85efc9e54e1e6bf5212766" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "num-complex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e0d21255c828d6f128a1e41534206671e8c3ea0c62f32291e808dc82cff17d" +dependencies = [ + "num-traits", + "serde", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" + +[[package]] +name = "ordered-float" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d84eb1409416d254e4a9c8fa56cc24701755025b458f0fcd8e59e1f5f40c23bf" +dependencies = [ + "num-traits", +] + +[[package]] +name = "os_str_bytes" +version = "6.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "paste" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba" + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +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 = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "rayon" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "356a0625f1954f730c0201cdab48611198dc6ce21f4acff55089b5a78e6e835b" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + +[[package]] +name = "rustix" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fdebc4b395b7fbb9ab11e462e20ed9051e7b16e42d24042c776eca0ac81b03" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "safe_arch" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "794821e4ccb0d9f979512f9c1973480123f9bd62a90d74ab0f9426fcf8f4a529" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "simba" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50582927ed6f77e4ac020c057f37a268fc6aebc29225050365aacbb9deeeddc4" +dependencies = [ + "approx", + "num-complex", + "num-traits", + "paste", + "wide", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wide" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae41ecad2489a1655c8ef8489444b0b113c0a0c795944a3572a0931cf7d2525c" +dependencies = [ + "bytemuck", + "safe_arch", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" diff --git a/assignment-1c/Cargo.toml b/assignment-1c/Cargo.toml new file mode 100644 index 000000000..bd9ba71 --- /dev/null +++ b/assignment-1c/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "assignment-1b" +authors = ["Michael Zhang "] +version = "0.1.0" +edition = "2021" + +# For profiling with flamegraphs +[profile.release] +debug = true + +# Optimize for size when creating handin +[profile.release-handin] +inherits = "release" +strip = true +lto = true + +[[bin]] +name = "raytracer1b" +path = "src/main.rs" + +[dependencies] +anyhow = "1.0.68" +base64 = "0.21.0" +clap = { version = "4.1.4", features = ["cargo", "derive"] } +derivative = "2.2.0" +nalgebra = "0.32.1" +num = { version = "0.4.0", features = ["serde"] } +ordered-float = "3.4.0" +rand = "0.8.5" +rayon = "1.6.1" +tracing = "0.1.37" +tracing-subscriber = "0.3.16" diff --git a/assignment-1c/Makefile b/assignment-1c/Makefile new file mode 100644 index 000000000..6355786 --- /dev/null +++ b/assignment-1c/Makefile @@ -0,0 +1,54 @@ +.PHONY: all clean + +.PRECIOUS: $(EXAMPLES_PPM) + +RAYTRACER_FLAGS := +DOCKER := docker +ZIP := zip +PANDOC := pandoc +CONVERT := convert + +HANDIN := ./hw1b.michael.zhang.zip +BINARY := ./raytracer1b +WRITEUP := ./writeup.pdf +SHOWCASE := ./showcase.png +SOURCES := Cargo.toml $(shell find -name "*.rs") + +EXAMPLES := $(shell find examples -name "*.txt") +EXAMPLES_PPM := $(patsubst %.txt,%.ppm,$(EXAMPLES)) +EXAMPLES_PNG := $(patsubst %.txt,%.png,$(EXAMPLES)) + +all: $(HANDIN) + +$(BINARY): $(SOURCES) + mkdir -p target/docker + $(DOCKER) run \ + --rm \ + -v "$(shell pwd)":/usr/src/myapp \ + -v cargo-registry:/usr/local/cargo \ + --user "$(shell id -u)":"$(shell id -g)" \ + -w /usr/src/myapp \ + -e CARGO_TARGET_DIR=/usr/src/myapp/target/docker \ + rust \ + cargo build --profile release-handin + mv target/docker/release-handin/raytracer1b $@ + +$(HANDIN): $(BINARY) $(WRITEUP) Makefile Cargo.toml Cargo.lock README.md $(EXAMPLES_PNG) $(EXAMPLES_PPM) $(SHOWCASE) + $(ZIP) -r $@ src examples $^ + +$(SHOWCASE): examples/soft-shadow-demo.png + cp $< $@ + +examples/%.ppm: examples/%.txt $(SOURCES) + cargo run --release -- -o $@ $(RAYTRACER_FLAGS) $< + +examples/%.png: examples/%.ppm + convert $< $@ + +writeup.pdf: writeup.md $(EXAMPLES_PNG) + $(PANDOC) -o $@ $< + +clean: + rm -rf target/docker \ + $(HANDIN) $(BINARY) $(WRITEUP) $(SHOWCASE) \ + $(EXAMPLES_PPM) $(EXAMPLES_PNG) diff --git a/assignment-1c/README.md b/assignment-1c/README.md new file mode 100644 index 000000000..c0a5eca --- /dev/null +++ b/assignment-1c/README.md @@ -0,0 +1,29 @@ +# Raycaster + +## Bundle contents + +Writeup is located at `/writeup.pdf`. + +The binary can be found at `/raytracer1b`. Run `./raytracer1b --help` to see +how to use it. The binary has been built using the Rust Docker image, which +should have an environment similar to CSELabs. If there is trouble running the +binary, try building from source, as documented below. + +Examples are found in the `examples` directory. The text files are the input +sources, and the ppm files are the corresponding outputs. They have been +generated by running this program. For convenience, pngs have also been provided +using imagemagick. + +## Showcase image + +The showcase image can be found at `/showcase.png`. + +## Building from source + +The Makefile currently uses Docker to produce a more consistent build. If you +have a Rust+Cargo toolchain installed locally, it's also possible to build the +source using just: + + cargo build --release + +The binary will be found in `target/release`. diff --git a/assignment-1c/examples/Hw1bSample1.txt b/assignment-1c/examples/Hw1bSample1.txt new file mode 100755 index 000000000..75ea085 --- /dev/null +++ b/assignment-1c/examples/Hw1bSample1.txt @@ -0,0 +1,18 @@ +imsize 512 512 +eye 0 0 0 +viewdir 0 0.1 -1 +hfov 90 +updir 0 1 0 +bkgcolor 0.1 0.1 0.1 + +light -1 -1 -1 0 0.9 0.5 0.05 + +mtlcolor 0 1 0 1 1 1 0.6 0.2 0.2 10 +sphere 0 1.5 -4 1 + +mtlcolor 0 1 0 1 1 1 0.1 0.8 0.2 10 +sphere -1.275 -0.75 -4 1 + +mtlcolor 0 1 0 1 1 1 0.1 0.2 0.8 10 +sphere 1.275 -0.75 -4 1 + diff --git a/assignment-1c/examples/attenuation-demo.txt b/assignment-1c/examples/attenuation-demo.txt new file mode 100644 index 000000000..f90d239 --- /dev/null +++ b/assignment-1c/examples/attenuation-demo.txt @@ -0,0 +1,15 @@ +imsize 600 200 +eye 0 0 15 +viewdir 0 0 -1 +hfov 90 +updir 0 1 0 +bkgcolor 0.4 0.4 0.4 + +attlight -15 10 5 1 1 1 1 0 0.25 0.03 + +mtlcolor 0.6 1 0.8 1 1 1 0.4 1 0.5 15 +sphere -10 0 0 2 +sphere -5 0 0 2 +sphere 0 0 0 2 +sphere 5 0 0 2 +sphere 10 0 0 2 diff --git a/assignment-1c/examples/cylinder.txt b/assignment-1c/examples/cylinder.txt new file mode 100644 index 000000000..d4392e0 --- /dev/null +++ b/assignment-1c/examples/cylinder.txt @@ -0,0 +1,12 @@ +imsize 640 480 +eye 0 0 15 +viewdir 0 0 -1 +hfov 60 +updir 0 1 0 +bkgcolor 0.1 0.1 0.1 + +light -10 10 -3 0 0 0.4 0.4 +light 10 10 -3 1 0.4 0 0.4 + +mtlcolor 0.5 1 0.5 0.2 0.4 0.8 0.2 0.4 0 10 +cylinder -2 4 -3 0 0 5 1 4 diff --git a/assignment-1c/examples/depth-cueing-demo.txt b/assignment-1c/examples/depth-cueing-demo.txt new file mode 100644 index 000000000..2b368c0 --- /dev/null +++ b/assignment-1c/examples/depth-cueing-demo.txt @@ -0,0 +1,15 @@ +imsize 640 480 +eye -2.5 2 -5 +viewdir -0.2 0 -1 +hfov 90 +updir 0 1 0 +bkgcolor 0.4 0.4 0.4 + +depthcueing 0.4 0.4 0.4 1 0.1 60 0 +light 0 5 -7.5 1 1 1 1 + +mtlcolor 1 0.8 0.7 1 1 1 0.6 0.6 0 10 +sphere -5 2 -10 2 +sphere -3 2 -20 2 +sphere 1.5 2 -30 2 +sphere 13 2 -60 2 diff --git a/assignment-1c/examples/ka-demo.txt b/assignment-1c/examples/ka-demo.txt new file mode 100644 index 000000000..395d198 --- /dev/null +++ b/assignment-1c/examples/ka-demo.txt @@ -0,0 +1,23 @@ +imsize 600 200 +eye 0 0 15 +viewdir 0 0 -1 +hfov 90 +updir 0 1 0 +bkgcolor 0.4 0.4 0.4 + +light 0 5 5 1 0.7 0.7 0.8 + +mtlcolor 0.6 1 0.4 1 1 1 0 0.4 0 1 +sphere -10 0 0 2 + +mtlcolor 0.6 1 0.4 1 1 1 0.2 0.4 0 1 +sphere -5 0 0 2 + +mtlcolor 0.6 1 0.4 1 1 1 0.4 0.4 0 1 +sphere 0 0 0 2 + +mtlcolor 0.6 1 0.4 1 1 1 0.6 0.4 0 1 +sphere 5 0 0 2 + +mtlcolor 0.6 1 0.4 1 1 1 0.8 0.4 0 1 +sphere 10 0 0 2 diff --git a/assignment-1c/examples/kd-demo.txt b/assignment-1c/examples/kd-demo.txt new file mode 100644 index 000000000..10097f6 --- /dev/null +++ b/assignment-1c/examples/kd-demo.txt @@ -0,0 +1,23 @@ +imsize 600 200 +eye 0 0 15 +viewdir 0 0 -1 +hfov 90 +updir 0 1 0 +bkgcolor 0.4 0.4 0.4 + +light 0 5 5 1 0.7 0.7 0.8 + +mtlcolor 0.6 1 0.4 1 1 1 0.2 0 0 1 +sphere -10 0 0 2 + +mtlcolor 0.6 1 0.4 1 1 1 0.2 0.2 0 1 +sphere -5 0 0 2 + +mtlcolor 0.6 1 0.4 1 1 1 0.2 0.4 0 1 +sphere 0 0 0 2 + +mtlcolor 0.6 1 0.4 1 1 1 0.2 0.6 0 1 +sphere 5 0 0 2 + +mtlcolor 0.6 1 0.4 1 1 1 0.2 0.8 0 1 +sphere 10 0 0 2 diff --git a/assignment-1c/examples/ks-demo.txt b/assignment-1c/examples/ks-demo.txt new file mode 100644 index 000000000..df401ff --- /dev/null +++ b/assignment-1c/examples/ks-demo.txt @@ -0,0 +1,23 @@ +imsize 600 200 +eye 0 0 15 +viewdir 0 0 -1 +hfov 90 +updir 0 1 0 +bkgcolor 0.4 0.4 0.4 + +light 0 5 5 1 0.7 0.7 0.8 + +mtlcolor 0.6 1 0.4 1 1 1 0.2 0.2 0 15 +sphere -10 0 0 2 + +mtlcolor 0.6 1 0.4 1 1 1 0.2 0.2 0.2 15 +sphere -5 0 0 2 + +mtlcolor 0.6 1 0.4 1 1 1 0.2 0.2 0.4 15 +sphere 0 0 0 2 + +mtlcolor 0.6 1 0.4 1 1 1 0.2 0.2 0.6 15 +sphere 5 0 0 2 + +mtlcolor 0.6 1 0.4 1 1 1 0.2 0.2 0.8 15 +sphere 10 0 0 2 diff --git a/assignment-1c/examples/multiple-lights-demo.txt b/assignment-1c/examples/multiple-lights-demo.txt new file mode 100644 index 000000000..cf2e84c --- /dev/null +++ b/assignment-1c/examples/multiple-lights-demo.txt @@ -0,0 +1,16 @@ +imsize 640 480 +eye 0 0 -2 +viewdir 0 0 -1 +hfov 90 +updir 0 1 0 +bkgcolor 0.4 0.4 0.4 + +light -5 5 -7.5 1 1 1 1 +light 5 5 -7.5 1 1 1 1 + +mtlcolor 0.4 0.6 1 1 1 1 0.2 0.6 0 10 +sphere 0 -2 -10 2 + +mtlcolor 0.8 0.4 0.4 1 1 1 0.2 0.6 0 10 +sphere -2.5 2.5 -10 1 +sphere 2.5 2.5 -10 1 diff --git a/assignment-1c/examples/n-demo.txt b/assignment-1c/examples/n-demo.txt new file mode 100644 index 000000000..38acb11 --- /dev/null +++ b/assignment-1c/examples/n-demo.txt @@ -0,0 +1,23 @@ +imsize 600 200 +eye 0 0 15 +viewdir 0 0 -1 +hfov 90 +updir 0 1 0 +bkgcolor 0.4 0.4 0.4 + +light 0 5 5 1 0.7 0.7 0.8 + +mtlcolor 0.6 1 0.4 1 1 1 0.2 0.2 0.4 2 +sphere -10 0 0 2 + +mtlcolor 0.6 1 0.4 1 1 1 0.2 0.2 0.4 6 +sphere -5 0 0 2 + +mtlcolor 0.6 1 0.4 1 1 1 0.2 0.2 0.4 10 +sphere 0 0 0 2 + +mtlcolor 0.6 1 0.4 1 1 1 0.2 0.2 0.4 50 +sphere 5 0 0 2 + +mtlcolor 0.6 1 0.4 1 1 1 0.2 0.2 0.4 100 +sphere 10 0 0 2 diff --git a/assignment-1c/examples/objects.txt b/assignment-1c/examples/objects.txt new file mode 100644 index 000000000..10f9e3a --- /dev/null +++ b/assignment-1c/examples/objects.txt @@ -0,0 +1,37 @@ +imsize 1920 1080 +eye 0 0 15 +viewdir 0 0 -1 +hfov 60 +updir 0 1 0 +bkgcolor 0.4 0.4 0.4 + +depthcueing 0.4 0.4 0.4 1 0.1 60 0 + +light -10 10 -3 0 0.8 0.8 0.8 +light -10 10 -3 1 0.8 0.8 0.8 + +mtlcolor 0 0.5 0.5 1 1 1 0.2 0.7 0.2 10 +sphere -1 -2 -5 2 +sphere 10 -10 -40 1 + +mtlcolor 0.5 0.5 1 0.4 0.4 0.4 0.2 0.7 0.4 10 +sphere 1 2 -3 3 +sphere -6 3 -4 1 + +mtlcolor 1.0 0 0.5 0.6 0.4 0.2 0.2 0.6 0.8 10 +sphere 20 20 -50 6 + +mtlcolor 0.5 0 0.5 0.6 0.4 0.2 0.2 0.5 0 10 +sphere -6 -4 -8 7 + +mtlcolor 0.8 0 0.3 0.6 0.4 0.2 0.2 0.4 0.2 10 +cylinder -2 2 -3 0 0 5 1 4 + +mtlcolor 0.8 0.8 0.3 0.6 0.4 0.2 0.2 0.8 0.2 10 +sphere -40 35 -80 7 +sphere -25 20 -60 5 +sphere -12.5 15 -40 3 +sphere -7 14 -30 2 + +mtlcolor 0.5 1 0.5 0.2 0.4 0.8 0.2 0.4 0 10 +cylinder 5 1 -2 1 -2 1 1 2 diff --git a/assignment-1c/examples/sample-1.txt b/assignment-1c/examples/sample-1.txt new file mode 100644 index 000000000..5ed539a --- /dev/null +++ b/assignment-1c/examples/sample-1.txt @@ -0,0 +1,12 @@ +imsize 256 256 +eye 0 0 0 +viewdir 0 0 -1 +hfov 90 +updir 0 1 0 +bkgcolor 0.1 0.1 0.1 + +light -2 3 -3 0 1 0.5 0.6 +light 2 3 -3 1 0.5 0.5 1 + +mtlcolor 0 1 0 1 1 1 0.2 0.4 0 10 +sphere 0 0 -5 2 diff --git a/assignment-1c/examples/soft-shadow-demo.txt b/assignment-1c/examples/soft-shadow-demo.txt new file mode 100644 index 000000000..279e3f1 --- /dev/null +++ b/assignment-1c/examples/soft-shadow-demo.txt @@ -0,0 +1,45 @@ +imsize 640 480 +eye 0 0 15 +viewdir 0 0 -1 +hfov 60 +updir 0 1 0 +bkgcolor 0.5 0.5 0.5 + +depthcueing 0.5 0.5 0.5 1 0.4 60 0 + +light 10 10 -10 1 1 1 1 + +mtlcolor 0.5 1 0.5 1 1 1 0.2 1 0.1 5 +sphere 4.5 4.5 -20 4.5 +sphere -4.5 -4.5 -20 4.5 + +mtlcolor 1 0.5 0.5 1 1 1 0.2 0.8 0 5 +sphere -10 0 -30 4 +sphere -20 0 -30 4 +sphere -30 0 -30 4 +sphere -40 0 -30 4 +sphere 0 0 -30 4 +sphere 10 0 -30 4 +sphere 20 0 -30 4 +sphere 30 0 -30 4 +sphere 40 0 -30 4 + +sphere -10 -10 -30 4 +sphere -20 -10 -30 4 +sphere -30 -10 -30 4 +sphere -40 -10 -30 4 +sphere 0 -10 -30 4 +sphere 10 -10 -30 4 +sphere 20 -10 -30 4 +sphere 30 -10 -30 4 +sphere 40 -10 -30 4 + +sphere -10 10 -30 4 +sphere -20 10 -30 4 +sphere -30 10 -30 4 +sphere -40 10 -30 4 +sphere 0 10 -30 4 +sphere 10 10 -30 4 +sphere 20 10 -30 4 +sphere 30 10 -30 4 +sphere 40 10 -30 4 diff --git a/assignment-1c/src/image.rs b/assignment-1c/src/image.rs new file mode 100644 index 000000000..2f80491 --- /dev/null +++ b/assignment-1c/src/image.rs @@ -0,0 +1,41 @@ +use std::io::{Result, Write}; + +use nalgebra::Vector3; + +/// A pixel color represented by a red, green, and blue value in the range 0-1. +pub type Color = Vector3; + +/// A representation of an image +pub struct Image { + /// Width in pixels + pub width: usize, + + /// Height in pixels + pub height: usize, + + /// Pixel data in row-major form. + pub data: Vec, +} + +impl Image { + /// Write the image in PPM format to a file. + pub fn write(&self, mut w: impl Write) -> Result<()> { + // Header + let header = format!("P3 {} {} 255\n", self.width, self.height); + w.write_all(header.as_bytes())?; + + // Pixel data + assert_eq!(self.data.len(), self.width * self.height); + + for pixel in self.data.iter() { + let pixel = pixel * 256.0; + let red = pixel.x as u8; + let green = pixel.y as u8; + let blue = pixel.z as u8; + let pixel = format!("{red} {green} {blue}\n"); + w.write_all(pixel.as_bytes())?; + } + + Ok(()) + } +} diff --git a/assignment-1c/src/lib.rs b/assignment-1c/src/lib.rs new file mode 100644 index 000000000..aa6a64d --- /dev/null +++ b/assignment-1c/src/lib.rs @@ -0,0 +1,13 @@ +#![doc = include_str!("../README.md")] + +#[macro_use] +extern crate anyhow; +#[macro_use] +extern crate derivative; +#[macro_use] +extern crate tracing; + +pub mod image; +pub mod ray; +pub mod scene; +pub mod utils; diff --git a/assignment-1c/src/main.rs b/assignment-1c/src/main.rs new file mode 100644 index 000000000..8334beb --- /dev/null +++ b/assignment-1c/src/main.rs @@ -0,0 +1,138 @@ +#[macro_use] +extern crate tracing; + +use std::fs::File; +use std::path::PathBuf; + +use anyhow::Result; +use assignment_1b::image::Image; +use assignment_1b::ray::Ray; +use assignment_1b::scene::Scene; + +use clap::Parser; + +use rayon::prelude::{IntoParallelIterator, ParallelIterator}; + +/// Simple raytracer with Blinn-Phong illumination and shadowing. +#[derive(Parser)] +#[clap(author, version, about, long_about = None)] +struct Opt { + /// Path to the input file to use. + #[clap()] + input_path: PathBuf, + + /// Path to the output (defaults to the same file name as the input except + /// with an extension of .ppm) + #[clap(short = 'o', long = "output")] + output_path: Option, + + /// Force parallel projection to be used + #[clap(long = "parallel")] + force_parallel: bool, + + /// Override distance from eye + #[clap(long = "distance", default_value = "1.0")] + distance: f64, +} + +fn main() -> Result<()> { + let opt = Opt::parse(); + + // Set up logging + tracing_subscriber::fmt() + .with_target(false) + .with_timer(tracing_subscriber::fmt::time::uptime()) + .with_level(true) + .init(); + + // Rename the output file if it's not provided + let out_file = opt + .output_path + .unwrap_or_else(|| opt.input_path.with_extension("ppm")); + + let mut scene = Scene::from_input_file(&opt.input_path)?; + let distance = opt.distance; + + // Force-override parallel projection + if opt.force_parallel { + scene.parallel_projection = true; + } + + // Translate image pixels to real-world 3d coords + let translate_pixel = scene.pixel_translation_function(distance); + + // Generate a parallel iterator for pixels + // The iterator preserves order and uses row-major order + let pixels_iter = (0..scene.image_height) + .into_par_iter() + .flat_map(|y| (0..scene.image_width).into_par_iter().map(move |x| (x, y))); + + // Loop through every single pixel of the output file + let pixels = pixels_iter + .map(|(px, py)| { + let pixel_in_space = translate_pixel(px, py); + + let ray_start = if scene.parallel_projection { + // For a parallel projection, we'll just take the view direction and + // subtract it from the target point. This means every single + // ray will be viewed from a point at infinity, rather than a single eye + // position. + let n = scene.view_dir.normalize(); + let view_dir = n * distance; + pixel_in_space - view_dir + } else { + scene.eye_pos + }; + + let ray = Ray::from_endpoints(ray_start, pixel_in_space); + + let intersections = scene + .objects + .iter() + .enumerate() + .filter_map(|(i, object)| { + match object.kind.intersects_ray_at(&ray) { + Ok(Some(t)) => { + // Return both the t and the sphere, because we want to sort on + // the t but later retrieve attributes from the sphere + Some(Ok((i, t, object))) + } + Ok(None) => None, + Err(err) => { + error!("Error: {err}"); + Some(Err(err)) + } + } + }) + .collect::>>()?; + + // Sort the list of intersection times by the lowest one. + let earliest_intersection = + intersections.into_iter().min_by_key(|(_, t, _)| t.time); + + Ok(match earliest_intersection { + // Take the object's material color + Some((obj_idx, intersection_context, object)) => scene + .compute_pixel_color(obj_idx, object.material, intersection_context), + + // There was no intersection, so this should default to the scene's + // background color + None => scene.bkg_color, + }) + }) + .collect::>>()?; + + // Construct and emit image + let image = Image { + width: scene.image_width, + height: scene.image_height, + data: pixels, + }; + + { + let file = File::create(out_file)?; + image.write(file)?; + } + + Ok(()) +} diff --git a/assignment-1c/src/ray.rs b/assignment-1c/src/ray.rs new file mode 100644 index 000000000..a722ddf --- /dev/null +++ b/assignment-1c/src/ray.rs @@ -0,0 +1,27 @@ +use nalgebra::Vector3; + +/// A normalized parametric Ray of the form (origin + direction * time) +/// +/// That means at any time t: f64, the point represented by origin + direction * +/// time occurs on the ray. +#[derive(Debug)] +pub struct Ray { + pub origin: Vector3, + pub direction: Vector3, +} + +impl Ray { + /// Construct a ray from endpoints + pub fn from_endpoints(start: Vector3, end: Vector3) -> Self { + let delta = (end - start).normalize(); + Ray { + origin: start, + direction: delta, + } + } + + /// Evaluate the ray at a certain point in time, yielding a point + pub fn eval(&self, time: f64) -> Vector3 { + self.origin + self.direction * time + } +} diff --git a/assignment-1c/src/scene/cylinder.rs b/assignment-1c/src/scene/cylinder.rs new file mode 100644 index 000000000..18a52fb --- /dev/null +++ b/assignment-1c/src/scene/cylinder.rs @@ -0,0 +1,204 @@ +use anyhow::Result; +use nalgebra::Vector3; +use ordered_float::NotNan; + +use crate::ray::Ray; +use crate::utils::compute_rotation_matrix; + +use super::{illumination::IntersectionContext}; + +#[derive(Debug)] +pub struct Cylinder { + pub center: Vector3, + pub direction: Vector3, + pub radius: f64, + pub length: f64, +} + +impl Cylinder { + /// Given a cylinder, returns the first time at which this ray intersects the + /// cylinder. + /// + /// If there is no intersection point, returns None. + pub fn intersects_ray_at( + &self, + ray: &Ray, + ) -> Result> { + // Determine rotation matrix for turning the cylinder upright along the + // Z-axis + let target_direction = Vector3::new(0.0, 0.0, 1.0); + let rotation_matrix = + compute_rotation_matrix(self.direction, target_direction)?; + let inverse_rotation_matrix = + rotation_matrix.try_inverse().ok_or_else(|| { + anyhow!("Rotation matrix for some reason does not have an inverse?") + })?; + + // Transform all parameters according to this rotation matrix + let rotated_cylinder_center = rotation_matrix * self.center; + let rotated_ray_origin = rotation_matrix * ray.origin; + let rotated_ray_direction = rotation_matrix * ray.direction; + + // Now that we know the cylinder is upright, we can start checking against + // the formula: + // + // (ox + t*rx - cx)^2 + (oy + t*ry - cy)^2 = r^2 + // + // where o{xy} is the ray origin, r{xy} is the ray direction, and c{xy} is + // the cylinder center. The z will be taken care of after the fact. To + // solve, we must put it into the form At^2 + Bt + c = 0. The variables + // are: + // + // A: rx^2 + ry^2 + // B: 2(rx(ox - cx) + ry(oy - cy)) + // C: (cx - ox)^2 + (cy - oy)^2 - r^2 + let (a, b, c) = { + let o = rotated_ray_origin; + let r = rotated_ray_direction; + let c = rotated_cylinder_center; + + ( + r.x.powi(2) + r.y.powi(2), + 2.0 * (r.x * (o.x - c.x) + r.y * (o.y - c.y)), + (c.x - o.x).powi(2) + (c.y - o.y).powi(2) - self.radius.powi(2), + ) + }; + + let discriminant = b * b - 4.0 * a * c; + + let possible_side_solutions = match discriminant { + // Discriminant < 0, means the equation has no solutions. + d if d < 0.0 => vec![], + + // Discriminant == 0 + d if d == 0.0 => vec![-b / 2.0 * a], + + // Discriminant > 0, 2 solutions available. + d if d > 0.0 => { + vec![ + (-b + discriminant.sqrt()) / (2.0 * a), + (-b - discriminant.sqrt()) / (2.0 * a), + ] + } + + // Probably hit some NaN or Infinity value due to faulty inputs... + _ => bail!("Invalid determinant value: {discriminant}"), + }; + + // Filter out solutions that don't have a valid Z position. + let side_solutions = possible_side_solutions.into_iter().filter_map(|t| { + let ray_point = ray.eval(t); + let rotated_ray_point = rotation_matrix * ray_point; + let z = rotated_ray_point.z - rotated_cylinder_center.z; + + // Check to see if z is between -len/2 and len/2 + if z.abs() > self.length / 2.0 { + return None; + } + + let time = NotNan::new(t).ok()?; + + // The point on the center of the cylinder that corresponds to the z-axis + // point of the intersection + let center_at_z = { + let mut center_point = rotation_matrix * ray_point; + center_point.x = rotated_cylinder_center.x; + center_point.y = rotated_cylinder_center.y; + + inverse_rotation_matrix * center_point + }; + let normal = (ray_point - center_at_z).normalize(); + + Some(IntersectionContext { + time, + point: ray_point, + normal, + }) + }); + + // We also need to add solutions for the two ends of the cylinder, which + // uses a similar method except backwards: check intersection points + // with the correct z-plane and then see if the points are within the + // circle. + // + // Luckily, this means we only need to care about one dimension at first, + // and don't need to perform the quadratic equation method above. + // + // oz + t * rz = cz +- (len / 2) + // t = (-oz + cz +- (len / 2)) / rz + let possible_z_intersections = { + let o = rotated_ray_origin; + let r = rotated_ray_direction; + let c = rotated_cylinder_center; + + if r.z == 0.0 { + Vec::new() // No solutions here + } else { + vec![ + (-o.z + c.z + self.length / 2.0) / r.z, + (-o.z + c.z - self.length / 2.0) / r.z, + ] + } + }; + + let end_solutions = possible_z_intersections.into_iter().filter_map(|t| { + let ray_point = ray.eval(t); + let rotated_point = rotation_matrix * ray_point; + + // Filter out all the solutions where the intersection point does not lie + // in the circle + if rotated_point.x.powi(2) + rotated_point.y.powi(2) > self.radius.powi(2) + { + return None; + } + + let normal_rotated = + Vector3::new(0.0, 0.0, rotated_point.z - rotated_cylinder_center.z) + .normalize(); + let normal = inverse_rotation_matrix * normal_rotated; + + let time = NotNan::new(t).ok()?; + Some(IntersectionContext { + time, + point: ray_point, + normal, + }) + }); + + let solutions = side_solutions + .into_iter() + .chain(end_solutions.into_iter()) + // Remove any t < 0, since that means it's behind the viewer and we + // can't see it. + .filter(|ctx| *ctx.time >= 0.0); + + // Return the minimum solution + Ok(solutions.min_by_key(|ctx| ctx.time)) + } +} + +#[cfg(test)] +mod tests { + use nalgebra::Vector3; + + use crate::{ray::Ray}; + + use super::Cylinder; + + #[test] + fn test_cylinder() { + let cylinder = Cylinder { + center: Vector3::new(0.0, 0.0, 0.0), + direction: Vector3::new(0.0, 1.0, 0.0), + radius: 3.0, + length: 4.0, + }; + + let eye = Vector3::new(0.0, 3.0, 3.0); + let end = Vector3::new(0.0, 2.0, 2.0); + let ray = Ray::from_endpoints(eye, end); + + let res = cylinder.intersects_ray_at(&ray); + panic!("Result: {res:?}"); + } +} diff --git a/assignment-1c/src/scene/data.rs b/assignment-1c/src/scene/data.rs new file mode 100644 index 000000000..45e5fdd --- /dev/null +++ b/assignment-1c/src/scene/data.rs @@ -0,0 +1,225 @@ +use std::fmt::Debug; + +use anyhow::Result; +use nalgebra::Vector3; + +use crate::image::Color; +use crate::ray::Ray; +use crate::utils::cross; + +use super::cylinder::Cylinder; +use super::illumination::IntersectionContext; +use super::sphere::Sphere; +use super::Scene; + +#[derive(Debug)] +pub enum ObjectKind { + Sphere(Sphere), + Cylinder(Cylinder), +} + +impl ObjectKind { + /// Determine where the ray intersects this object, returning the earliest + /// time this happens. Returns None if no intersection occurs. + /// + /// Also known as Trace_Ray in the slides, except not the part where it calls + /// Shade_Ray. + pub fn intersects_ray_at( + &self, + ray: &Ray, + ) -> Result> { + match self { + ObjectKind::Sphere(sphere) => sphere.intersects_ray_at(ray), + ObjectKind::Cylinder(cylinder) => cylinder.intersects_ray_at(ray), + } + } +} + +/// An object in the scene +#[derive(Debug)] +pub struct Object { + pub kind: ObjectKind, + + /// Index into the scene's material color list + pub material: usize, +} + +#[derive(Debug)] +pub struct Rect { + pub upper_left: Vector3, + pub upper_right: Vector3, + pub lower_left: Vector3, + pub lower_right: Vector3, +} + +#[derive(Debug)] +pub struct Material { + pub diffuse_color: Vector3, + pub specular_color: Vector3, + + pub k_a: f64, + pub k_d: f64, + pub k_s: f64, + pub exponent: f64, +} + +#[derive(Debug)] +pub enum LightKind { + /// A point light source exists at a point and emits light in all directions + Point { + location: Vector3, + + /// Whether light attenuation is enabled for this light + attenuation: Option, + }, + + /// A directional light source exists at an infinitely far location but emits + /// light in a specific direction + Directional { direction: Vector3 }, +} + +#[derive(Debug)] +pub struct Light { + /// The kind of light source, as well as its associated information + pub kind: LightKind, + + /// The color, or intensity, of the light source + pub color: Vector3, +} + +impl Light { + /// Get the unit directional vector pointing from the given point to this + /// light source + pub fn direction_from(&self, point: Vector3) -> Vector3 { + match self.kind { + LightKind::Point { location, .. } => location - point, + LightKind::Directional { direction } => -direction, + } + .normalize() + } +} + +#[derive(Debug)] +pub struct DepthCueing { + /// The color to tint (should be the same as the background color, to avoid + /// bizarre visual effects) + pub color: Color, + + /// Proportion of the color influenced by the depth tint when the distance is + /// maxed (caps at 1.0) + pub a_max: f64, + + /// Proportion of the color influenced by the depth tint when the distance is + /// at the minimum (caps at 1.0) + pub a_min: f64, + + /// The max distance that should be affected by the depth tint + pub dist_max: f64, + + /// The min distance that should be affected by the depth tint + pub dist_min: f64, +} + +/// A default implementation here needs to simulate what would happen if there +/// was no depth cueing. In this case, if we have both a_max and a_min be 1.0, +/// then the original color will always apply and there will be no need for +/// depth color +impl Default for DepthCueing { + fn default() -> Self { + Self { + color: Default::default(), + a_max: 1.0, + a_min: 1.0, + dist_max: 0.0, + dist_min: 0.0, + } + } +} + +/// Light attenuation dropoff coefficients +#[derive(Debug)] +pub struct Attenuation { + pub c1: f64, + pub c2: f64, + pub c3: f64, +} + +/// A default implementation here needs to simulate what would happen if there +/// was no light attenuation specified. In this case, c1 would just be a +/// constant of 1 and all the coefficients for anything involving distance would +/// be zeroed out +impl Default for Attenuation { + fn default() -> Self { + Self { + c1: 1.0, + c2: 0.0, + c3: 0.0, + } + } +} + +impl Scene { + /// Determine the boundaries of the viewing window in world coordinates + pub fn compute_viewing_window(&self, distance: f64) -> Rect { + // Compute viewing directions + let u = cross(self.view_dir, self.up_dir).normalize(); + let v = cross(u, self.view_dir).normalize(); + + // Compute dimensions of viewing window based on field of view + let viewing_width = { + // Divide the angle in 2 since we are trying to use trig rules so we must + // get it from a right triangle + let half_hfov = self.hfov.to_radians() / 2.0; + + // tan(hfov / 2) = w / 2d + let w_over_2d = half_hfov.tan(); + + // To find the viewing width we must multiply by 2d now + w_over_2d * 2.0 * distance + }; + + let aspect_ratio = self.image_width as f64 / self.image_height as f64; + let viewing_height = viewing_width / aspect_ratio; + + // Compute viewing window corners + let n = self.view_dir.normalize(); + + #[rustfmt::skip] // Don't format, or else this line wraps over + let view_window = Rect { + upper_left: self.eye_pos + n * distance - u * (viewing_width / 2.0) + v * (viewing_height / 2.0), + upper_right: self.eye_pos + n * distance + u * (viewing_width / 2.0) + v * (viewing_height / 2.0), + lower_left: self.eye_pos + n * distance - u * (viewing_width / 2.0) - v * (viewing_height / 2.0), + lower_right: self.eye_pos + n * distance + u * (viewing_width / 2.0) - v * (viewing_height / 2.0), + }; + + view_window + } + + /// Create a pixel translation function based on the viewing window of the + /// current scene + pub fn pixel_translation_function( + &self, + distance: f64, + ) -> impl Fn(usize, usize) -> Vector3 { + let view_window = self.compute_viewing_window(distance); + + let dx = view_window.upper_right - view_window.upper_left; + let pixel_base_x = dx / self.image_width as f64; + + let dy = view_window.lower_left - view_window.upper_left; + let pixel_base_y = dy / self.image_height as f64; + + // The final function to be returned + move |px: usize, py: usize| { + let x_component = pixel_base_x * px as f64; + let y_component = pixel_base_y * py as f64; + + // Without adding this, we would be getting the top-left of the pixel's + // rectangle. We want the center, so add half of the pixel size as + // well. + let center_offset = (pixel_base_x + pixel_base_y) / 2.0; + + view_window.upper_left + x_component + y_component + center_offset + } + } +} diff --git a/assignment-1c/src/scene/illumination.rs b/assignment-1c/src/scene/illumination.rs new file mode 100644 index 000000000..446bf85 --- /dev/null +++ b/assignment-1c/src/scene/illumination.rs @@ -0,0 +1,276 @@ +use std::iter; + +use nalgebra::Vector3; +use ordered_float::NotNan; +use rand::Rng; +use rayon::prelude::{ + IndexedParallelIterator, IntoParallelIterator, IntoParallelRefIterator, + ParallelIterator, +}; + +use crate::{image::Color, ray::Ray, utils::dot}; + +use super::{ + data::{DepthCueing, Light, LightKind, Object}, + Scene, +}; + +impl Scene { + /// Determine the color that should be used to fill this pixel. + /// + /// - material_idx is the index into the materials list. + /// - intersection_context contains information on vectors where the + /// intersection occurred + /// + /// Also known as Shade_Ray in the slides. + pub fn compute_pixel_color( + &self, + obj_idx: usize, + material_idx: usize, + intersection_context: IntersectionContext, + ) -> Color { + // TODO: Does it make sense to make this function fallible from an API + // design standpoint? + let material = match self.materials.get(material_idx) { + Some(v) => v, + None => return self.bkg_color, + }; + + let ambient_component = material.k_a * material.diffuse_color; + + // Diffuse and specular lighting for each separate light + let diffuse_and_specular: Vector3 = self + .lights + .par_iter() + .map(|light| { + // The vector pointing in the direction of the light + let light_direction = light.direction_from(intersection_context.point); + + let normal = intersection_context.normal.normalize(); + let viewer_direction = self.eye_pos - intersection_context.point; + let halfway_direction = + ((light_direction + viewer_direction) / 2.0).normalize(); + + let diffuse_component = material.k_d + * material.diffuse_color + * dot(normal, light_direction).max(0.0); + + let specular_component = material.k_s + * material.specular_color + * dot(normal, halfway_direction) + .max(0.0) + .powf(material.exponent); + + // Shadow coefficient between 0 and 1 to control how bright this pixel + // should be from being in the shadow of another object (could be + // between 0 and 1 when applying soft shadows) + let shadow_coefficient = self.compute_shadow_coefficient( + obj_idx, + intersection_context.point, + light, + ); + + let attenuation_coefficient = match &light.kind { + LightKind::Point { + location, + attenuation: Some(att), + } => { + let dist = (location - intersection_context.point).norm(); + let denom = att.c1 + att.c2 * dist + att.c3 * dist.powi(2); + if denom == 0.0 { + warn!("Light attenuation coefficients produced a denominator of 0. Check your inputs..."); + 1.0 // Some kind of graceful fallback here + } else { + 1.0 / denom + } + } + _ => 1.0, + }; + + let diffuse_and_specular = diffuse_component + specular_component; + + attenuation_coefficient + * shadow_coefficient + * light.color.component_mul(&diffuse_and_specular) + }) + .sum(); + + let color = ambient_component + diffuse_and_specular; + + // Apply depth cueing to the result + let a_dc = { + // Distance from the viewer + let d_obj = (intersection_context.point - self.eye_pos).norm(); + let DepthCueing { + dist_max, + dist_min, + a_max, + a_min, + .. + } = self.depth_cueing; + + if d_obj < dist_min { + a_max + } else if d_obj < dist_max { + a_min + (a_max - a_min) * (dist_max - d_obj) / (dist_max - dist_min) + } else { + a_min + } + }; + + let color = a_dc * color + (1.0 - a_dc) * self.depth_cueing.color; + + // Need to clamp the result so none of the components goes over 1 + let clamped_result = color.map(|v| v.min(1.0)); + + clamped_result + } + + /// Perform another ray casting to see if there are any objects obstructing + /// the light source to this particular point + pub fn compute_shadow_coefficient( + &self, + obj_idx: usize, + point: Vector3, + light: &Light, + ) -> f64 { + let light_direction = light.direction_from(point); + let ray = Ray { + origin: point, + direction: light_direction.normalize(), + }; + + // Small helper for iterating over all of the objects in the scene except + // for the current one + let other_objects = self + .objects + .par_iter() + .enumerate() + .filter(|(i, _)| *i != obj_idx); + + // Get the list of intersections with all the other objects in the scene + // This list will be a set of opacities + let intersections = other_objects + .filter_map(|(_, object)| { + let intersection_context = match object.kind.intersects_ray_at(&ray) { + Ok(v) => v, + Err(err) => { + error!("Error while performing shadow casting: {err}"); + None + } + }?; + let intersection_time = *intersection_context.time; + + match light.kind { + // In the case of point lights, we must check to see if both t > 0 and + // t is less than the time it took to even get to the light. + LightKind::Point { location, .. } => { + let light_time = (location - ray.origin).norm(); + + if intersection_time <= 0.0 || intersection_time >= light_time { + None + } else { + let soft_shadow_coefficient = + self.compute_soft_shadow_coefficient(location, point, object); + Some(soft_shadow_coefficient) + } + } + + // In the case of directional lights, only t > 0 needs to be checked, + // otherwise + LightKind::Directional { .. } => { + if intersection_time <= 0.0 { + None + } else { + Some(0.0) // complete obstruction + } + } + } + }) + .collect::>(); + + let average = + intersections.iter().cloned().sum::() / intersections.len() as f64; + + match intersections.is_empty() { + true => 1.0, + false => average, + } + } + + fn compute_soft_shadow_coefficient( + &self, + light_location: Vector3, + original_intersection_point: Vector3, + object: &Object, + ) -> f64 { + // Soft shadows: jitter some rays here to somewhere close to the + // actual location as well, and measure the proportion + // of them that intersect any objects + const JITTER_RADIUS: f64 = 1.0; + const JITTER_RAYS: usize = 75; + + let mut rng = rand::thread_rng(); + let locations = iter::repeat_with(|| { + let x = rng.gen_range(0.0..JITTER_RADIUS); + let y = rng.gen_range(0.0..JITTER_RADIUS); + let z = rng.gen_range(0.0..JITTER_RADIUS); + let delta = Vector3::new(x, y, z); + light_location + delta + }) + .take(JITTER_RAYS) + .collect::>(); + let num_obstructed_rays = locations + .into_par_iter() + .filter(|location| { + let direction = (location - original_intersection_point).normalize(); + let ray = Ray { + origin: original_intersection_point, + direction, + }; + + let intersection_context = match object.kind.intersects_ray_at(&ray) { + Ok(Some(v)) => v, + Ok(None) => return false, + Err(err) => { + error!("Error while performing shadow casting: {err}"); + return false; + } + }; + + let light_time = (location - ray.origin).norm(); + let intersection_time = *intersection_context.time; + + 0.0 < intersection_time && intersection_time < light_time + }) + .count(); + + (JITTER_RAYS - num_obstructed_rays) as f64 / JITTER_RAYS as f64 + } +} + +/// Information about an intersection +#[derive(Derivative)] +#[derivative(Debug, PartialEq, PartialOrd, Ord)] +pub struct IntersectionContext { + /// The time of the intersection in the parametric ray + /// + /// Unfortunately, IEEE floats in Rust don't have total ordering, because + /// NaNs violate ordering properties. The way to remedy this is to ensure we + /// don't have NaNs by wrapping it into this type, which then implements + /// total ordering. + pub time: NotNan, + + /// The intersection point. + #[derivative(PartialEq = "ignore", Ord = "ignore")] + pub point: Vector3, + + /// The normal vector protruding from the surface of the object at the + /// intersection point + #[derivative(PartialEq = "ignore", Ord = "ignore")] + pub normal: Vector3, +} + +impl Eq for IntersectionContext {} + +impl IntersectionContext {} diff --git a/assignment-1c/src/scene/input_file.rs b/assignment-1c/src/scene/input_file.rs new file mode 100644 index 000000000..c6882d2 --- /dev/null +++ b/assignment-1c/src/scene/input_file.rs @@ -0,0 +1,191 @@ +use std::{fs::File, io::Read, path::Path}; + +use anyhow::Result; +use nalgebra::Vector3; + +use crate::scene::{ + cylinder::Cylinder, + data::{Attenuation, Light, LightKind, Material, Object}, + sphere::Sphere, + Scene, +}; + +use super::data::{DepthCueing, ObjectKind}; + +impl Scene { + /// Parse the input file into a scene + pub fn from_input_file(path: impl AsRef) -> Result { + // Scope the read so the file is dropped and closed immediately after the + // contents have been read to memory + let contents = { + let mut contents = String::new(); + let mut file = File::open(path.as_ref())?; + file.read_to_string(&mut contents)?; + contents + }; + + let mut scene = Scene::default(); + let mut material_color = None; + + for line in contents.lines() { + let mut parts = line.split_whitespace(); + let keyword = match parts.next() { + Some(v) => v, + None => continue, + }; + + if keyword == "imsize" { + let parts = parts + .map(|s| s.parse::().map_err(|e| e.into())) + .collect::>>()?; + if let [width, height] = parts[..] { + scene.image_width = width; + scene.image_height = height; + } + } else if keyword == "projection" { + if let Some("parallel") = parts.next() { + scene.parallel_projection = true; + } + } + // Do float parsing instead + else { + let parts = parts + .map(|s| s.parse::().map_err(|e| e.into())) + .collect::>>()?; + + let read_vec3 = |start: usize| { + ensure!(parts.len() >= start + 3, "Vec3 requires 3 components."); + + Ok(Vector3::new( + parts[start], + parts[start + 1], + parts[start + 2], + )) + }; + + match keyword { + "eye" => scene.eye_pos = read_vec3(0)?, + "viewdir" => scene.view_dir = read_vec3(0)?, + "updir" => scene.up_dir = read_vec3(0)?, + + "hfov" => scene.hfov = parts[0], + "bkgcolor" => scene.bkg_color = read_vec3(0)?, + + // light x y z w r g b + "light" => { + ensure!(parts.len() == 7, "Light requires 7 params"); + + let kind = match parts[3] as usize { + 0 => LightKind::Directional { + direction: read_vec3(0)?, + }, + 1 => LightKind::Point { + location: read_vec3(0)?, + attenuation: None, + }, + _ => bail!("Invalid w; must be either 0 or 1"), + }; + let light = Light { + kind, + color: read_vec3(4)?, + }; + scene.lights.push(light); + } + + // attlight x y z w r g b c1 c2 c3 + "attlight" => { + ensure!(parts.len() == 10, "Attenuated light requires 10 params"); + + let kind = match parts[3] as usize { + // TODO: Is this even defined? Pending TA answer + 0 => LightKind::Directional { + direction: read_vec3(0)?, + }, + 1 => LightKind::Point { + location: read_vec3(0)?, + attenuation: Some(Attenuation { + c1: parts[7], + c2: parts[8], + c3: parts[9], + }), + }, + _ => bail!("Invalid w; must be either 0 or 1"), + }; + let light = Light { + kind, + color: read_vec3(4)?, + }; + scene.lights.push(light); + } + + // depthcueing dcr dcg dcb amax amin distmax distmin + "depthcueing" => { + ensure!(parts.len() == 7, "Depth cueing requires 7 params"); + + let color = read_vec3(0)?; + scene.depth_cueing = DepthCueing { + color, + a_max: parts[3], + a_min: parts[4], + dist_max: parts[5], + dist_min: parts[6], + }; + } + + // mtlcolor Odr Odg Odb Osr Osg Osb ka kd ks n + "mtlcolor" => { + ensure!(parts.len() == 10, "Material color requires 10 params"); + + let diffuse_color = read_vec3(0)?; + let specular_color = read_vec3(3)?; + + let material = Material { + diffuse_color, + specular_color, + k_a: parts[6], + k_d: parts[7], + k_s: parts[8], + exponent: parts[9], + }; + + let idx = scene.materials.len(); + material_color = Some(idx); + scene.materials.push(material); + } + + "sphere" => scene.objects.push(Object { + kind: ObjectKind::Sphere(Sphere { + center: read_vec3(0)?, + radius: parts[3], + }), + material: match material_color { + Some(v) => v, + None => { + bail!("Each sphere must be preceded by a `mtlcolor` line") + } + }, + }), + + "cylinder" => scene.objects.push(Object { + kind: ObjectKind::Cylinder(Cylinder { + center: read_vec3(0)?, + direction: read_vec3(3)?, + radius: parts[6], + length: parts[7], + }), + material: match material_color { + Some(v) => v, + None => { + bail!("Each sphere must be preceded by a `mtlcolor` line") + } + }, + }), + + _ => bail!("Unknown keyword {keyword}"), + } + } + } + + Ok(scene) + } +} diff --git a/assignment-1c/src/scene/mod.rs b/assignment-1c/src/scene/mod.rs new file mode 100644 index 000000000..cc683ab --- /dev/null +++ b/assignment-1c/src/scene/mod.rs @@ -0,0 +1,34 @@ +pub mod cylinder; +pub mod data; +pub mod illumination; +pub mod input_file; +pub mod sphere; + +use nalgebra::Vector3; + +use crate::image::Color; + +use self::data::{DepthCueing, Light, Material, Object, Attenuation}; + +#[derive(Debug, Default)] +pub struct Scene { + pub eye_pos: Vector3, + pub view_dir: Vector3, + pub up_dir: Vector3, + + /// Horizontal field of view (in degrees) + pub hfov: f64, + pub parallel_projection: bool, + + pub image_width: usize, + pub image_height: usize, + + /// Background color + pub bkg_color: Color, + pub depth_cueing: DepthCueing, + pub attenuation: Attenuation, + + pub materials: Vec, + pub lights: Vec, + pub objects: Vec, +} diff --git a/assignment-1c/src/scene/sphere.rs b/assignment-1c/src/scene/sphere.rs new file mode 100644 index 000000000..a9bcd1d --- /dev/null +++ b/assignment-1c/src/scene/sphere.rs @@ -0,0 +1,74 @@ +use anyhow::Result; +use nalgebra::Vector3; +use ordered_float::NotNan; + +use crate::{ray::Ray, utils::min_f64}; + +use super::illumination::IntersectionContext; + +#[derive(Debug)] +pub struct Sphere { + pub center: Vector3, + pub radius: f64, +} + +impl Sphere { + /// Given a sphere, returns the first time at which this ray intersects the + /// sphere. + /// + /// If there is no intersection point, returns None. + pub fn intersects_ray_at( + &self, + ray: &Ray, + ) -> Result> { + let a = ray.direction.norm(); + let b = 2.0 + * (ray.direction.x * (ray.origin.x - self.center.x) + + ray.direction.y * (ray.origin.y - self.center.y) + + ray.direction.z * (ray.origin.z - self.center.z)); + let c = (ray.origin.x - self.center.x).powi(2) + + (ray.origin.y - self.center.y).powi(2) + + (ray.origin.z - self.center.z).powi(2) + - self.radius.powi(2); + let discriminant = b * b - 4.0 * a * c; + + let time = match discriminant { + // Discriminant < 0, means the equation has no solutions. + d if d < 0.0 => None, + + // Discriminant == 0 + d if d == 0.0 => Some(-b / (2.0 * a)), + + d if d > 0.0 => { + let solution_1 = (-b + discriminant.sqrt()) / (2.0 * a); + let solution_2 = (-b - discriminant.sqrt()) / (2.0 * a); + + let solutions = [solution_1, solution_2] + .into_iter() + // Remove any t < 0, since that means it's behind the viewer and we + // can't see it. + .filter(|t| *t >= 0.0); + + // Return the minimum solution + min_f64(solutions) + } + + // Probably hit some NaN or Infinity value due to faulty inputs... + _ => unreachable!("Invalid determinant value: {discriminant}"), + }; + + let time = match time.and_then(|t| NotNan::new(t).ok()) { + Some(v) => v, + None => return Ok(None), + }; + + let point = ray.eval(*time); + let normal = (point - self.center).normalize(); + + Ok(Some(IntersectionContext { + time, + point, + normal, + })) + } +} diff --git a/assignment-1c/src/utils.rs b/assignment-1c/src/utils.rs new file mode 100644 index 000000000..95b1e0b --- /dev/null +++ b/assignment-1c/src/utils.rs @@ -0,0 +1,93 @@ +use anyhow::Result; +use nalgebra::{Matrix3, Vector3}; +use ordered_float::NotNan; + +/// Finds the minimum of an iterator of f64s, ignoring any NaN values +#[inline] +pub fn min_f64(i: I) -> Option +where + I: Iterator, +{ + i.filter_map(|i| NotNan::new(i).ok()) + .min() + .map(|i| i.into_inner()) +} + +/// Finds the minimum of an iterator of f64s using the given predicate, ignoring +/// any NaN values +#[inline] +pub fn min_f64_by_key(i: I, f: F) -> Option +where + I: Iterator, + F: FnMut(&NotNan), +{ + i.filter_map(|i| NotNan::new(i).ok()) + .min_by_key(f) + .map(|i| i.into_inner()) +} + +/// Dot-product between two 3D vectors. +#[inline] +pub fn dot(a: Vector3, b: Vector3) -> f64 { + a.x * b.x + a.y * b.y + a.z * b.z +} + +/// Cross-product between two 3D vectors. +#[inline] +pub fn cross(a: Vector3, b: Vector3) -> Vector3 { + let x = a.y * b.z - a.z * b.y; + let y = a.z * b.x - a.x * b.z; + let z = a.x * b.y - a.y * b.x; + Vector3::new(x, y, z) +} + +/// Calculate the rotation matrix between the 2 given vectors +/// +/// Based on the method given [here][1]. +/// +/// [1]: https://math.stackexchange.com/a/897677 +pub fn compute_rotation_matrix( + a: Vector3, + b: Vector3, +) -> Result> { + // Special case: if a and b are in the same direction, just return the + // identity matrix. + if a.normalize() == b.normalize() { + return Ok(Matrix3::identity()); + } + + let cos_t = dot(a, b); + let sin_t = cross(a, b).norm(); + + let g = Matrix3::new(cos_t, -sin_t, 0.0, sin_t, cos_t, 0.0, 0.0, 0.0, 1.0); + + // New basis vectors + let u = a; + let v = (b - cos_t * a).normalize(); + let w = cross(b, a); + + // Not sure if this is required to be invertible? + let f_inverse = Matrix3::from_columns(&[u, v, w]); + let f = match f_inverse.try_inverse() { + Some(v) => v, + None => { + // So I ran into this case trying to compute the rotation matrix where one + // of the vector endpoints was (0, 0, 0). I'm pretty sure this case makes + // no sense in reality, which means if I ever encounter this case, I + // probably made a mistake somewhere before. So going to just error + // out here and screw recovering. + // + // println!("Failed to compute inverse matrix."); + // println!("- Initial: a = {a}, b = {b}"); + // println!("- cos(t) = {cos_t}, sin(t) = {sin_t}"); + // println!("- Basis: u = {u}, v = {v}, w = {w}"); + bail!("Failed to compute inverse matrix of {f_inverse}\na = {a}\nb = {b}") + } + }; + + // if (f_inverse * g * f).norm() != 1.0 { + // bail!("WTF {}", (f_inverse * g * f).norm()); + // } + + Ok(f_inverse * g * f) +} diff --git a/assignment-1c/writeup.md b/assignment-1c/writeup.md new file mode 100644 index 000000000..17d375a --- /dev/null +++ b/assignment-1c/writeup.md @@ -0,0 +1,173 @@ +--- +geometry: margin=2cm +output: pdf_document +--- + +# Raytracer part B + +This project implements a raytracer with Blinn-Phong illumination and shadows +implemented. The primary formula that is used by this implementation is: + +\begin{equation} +I_{\lambda} = +k_a O_{d\lambda} + +\sum_{i=1}^{n_\textrm{lights}} \left( +f_\textrm{att} \cdot +S_i \cdot +IL_{i\lambda} \left[ +k_d O_{d\lambda} \max ( 0, \vec{N} \cdot \vec{L_i} ) + +k_s O_{s\lambda} \max ( 0, \vec{N} \cdot \vec{H_i} )^n +\right] +\right) +\end{equation} + +Where: + +- $I_{\lambda}$ is the final illumination of the pixel on an object +- $k_a$ is the material's ambient reflectivity +- $k_d$ is the material's diffuse reflectivity +- $k_s$ is the material's specular reflectivity +- $n_\textrm{lights}$ is the number of lights +- $f_\textrm{att}$ is the light attenuation factor (1.0 if attenuation is not on) +- $S_i$ is the shadow coefficient for light $i$ +- $IL_{i\lambda}$ is the intensity of light $i$ +- $O_{d\lambda}$ is the object's diffuse color +- $O_{s\lambda}$ is the object's specular color +- $\vec{N}$ is the normal vector to the object's surface +- $\vec{L_i}$ is the direction from the intersection point to the light $i$ +- $\vec{H_i}$ is halfway between the direction to the light $i$ and the + direction to the viewer +- $n$ is the exponent for the specular component + +In this report we will look through how these various factors influence the +rendering of the scene. All the images along with their source `.txt` files, +rendered `.ppm` files, and converted `.png` files can be found in the `examples` +directory of this handin. + +## Varying $k_a$ + +$k_a$ is the strength of ambient light. It's used as a coefficient for the +object's diffuse color, which keeps a constant value independent of the +positions of the object, light, and the viewer. In the image below, I varied +$k_a$ between 0.2 and 1. Note how the overall color of the ball increases or +decreases in brightness when all other factors remain constant. + +![Varying $k_a$](examples/ka-demo.png){width=360px} +\ + +## Varying $k_d$ + +$k_d$ is the strength of the diffuse component. It also affects an object's +diffuse color, but at a strength that's affected by how much of it faces the +light. Much like the dark side of the moon, the parts of the object that aren't +pointed at the light will not receive as much of the light's influence. In the +image below, I varied $k_d$ between 0.2 and 1. Note how the part pointed to the +light changes the strength of the brightness as all other factors remain +constant. + +![Varying $k_d$](examples/kd-demo.png){width=360px} +\ + +## Varying $k_s$ + +$k_s$ is the specular strength. It uses the object's specular color, which is +like its reflective component. When there is a large specular $k_s$, there's a +shine that appears on the object with a greater intensity. In the image below, I +varied $k_s$ between 0.2 and 1. Note how the whiteness of the light is more +reflective in higher $k_s$ values as other factors remain constant. + +![Varying $k_s$](examples/ks-demo.png){width=360px} +\ + +## Varying $n$ + +$n$ is the exponent saying how big the radius of the specular highlight should +be. In the equation, increasing the exponent usually leads to smaller shines. In +the image below, I varied $n$ between 2 and 100. Note how the size of the shine +is the same intensity, but more focused but covers a smaller area as $n$ +increases. + +![Varying $n$](examples/n-demo.png){width=360px} +\ + +## Multiple lights + +Multiple lights are handled by multiplying each light against an intensity +level, and then added together. Unfortunately, this means that the intensity of +each light can't be too bright. We rely on the image to not use lights that are +too bright. Because this may result in color values above 1.0, the final value +is clamped against 1.0. Below is an example of a scene with two lights; one to +the left and one to the right: + +![Multiple lights](examples/multiple-lights-demo.png){width=360px} +\ + +## Shadows + +Shadows are implemented by pointing a second ray between the intersection point +of the original view ray and each light. If the light has something obstructing +it in the middle, the light's effect is not used. + +The soft shadow effect is realized by jittering rays across an area. In my +implementation, a jitter radius of about 1.0 is used, and 75 rays are shot into +uniformly sampled points within that radius. This also has the side effect that +rays that are closer to the original ray are sampled more frequently. Each of +these rays produces either 0 or 1 depending on if it was obstructed by the +object. Taking the proportion of rays that hit as a coefficient for the shadow, +we can get some soft shadow effects like this: + +![Soft shadows](examples/soft-shadow-demo.png){width=360px} +\ + +## Light attenuation + +Light attenuation is when more of the light is applied for objects that are +closer to a particular light source. The function that's applied is an inverse +quadratic formula with respect to the distance the object is from the light: + +\begin{equation} +f_\textrm{att}(d) = \frac{1}{c_1 + c_2 d + c_3 d^2} +\end{equation} + +Where: + +- $f_\textrm{att}$ is the attenuation factor +- $d$ is the distance the object is from the light +- $c_1$, $c_2$, and $c_3$ are user-supplied coefficients + +As you can see below, the effect of the light drops off with the distance from +the light (light coming from the left): + +![Light attenuation](examples/attenuation-demo.png){width=360px} +\ + +## Depth Cueing + +Depth cueing is when the objects further from the viewer have a lower opacity to +"fade" into the background in some sense. A good example of this can be seen in +the image below; note how the objects are less and less bright the further they +are away from the eye. + +![Depth cueing](examples/depth-cueing-demo.png){width=360px} +\ + +## Shortcomings of the model + +The Phong formula is just a model of how light works, and doesn't actually +represent reality. There's not actually rays physically escaping our eyes and +hitting objects; it's actually the other way around, but computing it that way +would not be efficient since we would be factoring in a lot of rays that don't +ever get rendered. + +Also, one needs to take care to use reasonable constants. For example, if using +a different specular light color than the diffuse color, then it may produce +some bizarre lighting effects that may not actually look right compare to +reality. + +# Arbitrary Objects + +Here is an example scene with some objects that demonstrates some of the +features of the raytracer. + +![Objects in the scene](examples/objects.png){width=360px} +\ diff --git a/participation/2023-02-15.md b/participation/2023-02-15.md index 9172581..d36ab14 100644 --- a/participation/2023-02-15.md +++ b/participation/2023-02-15.md @@ -5,7 +5,14 @@ the points on the side of the triangle in the triangle or not?** _Hint: Two triangles that share a border may have problems._ +If the triangles share a border, then you will have to decide whether points on +the border belong to one or the other. I think there might need to be some kind +of average function or something that decides how different values might be +reconciled, but I think omitting the border from either might also not be the +right solution because you really don't want there to be a gap. + # Participation Question 2 **When d11 \* d22 - d12 \* d12 = 0, the system will have no solution. When might this happen, and what does it mean?** +