add assignment 1d

This commit is contained in:
Michael Zhang 2023-03-20 16:10:01 -05:00
parent 70cba6d253
commit a84ee1724d
27 changed files with 4244 additions and 0 deletions

10
assignment-1d/.gitignore vendored Normal file
View file

@ -0,0 +1,10 @@
/target
/assignment-1c
/raytracer1c
/examples/*.png
*.ppm
*.zip
*.pdf
perf.data*
flamegraph.svg
showcase.png

View file

980
assignment-1d/Cargo.lock generated Normal file
View file

@ -0,0 +1,980 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "addr2line"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97"
dependencies = [
"gimli",
]
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "anyhow"
version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61"
dependencies = [
"backtrace",
]
[[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-1d"
version = "0.1.0"
dependencies = [
"anyhow",
"base64",
"clap",
"derivative",
"generator",
"itertools",
"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 = "backtrace"
version = "0.3.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca"
dependencies = [
"addr2line",
"cc",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
]
[[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 = "generator"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d266041a359dfa931b370ef684cceb84b166beb14f7f0421f4a6a3d0c446d12e"
dependencies = [
"cc",
"libc",
"log",
"rustversion",
"windows",
]
[[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 = "gimli"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4"
[[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 = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[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 = "memchr"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "memoffset"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4"
dependencies = [
"autocfg",
]
[[package]]
name = "miniz_oxide"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa"
dependencies = [
"adler",
]
[[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 = "object"
version = "0.30.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439"
dependencies = [
"memchr",
]
[[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 = "rustc-demangle"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
[[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 = "rustversion"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70"
[[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"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1c4bd0a50ac6020f65184721f758dba47bb9fbc2133df715ec74a237b26794a"
dependencies = [
"windows_aarch64_msvc 0.39.0",
"windows_i686_gnu 0.39.0",
"windows_i686_msvc 0.39.0",
"windows_x86_64_gnu 0.39.0",
"windows_x86_64_msvc 0.39.0",
]
[[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 0.42.1",
"windows_i686_gnu 0.42.1",
"windows_i686_msvc 0.42.1",
"windows_x86_64_gnu 0.42.1",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc 0.42.1",
]
[[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.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7711666096bd4096ffa835238905bb33fb87267910e154b18b44eaabb340f2"
[[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.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "763fc57100a5f7042e3057e7e8d9bdd7860d330070251a73d003563a3bb49e1b"
[[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.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bc7cbfe58828921e10a9f446fcaaf649204dcfe6c1ddd712c5eebae6bda1106"
[[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.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6868c165637d653ae1e8dc4d82c25d4f97dd6605eaa8d784b5c6e0ab2a252b65"
[[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.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e4d40883ae9cae962787ca76ba76390ffa29214667a111db9e0a1ad8377e809"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"

34
assignment-1d/Cargo.toml Normal file
View file

@ -0,0 +1,34 @@
[package]
name = "assignment-1d"
authors = ["Michael Zhang <zhan4854@umn.edu>"]
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 = "raytracer1d"
path = "src/main.rs"
[dependencies]
anyhow = { version = "1.0.68", features = ["backtrace"] }
base64 = "0.21.0"
clap = { version = "4.1.4", features = ["cargo", "derive"] }
derivative = "2.2.0"
generator = "0.7.2"
itertools = "0.10.5"
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"

46
assignment-1d/Makefile Normal file
View file

@ -0,0 +1,46 @@
.PHONY: all clean
.PRECIOUS: $(EXAMPLES_PPM)
RAYTRACER_FLAGS :=
DOCKER := docker
ZIP := zip
PANDOC := pandoc
CONVERT := convert
HANDIN := ./hw1d.michael.zhang.zip
BINARY := ./raytracer1d
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/raytracer1d $@
$(HANDIN): $(BINARY) Makefile Cargo.toml Cargo.lock README.md $(EXAMPLES_PNG) $(EXAMPLES_PPM)
$(ZIP) -r $@ src examples $^
examples/%.ppm: examples/%.txt $(SOURCES)
cargo run --release -- -o $@ $(RAYTRACER_FLAGS) $<
examples/%.png: examples/%.ppm
convert $< $@
clean:
rm -rf target/docker \
$(HANDIN) $(BINARY) \
$(EXAMPLES_PPM) $(EXAMPLES_PNG)

29
assignment-1d/README.md Normal file
View file

@ -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`.

2
assignment-1d/examples/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
# Necessary files
!/earthtexture.ppm

View file

@ -0,0 +1,17 @@
eye 0 0 0
viewdir 0 0 -1
updir 0 1 0
hfov 60
imsize 512 256
bkgcolor 0.1 0.1 0.1
light -2 1 0 1 1 1 1
mtlcolor 1 0 1 1 1 1 0.1 0.4 0.4 20
v -2 1 -5
v -1 -1 -4
v 1 -1 -4
v 2 1 -6
f 1 2 3
f 1 3 4

View file

@ -0,0 +1,21 @@
eye 0 0 0
viewdir 0 0 -1
updir 0 1 0
hfov 60
imsize 512 256
bkgcolor 0.1 0.1 0.1
light -2 1 0 1 1 1 1
mtlcolor 1 0 1 1 1 1 0.1 0.4 0.4 20
v -2 1 -5
v -1 -1 -4
v 1 -1 -4
v 2 1 -6
vn -2 1 1
vn -1 -1 1
vn 2 1 1
f 1//1 2//2 3//3
f 1 3 4

View file

@ -0,0 +1,10 @@
eye 2 -6 1
viewdir -1 3 -0.5
updir 0 0 1
hfov 50
imsize 512 512
bkgcolor 0.5 0.7 0.9
light 0 1 -1 0 1 1 1
mtlcolor 0 1 0 1 1 1 0.2 0.8 0.1 20
texture earthtexture.ppm
sphere 0 0 0 2

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,27 @@
eye 0 0 4
viewdir 0 0 -1
updir 1 1 0
hfov 60
imsize 640 480
bkgcolor 0.4 0.4 0.4
light -2 1 0 1 1 1 1
mtlcolor 1 0.6 1 1 1 1 0.1 0.4 0.4 20
v 0 0 0
v 1 0 -1
v 0 1 -1
v -1 0 -1
v 0 -1 -1
vn 0 0 1
vn 1 0 1
vn 0 1 1
vn -1 0 1
vn 0 -1 1
f 1//1 2//2 3//3
f 1//1 3//3 4//4
f 1//1 4//4 5//5
f 1//1 5//5 2//2

View file

@ -0,0 +1,45 @@
imsize 1024 768
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

127
assignment-1d/src/image.rs Normal file
View file

@ -0,0 +1,127 @@
use std::{
fs::File,
io::{BufRead, BufReader, Read, Write},
path::Path,
};
use anyhow::{Context, Result};
use generator::{done, Gn};
use itertools::Itertools;
use nalgebra::Vector3;
/// A pixel color represented by a red, green, and blue value in the range 0-1.
pub type Color = Vector3<f64>;
/// A representation of an image
#[derive(Derivative)]
#[derivative(Debug)]
pub struct Image {
/// Width in pixels
pub width: usize,
/// Height in pixels
pub height: usize,
/// Pixel data in row-major form.
#[derivative(Debug = "ignore")]
pub data: Vec<Color>,
}
impl Image {
pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
let file = File::open(path)
.with_context(|| format!("Could not open file at {path:?}"))?;
Self::read(file)
}
/// Parse image from a Read
pub fn read(r: impl Read + Send) -> Result<Self> {
let mut line_reader = BufReader::new(r);
let mut header = String::new();
line_reader
.read_line(&mut header)
.context("Could not read line")?;
let parts = header.trim().split(" ").collect::<Vec<_>>();
let width = parts[1].parse::<usize>().context("Could not read width")?;
let height = parts[2].parse::<usize>().context("Could not read height")?;
let max_value = parts[3]
.parse::<usize>()
.context("Could not read max value")?;
// Generator for reading numbers
let numbers = Gn::<()>::new_scoped(move |mut s| {
macro_rules! gen_try {
($expr:expr, $str:expr $(, $($arg:expr),* $(,)?)?) => {
match $expr {
Ok(v) => v,
Err(e) => {
s.yield_(
Err(anyhow::Error::from(e))
.with_context(|| format!($str $(, $($arg,)*)?)),
);
done!();
}
}
};
}
for line in line_reader.lines() {
let line = gen_try!(line, "Could not read line");
let parts = line.trim().split_whitespace();
for part in parts {
let int =
gen_try!(part.parse::<u64>(), "Could not read int from: {}", part);
s.yield_(Ok(int));
}
}
done!()
});
let mut data = Vec::with_capacity(width * height);
for mut chunk in &(numbers).chunks(3) {
let (r, g, b) = match chunk.next_tuple() {
Some(v) => v,
None => bail!("Not enough elements"),
};
let r = r? as f64 / max_value as f64;
let g = g? as f64 / max_value as f64;
let b = b? as f64 / max_value as f64;
let color = Color::new(r, g, b);
data.push(color);
}
Ok(Image {
width,
height,
data,
})
}
/// 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(())
}
}

17
assignment-1d/src/lib.rs Normal file
View file

@ -0,0 +1,17 @@
use nalgebra::{Vector2, Vector3};
#[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;
pub type Point2 = Vector2<f64>;
pub type Point = Vector3<f64>;
pub type Vector = Vector3<f64>;

137
assignment-1d/src/main.rs Normal file
View file

@ -0,0 +1,137 @@
#[macro_use]
extern crate tracing;
use std::fs::File;
use std::path::PathBuf;
use anyhow::Result;
use assignment_1d::{image::Image, ray::Ray, 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<PathBuf>,
/// 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(&scene, &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::<Result<Vec<_>>>()?;
// 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, intersection_context)?
}
// There was no intersection, so this should default to the scene's
// background color
None => scene.bkg_color,
})
})
.collect::<Result<Vec<_>>>()?;
// 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(())
}

27
assignment-1d/src/ray.rs Normal file
View file

@ -0,0 +1,27 @@
use crate::{Point, Vector};
/// 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: Point,
pub direction: Vector,
}
impl Ray {
/// Construct a ray from endpoints
pub fn from_endpoints(start: Point, end: Point) -> 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) -> Point {
self.origin + self.direction * time
}
}

View file

@ -0,0 +1,206 @@
use anyhow::Result;
use ordered_float::NotNan;
use crate::utils::compute_rotation_matrix;
use crate::Vector;
use crate::{ray::Ray, Point};
use super::illumination::IntersectionContext;
use super::Scene;
#[derive(Debug)]
pub struct Cylinder {
pub center: Point,
pub direction: Vector,
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,
_: &Scene,
ray: &Ray,
) -> Result<Option<IntersectionContext>> {
// Determine rotation matrix for turning the cylinder upright along the
// Z-axis
let target_direction = Vector::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 =
Vector::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 crate::{ray::Ray, scene::Scene, Point, Vector};
use super::Cylinder;
#[test]
fn test_cylinder() {
let cylinder = Cylinder {
center: Point::new(0.0, 0.0, 0.0),
direction: Vector::new(0.0, 1.0, 0.0),
radius: 3.0,
length: 4.0,
};
let eye = Point::new(0.0, 3.0, 3.0);
let end = Point::new(0.0, 2.0, 2.0);
let ray = Ray::from_endpoints(eye, end);
let scene = Scene::default();
let _res = cylinder.intersects_ray_at(&scene, &ray);
// panic!("Result: {res:?}");
}
}

View file

@ -0,0 +1,187 @@
use std::fmt::Debug;
use crate::image::Color;
use crate::utils::cross;
use crate::Point;
use super::Scene;
#[derive(Debug)]
pub struct Rect {
pub upper_left: Point,
pub upper_right: Point,
pub lower_left: Point,
pub lower_right: Point,
}
#[derive(Debug)]
pub struct Material {
pub diffuse_color: Point,
pub specular_color: Point,
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: Point,
/// Whether light attenuation is enabled for this light
attenuation: Option<Attenuation>,
},
/// A directional light source exists at an infinitely far location but emits
/// light in a specific direction
Directional { direction: Point },
}
#[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: Point,
}
impl Light {
/// Get the unit directional vector pointing from the given point to this
/// light source
pub fn direction_from(&self, point: Point) -> Point {
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) -> Point {
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
}
}
}

View file

@ -0,0 +1,294 @@
use std::iter;
use anyhow::Result;
use ordered_float::NotNan;
use rand::Rng;
use rayon::prelude::{
IndexedParallelIterator, IntoParallelIterator, IntoParallelRefIterator,
ParallelIterator,
};
use crate::{image::Color, ray::Ray, utils::dot, Point, Vector};
use super::{
data::{DepthCueing, Light, LightKind},
object::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,
object: &Object,
intersection_context: IntersectionContext,
) -> Result<Color> {
let material = match self.materials.get(object.material_idx) {
Some(v) => v,
None => bail!("Material index not found."),
};
let diffuse_color = match object.texture_idx {
Some(texture_idx) => {
let (u, v) = object
.kind
.get_texture_coord(&self, &intersection_context)?;
let texture = match self.textures.get(texture_idx) {
Some(v) => v,
None => bail!("Texture index not found."),
};
texture.pixel_at(u, v)
}
None => material.diffuse_color,
};
let ambient_component = material.k_a * diffuse_color;
// Diffuse and specular lighting for each separate light
let diffuse_and_specular: Color = 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
* 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));
Ok(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: Point,
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(&self, &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::<Vec<_>>();
match intersections.is_empty() {
true => 1.0,
false => {
let average = intersections.iter().cloned().sum::<f64>()
/ intersections.len() as f64;
average
}
}
}
fn compute_soft_shadow_coefficient(
&self,
light_location: Point,
original_intersection_point: Point,
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 = Vector::new(x, y, z);
light_location + delta
})
.take(JITTER_RAYS)
.collect::<Vec<_>>();
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(&self, &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<f64>,
/// The intersection point.
#[derivative(PartialEq = "ignore", Ord = "ignore")]
pub point: Point,
/// The normal vector protruding from the surface of the object at the
/// intersection point
#[derivative(PartialEq = "ignore", Ord = "ignore")]
pub normal: Vector,
}
impl Eq for IntersectionContext {}
impl IntersectionContext {}

View file

@ -0,0 +1,398 @@
use std::fs::File;
use std::io::Read;
use std::path::Path;
use anyhow::{Context, Result};
use itertools::Itertools;
use nalgebra::Vector3;
use crate::{
image::{Color, Image},
scene::{
cylinder::Cylinder,
data::{Attenuation, Light, LightKind, Material},
object::{Object, ObjectKind},
sphere::Sphere,
texture::{Texture, NormalMap},
triangle::Triangle,
Scene,
},
Point, Vector,
};
use super::data::DepthCueing;
impl Scene {
/// Parse the input file into a scene
pub fn from_input_file(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
// 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)?;
file.read_to_string(&mut contents)?;
contents
};
let mut scene = Scene::default();
let mut material_idx = None;
let mut texture_idx = None;
for line in contents.lines() {
// Split lines into words. `parts' is an iterator, which is consumed upon
// iterating, rather than collected into a Vec
let mut parts = line.split_whitespace();
// The keyword is the very first space-separated substring, and tells us
// how to interpret the rest
let keyword = match parts.next() {
Some(v) => v,
None => continue,
};
/// Short for "read", macro for reading something from the iterator and
/// converting it into the appropriate format given by $ty. For this to
/// work, $ty must implement Construct
macro_rules! r {
($ty:ty) => {
<$ty>::construct(&mut parts, ())
.with_context(|| format!("Could not parse {}", stringify!($ty)))?
};
($ty:ty, $($ex:expr),* $(,)?) => {
<$ty>::construct(&mut parts, $($ex,)*)
.with_context(|| format!("Could not parse {}", stringify!($ty)))?
};
}
/// Shortcut for unwrapping one of the state `Option's
macro_rules! u {
($expr:expr) => {
match $expr {
Some(v) => v,
None => {
bail!(
"Each object must be preceded by a `{}` line",
stringify!($expr)
)
}
}
};
}
match keyword {
"imsize" => {
scene.image_width = r!(usize);
scene.image_height = r!(usize);
}
"projection" => {
if let Some("parallel") = parts.next() {
scene.parallel_projection = true;
}
}
"eye" => scene.eye_pos = r!(Vector3<f64>),
"viewdir" => scene.view_dir = r!(Vector3<f64>),
"updir" => scene.up_dir = r!(Vector3<f64>),
"hfov" => scene.hfov = r!(f64),
"bkgcolor" => scene.bkg_color = r!(Color),
// light x y z w r g b
// attlight x y z w r g b c1 c2 c3
"light" | "attlight" => {
let vec3 = r!(Vector3<f64>);
let w = r!(usize);
let color = r!(Color);
let attenuation = match keyword == "attlight" {
true => {
let c = r!(Vector3<f64>);
Some(Attenuation {
c1: c.x,
c2: c.y,
c3: c.z,
})
}
false => None,
};
let kind = match w as usize {
0 => LightKind::Directional { direction: vec3 },
1 => LightKind::Point {
location: vec3,
attenuation,
},
_ => bail!("Invalid w; must be either 0 or 1"),
};
let light = Light { kind, color };
scene.lights.push(light);
}
// depthcueing dcr dcg dcb amax amin distmax distmin
"depthcueing" => {
let color = r!(Color);
let a_max = r!(f64);
let a_min = r!(f64);
let dist_max = r!(f64);
let dist_min = r!(f64);
scene.depth_cueing = DepthCueing {
color,
a_max,
a_min,
dist_max,
dist_min,
};
}
// mtlcolor Odr Odg Odb Osr Osg Osb ka kd ks n
"mtlcolor" => {
let diffuse_color = r!(Color);
let specular_color = r!(Color);
let k_a = r!(f64);
let k_d = r!(f64);
let k_s = r!(f64);
let exponent = r!(f64);
let material = Material {
diffuse_color,
specular_color,
k_a,
k_d,
k_s,
exponent,
};
let idx = scene.materials.len();
material_idx = Some(idx);
scene.materials.push(material);
}
"sphere" => {
let center = r!(Point);
let radius = r!(f64);
scene.objects.push(Object {
kind: ObjectKind::Sphere(Sphere { center, radius }),
material_idx: u!(material_idx),
texture_idx,
});
}
"cylinder" => {
let center = r!(Point);
let direction = r!(Vector);
let radius = r!(f64);
let length = r!(f64);
scene.objects.push(Object {
kind: ObjectKind::Cylinder(Cylinder {
center,
direction,
radius,
length,
}),
material_idx: u!(material_idx),
texture_idx,
});
}
// Assignment 1C: Triangles and textures
// v x y z
"v" => scene.triangle_vertices.push(r!(Vector)),
// vn nx ny nz
"vn" => scene.vertex_normals.push(r!(Vector)),
// f v1 v2 v3
// f v1//n1 v2//n2 v3//n3
"f" => {
let v1 = r!(TriangleVertex);
let v2 = r!(TriangleVertex);
let v3 = r!(TriangleVertex);
let vs = Vector3::new(v1, v2, v3);
let vertices = vs.map(|v| v.vertex_idx);
let normals = vs.map(|v| v.normal_idx);
let normals = match normals.iter().filter(|o| o.is_some()).count() {
0 => None,
n if n == vs.len() => Some(normals.map(|o| o.unwrap())),
_ => bail!("Cannot mix and match having a normal index"),
};
let textures = vs.map(|v| v.texture_idx);
let textures = match textures.iter().filter(|o| o.is_some()).count() {
0 => None,
n if n == vs.len() => Some(textures.map(|o| o.unwrap())),
_ => bail!("Cannot mix and match having a normal index"),
};
let triangle = Triangle {
vertices,
normals,
textures,
};
scene.objects.push(Object {
kind: ObjectKind::Triangle(triangle),
material_idx: u!(material_idx),
texture_idx,
});
}
"texture" => {
let input_parent = path.parent().unwrap().to_path_buf();
let path = match parts.next() {
Some(s) => input_parent.join(s),
None => bail!("Did not provide path."),
};
let idx = scene.textures.len();
texture_idx = Some(idx);
let image = Image::from_file(path)?;
let texture = Texture::new(image);
scene.textures.push(texture);
}
"bump" => {
let input_parent = path.parent().unwrap().to_path_buf();
let path = match parts.next() {
Some(s) => input_parent.join(s),
None => bail!("Did not provide path."),
};
let idx = scene.textures.len();
texture_idx = Some(idx);
let image = Image::from_file(path)?;
let normal_map = NormalMap::new(image);
scene.normal_maps.push(normal_map);
}
_ => bail!("Unknown keyword {keyword}"),
}
}
Ok(scene)
}
}
pub trait Construct: Sized {
type Args;
/// Construct an element of this type from an iterator over strings.
fn construct<'a, I>(it: &mut I, args: Self::Args) -> Result<Self>
where
I: Iterator<Item = &'a str>;
}
impl<T: Construct> Construct for Option<T> {
type Args = T::Args;
fn construct<'a, I>(it: &mut I, args: Self::Args) -> Result<Self>
where
I: Iterator<Item = &'a str>,
{
let mut peeker = it.peekable();
if peeker.peek().is_none() {
return Ok(None);
}
T::construct(&mut peeker, args).map(Some)
}
}
macro_rules! impl_construct {
($ty:ty) => {
impl Construct for $ty {
type Args = ();
fn construct<'a, I>(it: &mut I, _: Self::Args) -> Result<Self>
where
I: Iterator<Item = &'a str>,
{
let item = match it.next() {
Some(v) => v,
None => bail!("Ran out of items"),
};
Ok(item.parse()?)
}
}
};
}
impl_construct!(f64);
impl_construct!(usize);
impl Construct for Vector3<f64> {
type Args = ();
fn construct<'a, I>(it: &mut I, _: Self::Args) -> Result<Self>
where
I: Iterator<Item = &'a str>,
{
let (x, y, z) = match it.next_tuple() {
Some(v) => v,
None => bail!("Expected 3 values"),
};
let x: f64 = x.parse()?;
let y: f64 = y.parse()?;
let z: f64 = z.parse()?;
Ok(Vector3::new(x, y, z))
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
struct TriangleVertex {
vertex_idx: usize,
normal_idx: Option<usize>,
texture_idx: Option<usize>,
}
impl Construct for TriangleVertex {
type Args = ();
fn construct<'a, I>(it: &mut I, _: Self::Args) -> Result<Self>
where
I: Iterator<Item = &'a str>,
{
let s = match it.next() {
Some(v) => v,
None => bail!("Waiting on another"),
};
// Note: indexed by 1 not 0, so we will just do the subtraction
// here to avoid having to deal with it later
let parts = s.split("/").collect_vec();
ensure!(parts.len() >= 1 && parts.len() <= 3);
let vertex_idx: usize = parts[0].parse::<usize>()? - 1;
let texture_idx =
match parts.get(1).and_then(|s| (!s.is_empty()).then(|| *s)) {
Some(s) => Some(s.parse::<usize>()? - 1),
None => None,
};
let normal_idx =
match parts.get(2).and_then(|s| (!s.is_empty()).then(|| *s)) {
Some(s) => Some(s.parse::<usize>()? - 1),
None => None,
};
Ok(TriangleVertex {
vertex_idx,
texture_idx,
normal_idx,
})
}
}

View file

@ -0,0 +1,51 @@
pub mod cylinder;
pub mod data;
pub mod illumination;
pub mod input_file;
pub mod object;
pub mod sphere;
pub mod texture;
pub mod triangle;
use crate::image::Color;
use crate::{Point, Point2, Vector};
use self::data::{Attenuation, DepthCueing, Light, Material};
use self::object::Object;
use self::texture::{Texture, NormalMap};
#[derive(Debug, Default)]
pub struct Scene {
pub eye_pos: Point,
pub view_dir: Vector,
pub up_dir: Vector,
/// 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<Material>,
pub lights: Vec<Light>,
pub objects: Vec<Object>,
/// List of textures
pub textures: Vec<Texture>,
/// List of normal maps (Extra credit)
pub normal_maps: Vec<NormalMap>,
/// Coordinates into a texture image
pub texture_vertices: Vec<Point2>,
/// Triangle vertices
pub triangle_vertices: Vec<Point>,
pub vertex_normals: Vec<Vector>,
}

View file

@ -0,0 +1,61 @@
use anyhow::Result;
use crate::ray::Ray;
use super::cylinder::Cylinder;
use super::illumination::IntersectionContext;
use super::sphere::Sphere;
use super::triangle::Triangle;
use super::Scene;
/// An object in the scene
#[derive(Debug)]
pub struct Object {
pub kind: ObjectKind,
/// Index into the scene's material color list
pub material_idx: usize,
/// If this object has a texture, this is the index into the texture list
pub texture_idx: Option<usize>,
}
#[derive(Debug)]
pub enum ObjectKind {
Sphere(Sphere),
Cylinder(Cylinder),
Triangle(Triangle),
}
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,
scene: &Scene,
ray: &Ray,
) -> Result<Option<IntersectionContext>> {
match self {
ObjectKind::Sphere(sphere) => sphere.intersects_ray_at(scene, ray),
ObjectKind::Cylinder(cylinder) => cylinder.intersects_ray_at(scene, ray),
ObjectKind::Triangle(triangle) => triangle.intersects_ray_at(scene, ray),
}
}
/// Get the (u, v) coordinates in the texture (between 0 and 1) that
/// corresponds to the intersection point
pub fn get_texture_coord(
&self,
scene: &Scene,
ctx: &IntersectionContext,
) -> Result<(f64, f64)> {
match self {
ObjectKind::Sphere(sphere) => sphere.get_texture_coord(ctx),
ObjectKind::Cylinder(cylinder) => todo!(),
ObjectKind::Triangle(triangle) => triangle.get_texture_coord(scene, ctx),
}
}
}

View file

@ -0,0 +1,94 @@
use std::f64::consts::PI;
use anyhow::Result;
use ordered_float::NotNan;
use crate::{ray::Ray, utils::min_f64, Point};
use super::illumination::IntersectionContext;
use super::Scene;
#[derive(Debug)]
pub struct Sphere {
pub center: Point,
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,
_: &Scene,
ray: &Ray,
) -> Result<Option<IntersectionContext>> {
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,
}))
}
pub fn get_texture_coord(
&self,
ctx: &IntersectionContext,
) -> Result<(f64, f64)> {
// Reverse engineer the angles from the coordinate of the intersection
let cosp = (ctx.point.z - self.center.z) / self.radius;
let phi = cosp.acos();
let theta =
(ctx.point.y - self.center.y).atan2(ctx.point.x - self.center.x);
// Map theta and phi into 0 - 1 range
let v = phi / PI;
let u = 0.5 + theta / (2.0 * PI);
Ok((u, v))
}
}

View file

@ -0,0 +1,64 @@
use crate::{
image::{Color, Image},
Vector,
};
#[derive(Debug)]
pub struct Texture(Image);
impl Texture {
pub fn new(image: Image) -> Self {
Self(image)
}
pub fn pixel_at_exact(&self, x: usize, y: usize) -> Color {
// TODO: Debug asserts?
self.0.data[y * self.0.width + x]
}
/// Returns a pixel at the given coordinate. For non-lattice coordinates,
/// bi-linear interpolation of the image is done.
pub fn pixel_at(&self, u: f64, v: f64) -> Color {
debug_assert!(0.0 <= u && u <= 1.0, "u must be between 0 and 1");
debug_assert!(0.0 <= v && v <= 1.0, "u must be between 0 and 1");
// Slide 121
let x = u * (self.0.width - 1) as f64;
let y = v * (self.0.height - 1) as f64;
let i = x.floor();
let j = y.floor();
let alpha = x - i;
let beta = y - j;
let i = i as usize;
let j = j as usize;
(1.0 - alpha) * (1.0 - beta) * self.pixel_at_exact(i, j)
+ (alpha) * (1.0 - beta) * self.pixel_at_exact(i + 1, j)
+ (1.0 - alpha) * (beta) * self.pixel_at_exact(i, j + 1)
+ (alpha) * (beta) * self.pixel_at_exact(i + 1, j + 1)
}
}
#[derive(Debug)]
pub struct NormalMap(Image);
impl NormalMap {
pub fn new(image: Image) -> Self {
Self(image)
}
pub fn normal_vector_at_exact(&self, x: usize, y: usize) -> Vector {
let vec = self.0.data[y * self.0.width + x];
// So, according to the instructions, this should actually be a value
// between -1 and 1. However, we're reading this in through an image.
// I'm just going to do the lazy thing here (which theoretically
// actually saves cycles) by only doing the transformation when loading
// out of the image
vec.map(|value| 2.0 * value / 255.0 - 1.0)
}
}

View file

@ -0,0 +1,184 @@
use std::f64::EPSILON;
use anyhow::Result;
use nalgebra::{Matrix2, Vector2, Vector3};
use ordered_float::NotNan;
use crate::ray::Ray;
use crate::utils::{cross, dot};
use crate::{Point, Vector};
use super::illumination::IntersectionContext;
use super::Scene;
#[derive(Debug)]
pub struct Triangle {
/// Indexes into the scene's vertex list
pub vertices: Vector3<usize>,
/// Indexes into the scene's normal coordinates list
pub normals: Option<Vector3<usize>>,
/// Indexes into the scene's texture coordinates list
pub textures: Option<Vector3<usize>>,
}
impl Triangle {
pub fn intersects_ray_at(
&self,
scene: &Scene,
ray: &Ray,
) -> Result<Option<IntersectionContext>> {
let (p0, e1, e2) = self.basis_vectors(scene);
// Solve for the plane equation coefficients A, B, C, D such that:
//
// $$
// Ax + By + Cz + D = 0
// $$
let n = cross(e1, e2);
let a = n.x;
let b = n.y;
let c = n.z;
// Sub in p0 to solve for D
let d = -(a * p0.x + b * p0.y + c * p0.z);
// Find the intersection point
let time = {
let (x0, y0, z0, xd, yd, zd) =
match (ray.origin.as_slice(), ray.direction.as_slice()) {
([x0, y0, z0], [xd, yd, zd]) => (x0, y0, z0, xd, yd, zd),
_ => unreachable!("lol rip no tuple interface"),
};
let denom = a * xd + b * yd + c * zd;
if denom == 0.0 {
// The ray is parallel to the plane, so there is no intersection point.
return Ok(None);
};
-(a * x0 + b * y0 + c * z0 + d) / denom
};
let time = NotNan::new(time)?;
let point = ray.eval(*time);
// Use barycentric coordinates to determine if the point is inside of the
// triangle
// p = p0 + beta * e1 + gamma * e2
// Using the whack linear algebra approach derived on slide 57
let ep = point - p0;
let p = Vector2::new(dot(e1, ep), dot(e2, ep));
let (alpha, beta, gamma) =
self.compute_barycentric_coordinates(scene, p)?;
// Each of alpha, beta, and gamma must be between 0 and 1
if ![alpha, beta, gamma]
.into_iter()
.all(|v| 0.0 - EPSILON <= v && v <= 1.0 + EPSILON)
{
return Ok(None);
}
let normal = match self.normals {
// If surface normals are provided, then interpolate the normals to do
// smooth shading
Some(normals) => {
let n0 = scene.vertex_normals[normals.x];
let n1 = scene.vertex_normals[normals.y];
let n2 = scene.vertex_normals[normals.z];
(alpha * n0 + beta * n1 + gamma * n2).normalize()
}
None => n.normalize(),
};
Ok(Some(IntersectionContext {
time,
point,
normal,
}))
}
/// Get the (u, v) texture coordinates corresponding to the point provided in
/// the intersection context
pub fn get_texture_coord(
&self,
scene: &Scene,
ctx: &IntersectionContext,
) -> Result<(f64, f64)> {
let texture_coordinates = match self.textures {
Some(v) => v,
None => {
bail!("Textured triangle requested without providing coordinates")
}
};
let p0 = scene.texture_vertices[texture_coordinates.x];
let p1 = scene.texture_vertices[texture_coordinates.y];
let p2 = scene.texture_vertices[texture_coordinates.z];
let p = self.convert_point(scene, ctx.point);
let (alpha, beta, gamma) =
self.compute_barycentric_coordinates(scene, p)?;
let u = alpha * p0.x + beta * p1.x + gamma * p2.x;
let v = alpha * p0.y + beta * p1.y + gamma * p2.y;
Ok((u, v))
}
fn convert_point(&self, scene: &Scene, point: Point) -> Vector2<f64> {
let (p0, e1, e2) = self.basis_vectors(scene);
let ep = point - p0;
Vector2::new(dot(e1, ep), dot(e2, ep))
}
/// Fetch the corners of the triangles from the scene
#[inline]
fn corner_coordinates(&self, scene: &Scene) -> (Point, Point, Point) {
let p0 = scene.triangle_vertices[self.vertices.x];
let p1 = scene.triangle_vertices[self.vertices.y];
let p2 = scene.triangle_vertices[self.vertices.z];
(p0, p1, p2)
}
/// Get the new basis vectors using p0 as the origin. Returns (p0, e1, e2)
#[inline]
fn basis_vectors(&self, scene: &Scene) -> (Vector, Vector, Vector) {
let (p0, p1, p2) = self.corner_coordinates(scene);
let e1 = p1 - p0;
let e2 = p2 - p0;
(p0, e1, e2)
}
/// Compute barycentric coordinates
fn compute_barycentric_coordinates(
&self,
scene: &Scene,
p: Vector2<f64>,
) -> Result<(f64, f64, f64)> {
let (_, e1, e2) = self.basis_vectors(scene);
let d = Matrix2::new(dot(e1, e1), dot(e1, e2), dot(e2, e1), dot(e2, e2));
let d_inv = match d.try_inverse() {
Some(v) => v,
// TODO: Whack
None => bail!("No inverse..."),
};
let sol = d_inv * p;
let beta = sol.x;
let gamma = sol.y;
// Slide 46
let alpha = 1.0 - beta - gamma;
Ok((alpha, beta, gamma))
}
}

View file

@ -0,0 +1,95 @@
use anyhow::Result;
use nalgebra::{Matrix3, Vector3};
use ordered_float::NotNan;
use crate::{Vector};
/// Finds the minimum of an iterator of f64s, ignoring any NaN values
#[inline]
pub fn min_f64<I>(i: I) -> Option<f64>
where
I: Iterator<Item = f64>,
{
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, F>(i: I, f: F) -> Option<f64>
where
I: Iterator<Item = f64>,
F: FnMut(&NotNan<f64>),
{
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: Vector, b: Vector) -> f64 {
a.x * b.x + a.y * b.y + a.z * b.z
}
/// Cross-product between two 3D vectors.
#[inline]
pub fn cross(a: Vector, b: Vector) -> Vector {
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;
Vector::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<f64>,
b: Vector3<f64>,
) -> Result<Matrix3<f64>> {
// 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)
}