Implement phong illumination

This commit is contained in:
Michael Zhang 2023-02-05 21:52:42 -06:00
parent cf1a540808
commit e34345296e
24 changed files with 1795 additions and 0 deletions

6
assignment-1b/.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
/target
/assignment-1
/examples/*.png
*.ppm
*.zip
*.pdf

View file

649
assignment-1b/Cargo.lock generated Normal file
View file

@ -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"

14
assignment-1b/Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[package]
name = "assignment-1b"
authors = ["Michael Zhang <zhan4854@umn.edu>"]
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"

45
assignment-1b/Makefile Normal file
View file

@ -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)

25
assignment-1b/README.md Normal file
View file

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

BIN
assignment-1b/assignment-1b Executable file

Binary file not shown.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<f64>;
/// 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<Color>,
}
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(())
}
}

View file

@ -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<Path>) -> Result<Scene> {
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::<usize>().map_err(|e| e.into()))
.collect::<Result<Vec<_>>>()?;
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::<f64>().map_err(|e| e.into()))
.collect::<Result<Vec<_>>>()?;
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)
}

153
assignment-1b/src/main.rs Normal file
View file

@ -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<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();
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::<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((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::<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-1b/src/ray.rs Normal file
View file

@ -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<f64>,
pub direction: Vector3<f64>,
}
impl Ray {
/// Construct a ray from endpoints
pub fn from_endpoints(start: Vector3<f64>, end: Vector3<f64>) -> 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<f64> {
self.origin + self.direction * time
}
}

View file

@ -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<f64>,
pub direction: Vector3<f64>,
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<Option<IntersectionContext>> {
// 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))
}
}

View file

@ -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<Option<IntersectionContext>>;
}
/// An object in the scene
#[derive(Debug)]
pub struct Object {
pub kind: Box<dyn ObjectKind>,
/// Index into the scene's material color list
pub material: usize,
}
#[derive(Debug)]
pub struct Rect {
pub upper_left: Vector3<f64>,
pub upper_right: Vector3<f64>,
pub lower_left: Vector3<f64>,
pub lower_right: Vector3<f64>,
}
#[derive(Debug)]
pub struct Material {
pub diffuse_color: Vector3<f64>,
pub specular_color: Vector3<f64>,
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<f64>,
},
Directional {
direction: Vector3<f64>,
},
}
#[derive(Debug)]
pub struct Light {
pub kind: LightKind,
pub color: Vector3<f64>,
}
#[derive(Debug, Default)]
pub struct Scene {
pub eye_pos: Vector3<f64>,
pub view_dir: Vector3<f64>,
pub up_dir: Vector3<f64>,
/// 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<Material>,
pub lights: Vec<Light>,
pub objects: Vec<Object>,
}
#[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: Vector3<f64>,
/// The normal vector protruding from the surface of the object at the
/// intersection point
#[derivative(PartialEq = "ignore", Ord = "ignore")]
pub normal: Vector3<f64>,
}
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<f64> = 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
}
}

View file

@ -0,0 +1,3 @@
pub mod data;
pub mod sphere;
pub mod cylinder;

View file

@ -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<f64>,
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<Option<IntersectionContext>> {
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,
}))
}
}

View file

@ -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: 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
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())
}
/// 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<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 = 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)
}

25
assignment-1b/writeup.md Normal file
View file

@ -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}