diff --git a/assignment-1b/.gitignore b/assignment-1b/.gitignore new file mode 100644 index 000000000..44d187f --- /dev/null +++ b/assignment-1b/.gitignore @@ -0,0 +1,6 @@ +/target +/assignment-1 +/examples/*.png +*.ppm +*.zip +*.pdf diff --git a/assignment-1b/ASSIGNMENT.md b/assignment-1b/ASSIGNMENT.md new file mode 100644 index 000000000..e69de29 diff --git a/assignment-1b/Cargo.lock b/assignment-1b/Cargo.lock new file mode 100644 index 000000000..e61aaab --- /dev/null +++ b/assignment-1b/Cargo.lock @@ -0,0 +1,649 @@ +# 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", + "clap", + "derivative", + "nalgebra", + "num", + "ordered-float", + "rayon", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[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 = "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 = "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 = "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 = "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 = "paste" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba" + +[[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 = "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 = "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 = "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 = "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 = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[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-1b/Cargo.toml b/assignment-1b/Cargo.toml new file mode 100644 index 000000000..63a1cf3 --- /dev/null +++ b/assignment-1b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "assignment-1b" +authors = ["Michael Zhang "] +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0.68" +clap = { version = "4.1.4", features = ["derive"] } +derivative = "2.2.0" +nalgebra = "0.32.1" +num = { version = "0.4.0", features = ["serde"] } +ordered-float = "3.4.0" +rayon = "1.6.1" diff --git a/assignment-1b/Makefile b/assignment-1b/Makefile new file mode 100644 index 000000000..d7d255e --- /dev/null +++ b/assignment-1b/Makefile @@ -0,0 +1,45 @@ +.PHONY: all clean + +.PRECIOUS: $(EXAMPLES_PPM) + +DOCKER := docker +ZIP := zip +PANDOC := pandoc +CONVERT := convert + +HANDIN := hw1b.michael.zhang.zip +BINARY := ./assignment-1b +WRITEUP := writeup.pdf +SOURCES := $(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) + $(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 \ + rust \ + cargo build --release + mv target/release/assignment-1b $@ + +$(HANDIN): $(BINARY) $(WRITEUP) Makefile Cargo.toml Cargo.lock README.md $(EXAMPLES_PNG) $(EXAMPLES_PPM) + $(ZIP) -r $@ src examples $^ + +examples/%.ppm: examples/%.txt $(SOURCES) + cargo run -- -o $@ $< + +examples/%.png: examples/%.ppm + convert $< $@ + +writeup.pdf: writeup.md $(EXAMPLES_PNG) + $(PANDOC) -o $@ $< + +clean: + rm -f $(HANDIN) $(BINARY) $(WRITEUP) $(EXAMPLES_PPM) $(EXAMPLES_PNG) diff --git a/assignment-1b/README.md b/assignment-1b/README.md new file mode 100644 index 000000000..9d6e0ac --- /dev/null +++ b/assignment-1b/README.md @@ -0,0 +1,25 @@ +# Raycaster + +## Bundle contents + +Writeup is located at `/writeup.pdf`. + +The binary can be found at `/assignment-1`. Run `./assignment-1 --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. + +## 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-1b/assignment-1b b/assignment-1b/assignment-1b new file mode 100755 index 000000000..bc2f626 Binary files /dev/null and b/assignment-1b/assignment-1b differ diff --git a/assignment-1b/examples/cylinder.txt b/assignment-1b/examples/cylinder.txt new file mode 100644 index 000000000..d4392e0 --- /dev/null +++ b/assignment-1b/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-1b/examples/ka-demo.txt b/assignment-1b/examples/ka-demo.txt new file mode 100644 index 000000000..c65d309 --- /dev/null +++ b/assignment-1b/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.1 0.1 0.1 + +light 0 3 3 0 0.3 0.3 0.4 + +mtlcolor 0 1 0 1 1 1 0 0.4 0 1 +sphere -10 0 0 2 + +mtlcolor 0 1 0 1 1 1 0.2 0.4 0 1 +sphere -5 0 0 2 + +mtlcolor 0 1 0 1 1 1 0.4 0.4 0 1 +sphere 0 0 0 2 + +mtlcolor 0 1 0 1 1 1 0.6 0.4 0 1 +sphere 5 0 0 2 + +mtlcolor 0 1 0 1 1 1 0.8 0.4 0 1 +sphere 10 0 0 2 diff --git a/assignment-1b/examples/kd-demo.txt b/assignment-1b/examples/kd-demo.txt new file mode 100644 index 000000000..e5eb7dd --- /dev/null +++ b/assignment-1b/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.1 0.1 0.1 + +light 0 3 3 0 0.3 0.3 0.4 + +mtlcolor 0 1 0 1 1 1 0.2 0 0 1 +sphere -10 0 0 2 + +mtlcolor 0 1 0 1 1 1 0.2 0.2 0 1 +sphere -5 0 0 2 + +mtlcolor 0 1 0 1 1 1 0.2 0.4 0 1 +sphere 0 0 0 2 + +mtlcolor 0 1 0 1 1 1 0.2 0.6 0 1 +sphere 5 0 0 2 + +mtlcolor 0 1 0 1 1 1 0.2 0.8 0 1 +sphere 10 0 0 2 diff --git a/assignment-1b/examples/ks-demo.txt b/assignment-1b/examples/ks-demo.txt new file mode 100644 index 000000000..7108f1c --- /dev/null +++ b/assignment-1b/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.1 0.1 0.1 + +light 0 3 3 0 0.3 0.3 0.4 + +mtlcolor 0 1 0 1 1 1 0.1 0.2 0 15 +sphere -10 0 0 2 + +mtlcolor 0 1 0 1 1 1 0.1 0.2 0.2 15 +sphere -5 0 0 2 + +mtlcolor 0 1 0 1 1 1 0.1 0.2 0.4 15 +sphere 0 0 0 2 + +mtlcolor 0 1 0 1 1 1 0.1 0.2 0.6 15 +sphere 5 0 0 2 + +mtlcolor 0 1 0 1 1 1 0.1 0.2 0.8 15 +sphere 10 0 0 2 diff --git a/assignment-1b/examples/n-demo.txt b/assignment-1b/examples/n-demo.txt new file mode 100644 index 000000000..9a25aae --- /dev/null +++ b/assignment-1b/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.1 0.1 0.1 + +light 0 3 3 0 0.3 0.3 0.4 + +mtlcolor 0 1 0 1 1 1 0.1 0.2 0.4 2 +sphere -10 0 0 2 + +mtlcolor 0 1 0 1 1 1 0.1 0.2 0.4 6 +sphere -5 0 0 2 + +mtlcolor 0 1 0 1 1 1 0.1 0.2 0.4 10 +sphere 0 0 0 2 + +mtlcolor 0 1 0 1 1 1 0.1 0.2 0.4 50 +sphere 5 0 0 2 + +mtlcolor 0 1 0 1 1 1 0.1 0.2 0.4 100 +sphere 10 0 0 2 diff --git a/assignment-1b/examples/objects.txt b/assignment-1b/examples/objects.txt new file mode 100644 index 000000000..8b68339 --- /dev/null +++ b/assignment-1b/examples/objects.txt @@ -0,0 +1,25 @@ +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 0.5 0.5 1 1 1 0.2 0.4 0 10 +sphere -1 -2 -5 2 +sphere 3 -5 -1 0.5 + +mtlcolor 0.5 0.5 1 0.4 0.4 0.4 0.2 0.4 0 10 +sphere 1 2 -3 3 +sphere -6 3 -4 1 + +mtlcolor 0.5 0 0.5 0.6 0.4 0.2 0.2 0.4 0 10 +sphere 5 5 -1 1 +sphere -6 -4 -8 7 +cylinder -2 4 -3 0 0 5 1 4 + +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-1b/examples/sample-1.txt b/assignment-1b/examples/sample-1.txt new file mode 100644 index 000000000..5ed539a --- /dev/null +++ b/assignment-1b/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-1b/src/image.rs b/assignment-1b/src/image.rs new file mode 100644 index 000000000..918bdf7 --- /dev/null +++ b/assignment-1b/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(crate) width: usize, + + /// Height in pixels + pub(crate) height: usize, + + /// Pixel data in row-major form. + pub(crate) 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-1b/src/input_file.rs b/assignment-1b/src/input_file.rs new file mode 100644 index 000000000..9b9dfcf --- /dev/null +++ b/assignment-1b/src/input_file.rs @@ -0,0 +1,138 @@ +use std::{fs::File, io::Read, path::Path}; + +use anyhow::Result; +use nalgebra::Vector3; + +use crate::{ + image::Color, + scene::{ + cylinder::Cylinder, + data::{Light, LightKind, Material, Object, Scene}, + sphere::Sphere, + }, +}; + +/// Parse the input file into a scene +pub fn parse_input_file(path: impl AsRef) -> Result { + 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| { + if parts.len() < start + 3 { + bail!("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" => { + let kind = match parts[3] as usize { + 0 => LightKind::Directional { + direction: read_vec3(0)?, + }, + 1 => LightKind::Point { + location: read_vec3(0)?, + }, + _ => bail!("Invalid w"), + }; + let light = Light { + kind, + color: read_vec3(4)?, + }; + scene.lights.push(light); + } + + // mtlcolor Odr Odg Odb Osr Osg Osb ka kd ks n + "mtlcolor" => { + 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: Box::new(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: Box::new(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-1b/src/main.rs b/assignment-1b/src/main.rs new file mode 100644 index 000000000..6235353 --- /dev/null +++ b/assignment-1b/src/main.rs @@ -0,0 +1,153 @@ +#[macro_use] +extern crate anyhow; +#[macro_use] +extern crate derivative; + +mod image; +mod input_file; +mod ray; +mod scene; +mod utils; + +use std::fs::File; +use std::path::PathBuf; + +use anyhow::Result; +use clap::Parser; +use rayon::prelude::{IntoParallelIterator, ParallelIterator}; + +use crate::image::Image; +use crate::input_file::parse_input_file; +use crate::ray::Ray; + +/// Simple raycaster. +#[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(); + let out_file = opt + .output_path + .unwrap_or_else(|| opt.input_path.with_extension("ppm")); + + let mut scene = parse_input_file(&opt.input_path)?; + let distance = opt.distance; + + if opt.force_parallel { + scene.parallel_projection = true; + } + + // Compute the viewing window + let view_window = scene.compute_viewing_window(distance); + + // Translate image pixels to real-world 3d coords + let translate_pixel = { + let dx = view_window.upper_right - view_window.upper_left; + let pixel_base_x = dx / scene.image_width as f64; + + let dy = view_window.lower_left - view_window.upper_left; + let pixel_base_y = dy / scene.image_height as f64; + + 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 + } + }; + + // 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() + .filter_map(|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((t, object))) + } + Ok(None) => None, + Err(e) => Some(Err(e)), + } + }) + .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((intersection_context, object)) => { + scene.compute_pixel_color(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-1b/src/ray.rs b/assignment-1b/src/ray.rs new file mode 100644 index 000000000..a722ddf --- /dev/null +++ b/assignment-1b/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-1b/src/scene/cylinder.rs b/assignment-1b/src/scene/cylinder.rs new file mode 100644 index 000000000..07459ac --- /dev/null +++ b/assignment-1b/src/scene/cylinder.rs @@ -0,0 +1,179 @@ +use anyhow::Result; +use nalgebra::Vector3; +use ordered_float::NotNan; + +use crate::ray::Ray; +use crate::utils::compute_rotation_matrix; + +use super::data::{IntersectionContext, ObjectKind}; + +#[derive(Debug)] +pub struct Cylinder { + pub center: Vector3, + pub direction: Vector3, + pub radius: f64, + pub length: f64, +} + +impl ObjectKind for Cylinder { + /// Given a cylinder, returns the first time at which this ray intersects the + /// cylinder. + /// + /// If there is no intersection point, returns None. + 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 { + // No solutions here + vec![] + } 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)) + } +} diff --git a/assignment-1b/src/scene/data.rs b/assignment-1b/src/scene/data.rs new file mode 100644 index 000000000..8e67cf3 --- /dev/null +++ b/assignment-1b/src/scene/data.rs @@ -0,0 +1,202 @@ +use std::fmt::Debug; + +use anyhow::Result; +use nalgebra::Vector3; +use ordered_float::NotNan; + +use crate::image::Color; +use crate::ray::Ray; + +pub trait ObjectKind: Debug + Send + Sync { + /// 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. + fn intersects_ray_at(&self, ray: &Ray) + -> Result>; +} + +/// An object in the scene +#[derive(Debug)] +pub struct Object { + pub kind: Box, + + /// 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, + }, + + Directional { + direction: Vector3, + }, +} + +#[derive(Debug)] +pub struct Light { + pub kind: LightKind, + pub color: Vector3, +} + +#[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 materials: Vec, + pub lights: Vec, + pub objects: Vec, +} + +#[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 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, + 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; + + let diffuse_and_specular: Vector3 = self + .lights + .iter() + .map(|light| { + // The vector pointing in the direction of the light + let light_direction = match light.kind { + LightKind::Point { location } => { + location - intersection_context.point + } + LightKind::Directional { direction } => direction, + } + .normalize(); + + 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 + * normal.dot(&light_direction).max(0.0); + + let specular_component = material.k_s + * material.specular_color + * normal + .dot(&halfway_direction) + .max(0.0) + .powf(material.exponent); + + diffuse_component + specular_component + }) + .sum(); + + ambient_component + diffuse_and_specular + } + + /// Determine the boundaries of the viewing window in world coordinates + pub fn compute_viewing_window(&self, distance: f64) -> Rect { + // Compute viewing directions + let u = self.view_dir.cross(&self.up_dir).normalize(); + let v = u.cross(&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] // Otherwise 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 + } +} diff --git a/assignment-1b/src/scene/mod.rs b/assignment-1b/src/scene/mod.rs new file mode 100644 index 000000000..e0465c9 --- /dev/null +++ b/assignment-1b/src/scene/mod.rs @@ -0,0 +1,3 @@ +pub mod data; +pub mod sphere; +pub mod cylinder; diff --git a/assignment-1b/src/scene/sphere.rs b/assignment-1b/src/scene/sphere.rs new file mode 100644 index 000000000..3571322 --- /dev/null +++ b/assignment-1b/src/scene/sphere.rs @@ -0,0 +1,76 @@ +use anyhow::Result; +use nalgebra::Vector3; +use ordered_float::NotNan; + +use crate::{ray::Ray, utils::min_f64}; + +use super::data::{IntersectionContext, ObjectKind}; + +#[derive(Debug)] +pub struct Sphere { + pub center: Vector3, + pub radius: f64, +} + +impl ObjectKind for Sphere { + /// Given a sphere, returns the first time at which this ray intersects the + /// sphere. + /// + /// If there is no intersection point, returns None. + fn intersects_ray_at( + &self, + ray: &Ray, + ) -> Result> { + let a = ray.direction.x.powi(2) + + ray.direction.y.powi(2) + + ray.direction.z.powi(2); + 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-1b/src/utils.rs b/assignment-1b/src/utils.rs new file mode 100644 index 000000000..10b1680 --- /dev/null +++ b/assignment-1b/src/utils.rs @@ -0,0 +1,71 @@ +use anyhow::Result; +use nalgebra::{Matrix3, Vector3}; +use ordered_float::NotNan; + +/// Finds the minimum of an iterator of f64s, ignoring any NaN values +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 +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()) +} + +/// Calculate the rotation matrix between the 2 given vectors +/// Based on the method here: 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 = a.dot(&b); + let sin_t = a.cross(&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 - a.dot(&b) * a).normalize(); + let w = b.cross(&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, 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-1b/writeup.md b/assignment-1b/writeup.md new file mode 100644 index 000000000..df8bac6 --- /dev/null +++ b/assignment-1b/writeup.md @@ -0,0 +1,25 @@ +--- +geometry: margin=2cm +output: pdf_document +--- + +## Varying $k_a$ + +![Varying $k_a$](examples/ka-demo.png){width=240px} + +## Varying $k_d$ + +![Varying $k_d$](examples/kd-demo.png){width=240px} + +## Varying $k_s$ + +![Varying $k_s$](examples/ks-demo.png){width=240px} + +## Varying $n$ + +![Varying $n$](examples/n-demo.png){width=240px} + +# Arbitrary Objects + +![Varying $n$](examples/objects.png){width=240px} +