Compare commits
341 Commits
2b1112d39e
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| f93d215fc2 | |||
| 593632a9e3 | |||
| ed2ee749cd | |||
| 42f3daefaa | |||
| 9430a1e7da | |||
| d3153032b1 | |||
| 35071b06ac | |||
| 5f0e7a26dd | |||
| 6fbdb49ce1 | |||
| 23bc5b0bf0 | |||
| 37137ac9ca | |||
| 9353ff675e | |||
| 4b8bd84a84 | |||
| e19ec20c7b | |||
| deb46acb5a | |||
| 1076e6dcaf | |||
| 7d9750b9d0 | |||
| 88b8c547e0 | |||
| ac555beafc | |||
| 0158f9ea15 | |||
| 450342c3d4 | |||
| 41f9fa2742 | |||
| 7ec30d8557 | |||
| f51d3396f4 | |||
| a2f9166b5a | |||
| f73a471cb6 | |||
| cd149755cb | |||
| 9188ce17fa | |||
| 63975bad96 | |||
| df928e1779 | |||
| 3c28466d68 | |||
| a0b79ee2fa | |||
| 4e62975d56 | |||
| eea5c7c61e | |||
| 188b550fb7 | |||
| f7c5f29e67 | |||
| 6ab3021403 | |||
| d213e04c11 | |||
| 739b38b4ed | |||
| 5ba5aa5f5d | |||
| 4e6e9bf78a | |||
| beeb5e479b | |||
| fc1bfa419e | |||
| 95827a4a52 | |||
| d3dd002883 | |||
| ef737c6df9 | |||
| 2c490b7e83 | |||
| 63f8fba6a4 | |||
| 5c2786a54d | |||
| 2d696932e3 | |||
| 27d6c1280b | |||
| 4506418706 | |||
| 1d8aff7905 | |||
| 585ad4805c | |||
| b7f163c5a9 | |||
| 4ab9425a97 | |||
| 468cba97b3 | |||
| b9ebc186fa | |||
| 3e9d900f1e | |||
| 9e81acfda9 | |||
| f8ec874d13 | |||
| a8756debb8 | |||
| a0fb4637b5 | |||
| 6069bf9a65 | |||
| eeb7813243 | |||
| c644299726 | |||
| e6db61543b | |||
| 39eeb79409 | |||
| 54e72cd81d | |||
| 2d91f781f3 | |||
| 24e8b4f9cf | |||
| a12938db95 | |||
| 4066bf4b85 | |||
| b432e9a6dd | |||
| 62317d57ae | |||
| 8d92cc861e | |||
| f2ade1eee2 | |||
| a4baedefec | |||
| 8adf1bcadb | |||
| aea437785a | |||
| 91fd65259c | |||
| e5ffe87192 | |||
| ac73d13fb0 | |||
| 6b4be0ed1e | |||
| 85b87a6854 | |||
| d15a9e6c3e | |||
| a2012e6742 | |||
| 6d7998ad9f | |||
| 58646e4142 | |||
| 94b0f8355e | |||
| f4d3129d5a | |||
| e31f5e0a3a | |||
| 84a0ba2ec6 | |||
| 5043a7e526 | |||
| 622c23d5ed | |||
| f2c68e0b6f | |||
| 287344c272 | |||
| 8bc5e347cc | |||
| f0da916a22 | |||
| 386daf5876 | |||
| a4adefdb23 | |||
| f3aace486b | |||
| 93bfeb9125 | |||
| 55af087d69 | |||
| 78f7ca8956 | |||
| 51185e9e84 | |||
| 1ca903c64b | |||
| 5e7139f0ba | |||
| 665ae244d7 | |||
| e574cdb592 | |||
| 270a7ec349 | |||
| de6cd0da4d | |||
| 7a80179f41 | |||
| 926fffa29f | |||
| 9befbd9ad2 | |||
| c882fc81e5 | |||
| 1c2caf2cc5 | |||
| 9006671a26 | |||
| 3838efd134 | |||
| e3d8988658 | |||
| e4846de25b | |||
| 4352c03d20 | |||
| 1d5e5a164b | |||
| f476822bcd | |||
| 135a519526 | |||
| 5d6b3e6d57 | |||
| 3aea76b35c | |||
| cd2a4770ca | |||
| 5debb16d10 | |||
| 42e8ebe3bd | |||
| 1d61f59935 | |||
| 7f36aecf5e | |||
| 0c7bbae4a3 | |||
| eaae65712b | |||
| 68709da6c2 | |||
| 77215193fa | |||
| 74fe69188a | |||
| bdcee49d5a | |||
| 2e4e8b3dcd | |||
| b9f2c3f0ec | |||
| 8b79876aee | |||
| bfa3282a37 | |||
| 3e383c4dbd | |||
| c158d92252 | |||
| 62ad827507 | |||
| 363f15fb00 | |||
| be2041285c | |||
| 958c4c3ee8 | |||
| 62cb5e4ec4 | |||
| 4680c97adc | |||
| 0965ac9ddf | |||
| 4936839723 | |||
| c500d56d4d | |||
| c058d043e0 | |||
| 166c87dfe5 | |||
| 9389fed84c | |||
| 28fe6fe982 | |||
| 44b46187a0 | |||
| de898f0b0a | |||
| e041fd1f6a | |||
| 952ed8bf81 | |||
| a553786807 | |||
| b42adcebfc | |||
| d999e8196b | |||
| fc5ef09cc3 | |||
| e419994fae | |||
| ee8ef4e2c5 | |||
| 41f3b63ad0 | |||
| b2dc0e9509 | |||
| 2d8a3927f4 | |||
| 5600d6c561 | |||
| 0e8a0e4163 | |||
| 2f85697b88 | |||
| c0e422a7eb | |||
| 7741766635 | |||
| 3799f93393 | |||
| 1629b2cbfa | |||
| 839642b886 | |||
| c4f10126e3 | |||
| 7a8ed15017 | |||
| 5d8024a485 | |||
| ecf7cd7bdc | |||
| 1c06833658 | |||
| 95de5863cc | |||
| 5d57304d95 | |||
| 599f484dff | |||
| ce998c68eb | |||
| 37048ddd52 | |||
| 19b2ef6ded | |||
| 7e450d664e | |||
| 09047eb713 | |||
| 1065702a5d | |||
| efdc963a48 | |||
| 02ef8634c3 | |||
| 7e19d7e61b | |||
| dbf5451070 | |||
| 4f88d2c101 | |||
| 94ea724344 | |||
| 967920e1fa | |||
| 2eeeb2013b | |||
| 538b8ad364 | |||
| 2395c96e01 | |||
| 5f3bfd744e | |||
| e752536430 | |||
| 059f710706 | |||
| ad02d7e945 | |||
| 5911610064 | |||
| 39f7f77b74 | |||
| 125c96c25f | |||
| 249a2915d9 | |||
| 9f00485256 | |||
| b37398ac40 | |||
| 86d052d38b | |||
| b3737dcd5f | |||
| 72c6944ab9 | |||
| 9924330f98 | |||
| 2316df896a | |||
| 2c79132ebc | |||
| e9f2ef0118 | |||
| eebdc270eb | |||
| 3bc9f6f924 | |||
| cbecaa70ef | |||
| 6863b4ecd6 | |||
| bad54bb433 | |||
| d8e5476806 | |||
| 385ed70d88 | |||
| 1cbfbc8641 | |||
| 81540cd484 | |||
| b4428f924c | |||
| bcf847660a | |||
| e529710d5d | |||
| ac4f5eb9a6 | |||
| f846da18ad | |||
| e59029a94a | |||
| 339ce84903 | |||
| 6e7bd1c136 | |||
| e430e3769e | |||
| 7609201c16 | |||
| 4bb6a72e4b | |||
| cb1b3ec801 | |||
| ad7b10322f | |||
| 6e73bab37f | |||
| 87bf924094 | |||
| 9f22d820e7 | |||
| 324a26212a | |||
| c9b42d94b3 | |||
| 0ce1e8f7af | |||
| 8b451a2395 | |||
| da98744288 | |||
| f44d671573 | |||
| 191760fa13 | |||
| 12c2382327 | |||
| 5df2917668 | |||
| af5e61136c | |||
| 4b0d882b84 | |||
| 83799a02a9 | |||
| b8df830460 | |||
| 245b02b443 | |||
| f792d1a626 | |||
| 117d7185e4 | |||
| 462c90e8c8 | |||
| ac3a18a864 | |||
| 656f1c3a94 | |||
| 762cd45f63 | |||
| d6ad12e344 | |||
| f5d79908f6 | |||
| a69e404817 | |||
| 43d95041af | |||
| c97bc25323 | |||
| dda29eb836 | |||
| fa5971faa4 | |||
| 4d649c735b | |||
| 3d2d763a3b | |||
| ea6114b9ae | |||
| ec0331b88b | |||
| 72b15e5516 | |||
| 3cf580f607 | |||
| 78a360ae89 | |||
| f24a90b77b | |||
| c9ec19c3cd | |||
| 709465dafe | |||
| 7786aa99a1 | |||
| 536b6bed1f | |||
| d8f91a823e | |||
| 42455d593e | |||
| 21ac03acfb | |||
| df495feb57 | |||
| 21afbf8e7c | |||
| 1ea90770bc | |||
| 758f94acde | |||
| b159820bad | |||
| 3952a8ba83 | |||
| 495c64249c | |||
| a30a5a383c | |||
| 33a126f4d7 | |||
| 4d5056428b | |||
| 4cddc8571f | |||
| ea30bc9ed4 | |||
| 3fb564ff19 | |||
| d1a04b9b0c | |||
| 3e5a71440e | |||
| bf8b533b15 | |||
| 9dcb36612d | |||
| ae3b173f3f | |||
| a227d54705 | |||
| 0249ac6db9 | |||
| f6e8477107 | |||
| 2e13fa317e | |||
| 7ad3e82309 | |||
| 53398a57b7 | |||
| 1ea4ec669a | |||
| ea40835125 | |||
| d9d183b1e5 | |||
| 2541b76ae6 | |||
| c21acb49fe | |||
| f965a00e0c | |||
| 6bd29a2152 | |||
| 5841ab61e8 | |||
| 56743b5d77 | |||
| bda42922e4 | |||
| 96e74b3ebf | |||
| ea31b570db | |||
|
|
62ea19f6c7 | ||
| c903a743b5 | |||
| 848e9879cb | |||
| 27ca936264 | |||
| af6cda7349 | |||
| 12da8b2d16 | |||
| 5d9e180817 | |||
| f0f90a6b80 | |||
| fd7b9fd1b8 | |||
| e8be4d2e0d | |||
| 690048cbef | |||
| fcc22b24cd | |||
| 90c4e15ad1 | |||
| 38317de40d | |||
| 92c8f1980c | |||
| 5d5f3c7244 | |||
| 051482e7fe | |||
| 7b5571344e | |||
|
|
d796896f26 |
18
.drone.yml
18
.drone.yml
@@ -5,30 +5,34 @@ steps:
|
||||
- name: debug
|
||||
image: registry.z.xinu.tv/drone/omnibus
|
||||
commands:
|
||||
- cargo version
|
||||
- rustc -V
|
||||
- env | sort
|
||||
- find $PWD
|
||||
|
||||
- name: rtchallenge
|
||||
image: registry.z.xinu.tv/drone/omnibus
|
||||
commands:
|
||||
- (cd rtchallenge && cargo build --examples)
|
||||
- (cd rtchallenge && cargo test)
|
||||
- (cd rtchallenge && cargo build --examples --no-default-features)
|
||||
- (cd rtchallenge && cargo test --no-default-features)
|
||||
|
||||
- name: rtiow
|
||||
image: registry.z.xinu.tv/drone/omnibus
|
||||
commands:
|
||||
- sccache -s
|
||||
- apt-get update && apt-get install -y libgoogle-perftools-dev
|
||||
- (cd rtiow && cargo build --verbose --all)
|
||||
- (cd rtiow && ./build_all_features.sh)
|
||||
- (cd rtiow && cargo test --verbose --all)
|
||||
- sccache -s
|
||||
|
||||
- name: aobench
|
||||
image: registry.z.xinu.tv/drone/omnibus
|
||||
commands:
|
||||
- sccache -s
|
||||
- (cd aobench && cargo build --verbose --all)
|
||||
- (cd aobench && cargo test --verbose --all)
|
||||
- sccache -s
|
||||
|
||||
- name: bheisler
|
||||
image: registry.z.xinu.tv/drone/omnibus
|
||||
commands:
|
||||
- sccache -s
|
||||
- (cd bheisler && cargo build --verbose --all)
|
||||
- (cd bheisler && cargo test --verbose --all)
|
||||
- sccache -s
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,4 @@
|
||||
**/target
|
||||
**/*.rs.bk
|
||||
**/zig-out
|
||||
**/zig-cache
|
||||
|
||||
@@ -5,3 +5,5 @@ Raytracers
|
||||
|
||||
Random collection of ray tracing experiments.
|
||||
|
||||
# TODO
|
||||
Implement http://www.kevinbeason.com/smallpt/
|
||||
|
||||
292
aobench/Cargo.lock
generated
292
aobench/Cargo.lock
generated
@@ -1,63 +1,72 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
[[package]]
|
||||
name = "ansi_term"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aobench"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"log 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"stderrlog 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"structopt 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.4.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"stderrlog 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"structopt 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.10"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.97 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"termion 1.5.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.0.3"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "0.1.4"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.4"
|
||||
version = "0.4.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-traits 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.97 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-integer 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-traits 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"time 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "2.32.0"
|
||||
version = "2.33.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"atty 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"textwrap 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-width 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"vec_map 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -65,57 +74,65 @@ name = "cloudabi"
|
||||
version = "0.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fuchsia-zircon"
|
||||
name = "fuchsia-cprng"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-segmentation 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fuchsia-zircon-sys"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.0.2"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.42"
|
||||
version = "0.2.97"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.3"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"cfg-if 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"cfg-if 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.39"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"num-traits 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"autocfg 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-traits 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.5"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"autocfg 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numtoa"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "0.4.9"
|
||||
version = "0.4.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -123,135 +140,154 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "0.6.3"
|
||||
version = "0.6.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"proc-macro2 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.5.4"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand_core 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.97 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.2.1"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.1.40"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_termios"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"redox_syscall 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stderrlog"
|
||||
version = "0.4.1"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"chrono 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"termcolor 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"chrono 0.4.19 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.4.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"termcolor 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"thread_local 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "structopt"
|
||||
version = "0.2.10"
|
||||
version = "0.2.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"structopt-derive 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"clap 2.33.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"structopt-derive 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "structopt-derive"
|
||||
version = "0.2.10"
|
||||
version = "0.2.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"proc-macro2 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"quote 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"syn 0.14.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"heck 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "0.14.4"
|
||||
version = "0.15.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"proc-macro2 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"quote 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "0.3.6"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"wincolor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi-util 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termion"
|
||||
version = "1.5.1"
|
||||
version = "1.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.97 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"redox_syscall 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"redox_termios 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.10.0"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-width 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "0.3.5"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"lazy_static 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.1.40"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.97 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"wasi 0.10.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.5"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
@@ -269,7 +305,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vec_map"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
@@ -277,9 +313,14 @@ name = "void"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.10.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.5"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -291,56 +332,61 @@ name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "wincolor"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[metadata]
|
||||
"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
|
||||
"checksum atty 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "2fc4a1aa4c24c0718a250f0681885c1af91419d242f29eb8f2ab28502d80dbd1"
|
||||
"checksum bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d0c54bb8f454c567f21197eefcdbf5679d0bd99f2ddbe52e84c77061952e6789"
|
||||
"checksum cfg-if 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "efe5c877e17a9c717a0bf3613b2709f723202c4e4675cc8f12926ded29bcb17e"
|
||||
"checksum chrono 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "6962c635d530328acc53ac6a955e83093fedc91c5809dfac1fa60fa470830a37"
|
||||
"checksum clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b957d88f4b6a63b9d70d5f454ac8011819c6efa7727858f458ab71c756ce2d3e"
|
||||
"checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652"
|
||||
"checksum autocfg 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
|
||||
"checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
|
||||
"checksum cfg-if 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
"checksum chrono 0.4.19 (registry+https://github.com/rust-lang/crates.io-index)" = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
|
||||
"checksum clap 2.33.3 (registry+https://github.com/rust-lang/crates.io-index)" = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
|
||||
"checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
|
||||
"checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
|
||||
"checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
|
||||
"checksum lazy_static 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "fb497c35d362b6a331cfd94956a07fc2c78a4604cdbee844a81170386b996dd3"
|
||||
"checksum libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)" = "b685088df2b950fccadf07a7187c8ef846a959c142338a48f9dc0b94517eb5f1"
|
||||
"checksum log 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "61bd98ae7f7b754bc53dca7d44b604f733c6bba044ea6f41bc8d89272d8161d2"
|
||||
"checksum num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "e83d528d2677f0518c570baf2b7abdcf0cd2d248860b68507bdcb3e91d4c0cea"
|
||||
"checksum num-traits 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "630de1ef5cc79d0cdd78b7e33b81f083cbfe90de0f4b2b2f07f905867c70e9fe"
|
||||
"checksum proc-macro2 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)" = "cccdc7557a98fe98453030f077df7f3a042052fae465bb61d2c2c41435cfd9b6"
|
||||
"checksum quote 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e44651a0dc4cdd99f71c83b561e221f714912d11af1a4dff0631f923d53af035"
|
||||
"checksum rand 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)" = "12397506224b2f93e6664ffc4f664b29be8208e5157d3d90b44f09b5fae470ea"
|
||||
"checksum rand_core 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "edecf0f94da5551fc9b492093e30b041a891657db7940ee221f9d2f66e82eef2"
|
||||
"checksum redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "c214e91d3ecf43e9a4e41e578973adeb14b474f2bee858742d127af75a0112b1"
|
||||
"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"
|
||||
"checksum stderrlog 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "61dc66b7ae72b65636dbf36326f9638fb3ba27871bb737a62e2c309b87d91b70"
|
||||
"checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550"
|
||||
"checksum structopt 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d8e9ad6a11096cbecdcca0cc6aa403fdfdbaeda2fb3323a39c98e6a166a1e45a"
|
||||
"checksum structopt-derive 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4cbce8ccdc62166bd594c14396a3242bf94c337a51dbfa9be1076dd74b3db2af"
|
||||
"checksum syn 0.14.4 (registry+https://github.com/rust-lang/crates.io-index)" = "2beff8ebc3658f07512a413866875adddd20f4fd47b2a4e6c9da65cd281baaea"
|
||||
"checksum termcolor 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "adc4587ead41bf016f11af03e55a624c06568b5a19db4e90fde573d805074f83"
|
||||
"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096"
|
||||
"checksum textwrap 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "307686869c93e71f94da64286f9a9524c0f308a9e1c87a583de8e9c9039ad3f6"
|
||||
"checksum thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "279ef31c19ededf577bfd12dfae728040a21f635b06a24cd670ff510edd38963"
|
||||
"checksum time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "d825be0eb33fda1a7e68012d51e9c7f451dc1a69391e7fdc197060bb8c56667b"
|
||||
"checksum unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526"
|
||||
"checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
|
||||
"checksum heck 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
|
||||
"checksum lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73"
|
||||
"checksum libc 0.2.97 (registry+https://github.com/rust-lang/crates.io-index)" = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6"
|
||||
"checksum log 0.4.14 (registry+https://github.com/rust-lang/crates.io-index)" = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
|
||||
"checksum num-integer 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)" = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
|
||||
"checksum num-traits 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
|
||||
"checksum numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
|
||||
"checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759"
|
||||
"checksum quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1"
|
||||
"checksum rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c618c47cd3ebd209790115ab837de41425723956ad3ce2e6a7f09890947cacb9"
|
||||
"checksum rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
|
||||
"checksum rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
|
||||
"checksum redox_syscall 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc"
|
||||
"checksum redox_termios 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f"
|
||||
"checksum stderrlog 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "32e5ee9b90a5452c570a0b0ac1c99ae9498db7e56e33d74366de7f2a7add7f25"
|
||||
"checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
|
||||
"checksum structopt 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "16c2cdbf9cc375f15d1b4141bc48aeef444806655cd0e904207edc8d68d86ed7"
|
||||
"checksum structopt-derive 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "53010261a84b37689f9ed7d395165029f9cc7abb9f56bbfe86bee2597ed25107"
|
||||
"checksum syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)" = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5"
|
||||
"checksum termcolor 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
|
||||
"checksum termion 1.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e"
|
||||
"checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
|
||||
"checksum thread_local 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "1697c4b57aeeb7a536b647165a2825faddffb1d3bad386d507709bd51a90bb14"
|
||||
"checksum time 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)" = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
|
||||
"checksum unicode-segmentation 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796"
|
||||
"checksum unicode-width 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
|
||||
"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
|
||||
"checksum unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56"
|
||||
"checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a"
|
||||
"checksum vec_map 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
|
||||
"checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
|
||||
"checksum winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "773ef9dcc5f24b7d850d0ff101e542ff24c3b090a9768e03ff889fdef41f00fd"
|
||||
"checksum wasi 0.10.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
|
||||
"checksum winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
"checksum winapi-util 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
|
||||
"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
"checksum wincolor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "eeb06499a3a4d44302791052df005d5232b927ed1a9658146d842165c4de7767"
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
name = "aobench"
|
||||
version = "0.1.0"
|
||||
authors = ["Bill Thiede <rust@xinu.tv>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4"
|
||||
rand = "0.5"
|
||||
stderrlog = "0.4.1"
|
||||
structopt = "0.2.10"
|
||||
rand = "0.5"
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate rand;
|
||||
extern crate stderrlog;
|
||||
#[macro_use]
|
||||
extern crate structopt;
|
||||
|
||||
use rand::Rng;
|
||||
use std::clone::Clone;
|
||||
use std::f64;
|
||||
use std::fmt;
|
||||
@@ -14,6 +6,9 @@ use std::io::Write;
|
||||
use std::ops::Add;
|
||||
use std::ops::Mul;
|
||||
use std::ops::Sub;
|
||||
|
||||
use log::info;
|
||||
use rand::Rng;
|
||||
use structopt::StructOpt;
|
||||
|
||||
fn vdot(v0: &Vector, v1: &Vector) -> f64 {
|
||||
|
||||
2
bheisler/Cargo.lock
generated
2
bheisler/Cargo.lock
generated
@@ -1,3 +1,5 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
[[package]]
|
||||
name = "adler32"
|
||||
version = "1.0.3"
|
||||
|
||||
8
git-hooks/README.md
Normal file
8
git-hooks/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Installing
|
||||
Symlink the files in this directory into your `.git/hooks/` directory to
|
||||
enable policies for this project. Example:
|
||||
|
||||
```bash
|
||||
$ cd .git/hooks
|
||||
$ ln -s ../../git-hooks/pre-commit .
|
||||
```
|
||||
33
git-hooks/pre-commit
Executable file
33
git-hooks/pre-commit
Executable file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||
|
||||
pushd () {
|
||||
command pushd "$@" > /dev/null
|
||||
}
|
||||
|
||||
popd () {
|
||||
command popd "$@" > /dev/null
|
||||
}
|
||||
|
||||
for workspace in rtchallenge rtiow; do
|
||||
pushd ${workspace:?}
|
||||
cargo fmt -- --check || (echo "To fix, run: cd ${workspace:?} && cargo fmt" && false)
|
||||
popd
|
||||
done
|
||||
|
||||
RT=rtchallenge
|
||||
AFFECTED="$(git diff-index --cached --name-only HEAD)"
|
||||
#echo "AFFECTED $AFFECTED"
|
||||
RT_AFFECTED=$(echo "${AFFECTED:?}" | grep ${RT:?} || true)
|
||||
NO_FEATURES_TARGET_DIR="${SCRIPT_DIR:?}/../../rtchallenge/target/no-default-features"
|
||||
if [ ! -z "$RT_AFFECTED" ]; then
|
||||
echo "Files for the rt challenge were touched, running tests"
|
||||
cd ${RT:?} && \
|
||||
cargo build --examples && \
|
||||
cargo test && \
|
||||
cargo build --target-dir=${NO_FEATURES_TARGET_DIR:?} --no-default-features --examples && \
|
||||
cargo test --target-dir=${NO_FEATURES_TARGET_DIR:?} --no-default-features
|
||||
fi
|
||||
# Uncomment to debug presubmit.
|
||||
#exit 1
|
||||
1087
rtchallenge/Cargo.lock
generated
Normal file
1087
rtchallenge/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
rtchallenge/Cargo.toml
Normal file
34
rtchallenge/Cargo.toml
Normal file
@@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "rtchallenge"
|
||||
version = "0.1.0"
|
||||
authors = ["Bill Thiede <git@xinu.tv>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
default = [ "float-as-double" ]
|
||||
float-as-double = []
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.41"
|
||||
core_affinity = "0.5.10"
|
||||
criterion = "0.3.4"
|
||||
derive_builder = "0.10.2"
|
||||
enum-utils = "0.1.2"
|
||||
num_cpus = "1.13.0"
|
||||
png = "0.16.8"
|
||||
rand = "0.8.4"
|
||||
rayon = "1.5.1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0.64"
|
||||
structopt = "0.3.22"
|
||||
thiserror = "1.0.25"
|
||||
|
||||
|
||||
[[bench]]
|
||||
name = "matrices"
|
||||
harness = false
|
||||
|
||||
[profile.release]
|
||||
debug = true
|
||||
4
rtchallenge/README.md
Normal file
4
rtchallenge/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# The Ray Tracer Challenge: A Test-Driven Guide to Your First 3D Renderer
|
||||
This is a rust implementation of https://pragprog.com/titles/jbtracer/the-ray-tracer-challenge/
|
||||
|
||||
It attempts to be a test-driven developed ray tracer.
|
||||
13
rtchallenge/benches/matrices.rs
Normal file
13
rtchallenge/benches/matrices.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
|
||||
use rtchallenge::matrices::Matrix4x4;
|
||||
|
||||
fn criterion_benchmark(c: &mut Criterion) {
|
||||
let mut i = Matrix4x4::identity();
|
||||
c.bench_function("inverse_new", |b| b.iter(|| i = i.inverse()));
|
||||
let mut i = Matrix4x4::identity();
|
||||
c.bench_function("inverse_old", |b| b.iter(|| i = i.inverse_old()));
|
||||
}
|
||||
|
||||
criterion_group!(benches, criterion_benchmark);
|
||||
criterion_main!(benches);
|
||||
119
rtchallenge/examples/balls.rs
Normal file
119
rtchallenge/examples/balls.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::Result;
|
||||
use structopt::StructOpt;
|
||||
|
||||
use rtchallenge::{
|
||||
camera::{Camera, RenderStrategy},
|
||||
float::consts::PI,
|
||||
lights::PointLightBuilder,
|
||||
materials::MaterialBuilder,
|
||||
matrices::Matrix4x4,
|
||||
shapes::{Geometry, ShapeBuilder},
|
||||
transformations::view_transform,
|
||||
tuples::Tuple,
|
||||
world::WorldBuilder,
|
||||
Float, WHITE,
|
||||
};
|
||||
|
||||
/// Experimenting with balls.
|
||||
#[derive(StructOpt, Debug)]
|
||||
#[structopt(name = "balls")]
|
||||
struct Opt {
|
||||
#[structopt(long, default_value = "rayon")]
|
||||
render_strategy: RenderStrategy,
|
||||
/// Number of samples per pixel. 0 renders from the center of the pixel, 1 or more samples N
|
||||
/// times randomly across the pixel.
|
||||
#[structopt(short, long, default_value = "0")]
|
||||
samples: usize,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let start = Instant::now();
|
||||
let opt = Opt::from_args();
|
||||
let width = 2560;
|
||||
let height = 1440;
|
||||
|
||||
let light1 = PointLightBuilder::default()
|
||||
.position(Tuple::point(-5., 5., -5.))
|
||||
.intensity(WHITE)
|
||||
.build()?;
|
||||
let light2 = PointLightBuilder::default()
|
||||
.position(Tuple::point(5., 5., -5.))
|
||||
.intensity([0.2, 0.2, 0.6])
|
||||
.build()?;
|
||||
let light3 = PointLightBuilder::default()
|
||||
.position(Tuple::point(0., 2., -5.))
|
||||
.intensity([0.2, 0.2, 0.1])
|
||||
.build()?;
|
||||
|
||||
let ball_size = 0.5;
|
||||
let num_per_axis = 3;
|
||||
let center_to_center = 4. * ball_size;
|
||||
let from = Tuple::point(
|
||||
-5. * center_to_center,
|
||||
5. * center_to_center,
|
||||
-4. * center_to_center,
|
||||
);
|
||||
let to = Tuple::point(
|
||||
num_per_axis as Float * center_to_center / 2.,
|
||||
0., //num_per_axis as Float * center_to_center / 2.,
|
||||
num_per_axis as Float * center_to_center / 2.,
|
||||
);
|
||||
let up = Tuple::point(0., 1., 0.);
|
||||
let mut camera = Camera::new(width, height, PI / 6.);
|
||||
camera.set_transform(view_transform(from, to, up));
|
||||
camera.render_strategy = opt.render_strategy;
|
||||
camera.samples_per_pixel = opt.samples;
|
||||
|
||||
let floor = ShapeBuilder::default()
|
||||
.geometry(Geometry::Plane)
|
||||
.transform(Matrix4x4::translation(0., -ball_size, 0.))
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color([0.2, 0.2, 0.2])
|
||||
.specular(0.)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
let mut objects = vec![floor];
|
||||
|
||||
for z in 0..num_per_axis {
|
||||
for y in 0..num_per_axis {
|
||||
for x in 0..num_per_axis {
|
||||
objects.push(
|
||||
ShapeBuilder::default()
|
||||
.transform(
|
||||
Matrix4x4::translation(
|
||||
x as Float * center_to_center,
|
||||
y as Float * center_to_center,
|
||||
z as Float * center_to_center,
|
||||
) * Matrix4x4::scaling(ball_size, ball_size, ball_size),
|
||||
)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color([0.1, 1., 0.5])
|
||||
.ambient(y as Float / 4.)
|
||||
.diffuse(x as Float / 4.)
|
||||
.specular(z as Float / 4.)
|
||||
.build()?,
|
||||
)
|
||||
.build()?,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let world = WorldBuilder::default()
|
||||
.lights(vec![light1, light2, light3])
|
||||
.objects(objects)
|
||||
.build()?;
|
||||
|
||||
let image = camera.render(&world);
|
||||
|
||||
let path = "/tmp/balls.png";
|
||||
println!("saving output to {}", path);
|
||||
image.write_to_file(path)?;
|
||||
println!("Render time {:.3} seconds", start.elapsed().as_secs_f32());
|
||||
Ok(())
|
||||
}
|
||||
39
rtchallenge/examples/eoc1.rs
Normal file
39
rtchallenge/examples/eoc1.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use rtchallenge::tuples::Tuple;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Environment {
|
||||
gravity: Tuple,
|
||||
wind: Tuple,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
struct Projectile {
|
||||
position: Tuple,
|
||||
velocity: Tuple,
|
||||
}
|
||||
|
||||
fn tick(env: &Environment, proj: &Projectile) -> Projectile {
|
||||
let position = proj.position + proj.velocity;
|
||||
let velocity = proj.velocity + env.gravity + env.wind;
|
||||
Projectile { position, velocity }
|
||||
}
|
||||
fn main() {
|
||||
let mut p = Projectile {
|
||||
position: Tuple::point(0., 1., 0.),
|
||||
velocity: 4. * Tuple::vector(1., 1., 0.).normalize(),
|
||||
};
|
||||
let e = Environment {
|
||||
gravity: Tuple::vector(0., -0.1, 0.).normalize(),
|
||||
wind: Tuple::vector(-0.01, 0., 0.),
|
||||
};
|
||||
|
||||
let mut i = 0;
|
||||
while p.position.y > 0. {
|
||||
p = tick(&e, &p);
|
||||
println!("tick {}: position {:?}", i, p.position);
|
||||
i += 1;
|
||||
if i > 100 {
|
||||
eprintln!("too many iterations");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
171
rtchallenge/examples/eoc10.rs
Normal file
171
rtchallenge/examples/eoc10.rs
Normal file
@@ -0,0 +1,171 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::Result;
|
||||
use structopt::StructOpt;
|
||||
|
||||
use rtchallenge::prelude::*;
|
||||
|
||||
use rtchallenge::{
|
||||
camera::RenderStrategy,
|
||||
float::consts::PI,
|
||||
patterns::{test_pattern, BLACK_PAT, WHITE_PAT},
|
||||
WHITE,
|
||||
};
|
||||
|
||||
/// End of chapter 10 challenge.
|
||||
#[derive(StructOpt, Debug)]
|
||||
#[structopt(name = "eoc10")]
|
||||
struct Opt {
|
||||
/// Strategy for casting rays into image.
|
||||
#[structopt(long, default_value = "rayon")]
|
||||
render_strategy: RenderStrategy,
|
||||
/// Number of samples per pixel. 0 renders from the center of the pixel, 1 or more samples N
|
||||
/// times randomly across the pixel.
|
||||
#[structopt(short, long, default_value = "0")]
|
||||
samples: usize,
|
||||
/// Rendered image width in pixels.
|
||||
#[structopt(short, long, default_value = "2560")]
|
||||
width: usize,
|
||||
/// Rendered image height in pixels.
|
||||
#[structopt(short, long, default_value = "1440")]
|
||||
height: usize,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let start = Instant::now();
|
||||
let opt = Opt::from_args();
|
||||
|
||||
let light1 = PointLightBuilder::default()
|
||||
.position(point(-5., 5., -5.))
|
||||
.intensity(WHITE)
|
||||
.build()?;
|
||||
let light2 = PointLightBuilder::default()
|
||||
.position(point(5., 5., -5.))
|
||||
.intensity([0.2, 0.2, 0.6])
|
||||
.build()?;
|
||||
let light3 = PointLightBuilder::default()
|
||||
.position(point(0., 2., -5.))
|
||||
.intensity([0.2, 0.2, 0.1])
|
||||
.build()?;
|
||||
|
||||
let from = point(2., 2., -10.);
|
||||
let to = point(2., 1., 0.);
|
||||
let up = point(0., 1., 0.);
|
||||
let camera = CameraBuilder::default()
|
||||
.hsize(opt.width)
|
||||
.vsize(opt.height)
|
||||
.field_of_view(PI / 4.)
|
||||
.transform(view_transform(from, to, up))
|
||||
.render_strategy(opt.render_strategy)
|
||||
.samples_per_pixel(opt.samples)
|
||||
.build()?;
|
||||
|
||||
let floor = plane()
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color(
|
||||
checkers_pattern(WHITE_PAT, BLACK_PAT)
|
||||
.transform(translation(1., 0., 0.) * scaling(2., 2., 2.))
|
||||
.build()?,
|
||||
)
|
||||
.specular(0.)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
let sphere_size = scaling(0.5, 0.5, 0.5);
|
||||
|
||||
let x1y1 = sphere()
|
||||
.transform(translation(1., 1., 0.) * sphere_size)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color(
|
||||
gradient_pattern([0., 0., 1.].into(), [1., 1., 0.].into())
|
||||
.transform(scaling(2., 1., 1.) * translation(-0.5, 0., 0.))
|
||||
.build()?,
|
||||
)
|
||||
.diffuse(0.7)
|
||||
.specular(0.3)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let x2y1 = sphere()
|
||||
.transform(translation(2., 1., 0.) * sphere_size)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color(stripe_pattern(WHITE_PAT, BLACK_PAT).build()?)
|
||||
.diffuse(0.7)
|
||||
.specular(0.3)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let x3y1 = sphere()
|
||||
.transform(translation(3., 1., 0.) * sphere_size)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color(
|
||||
stripe_pattern(WHITE_PAT, BLACK_PAT)
|
||||
.transform(scaling(0.2, 1., 1.))
|
||||
.build()?,
|
||||
)
|
||||
.diffuse(0.7)
|
||||
.specular(0.0)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let x1y2 = sphere()
|
||||
.transform(translation(1., 2., 0.) * sphere_size)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color(test_pattern().build()?)
|
||||
.diffuse(0.7)
|
||||
.specular(0.3)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let x2y2 = sphere()
|
||||
.transform(translation(2., 2., 0.) * sphere_size)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color(
|
||||
ring_pattern(WHITE_PAT, BLACK_PAT)
|
||||
.transform(scaling(0.2, 0.2, 0.2))
|
||||
.build()?,
|
||||
)
|
||||
.diffuse(0.7)
|
||||
.specular(0.3)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let x3y2 = sphere()
|
||||
.transform(translation(3., 2., 0.) * sphere_size)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color(
|
||||
checkers_pattern(WHITE_PAT, BLACK_PAT)
|
||||
.transform(scaling(0.5, 0.5, 0.5))
|
||||
.build()?,
|
||||
)
|
||||
.diffuse(0.7)
|
||||
.specular(0.3)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let world = WorldBuilder::default()
|
||||
.lights(vec![light1, light2, light3])
|
||||
.objects(vec![floor, x1y1, x2y1, x3y1, x1y2, x2y2, x3y2])
|
||||
.build()?;
|
||||
|
||||
let image = camera.render(&world);
|
||||
|
||||
let path = "/tmp/eoc10.png";
|
||||
println!("saving output to {}", path);
|
||||
image.write_to_file(path)?;
|
||||
println!("Render time {:.3} seconds", start.elapsed().as_secs_f32());
|
||||
Ok(())
|
||||
}
|
||||
218
rtchallenge/examples/eoc11.rs
Normal file
218
rtchallenge/examples/eoc11.rs
Normal file
@@ -0,0 +1,218 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::Result;
|
||||
use structopt::StructOpt;
|
||||
|
||||
use rtchallenge::prelude::*;
|
||||
|
||||
use rtchallenge::{
|
||||
camera::RenderStrategy,
|
||||
float::consts::PI,
|
||||
patterns::{test_pattern, BLACK_PAT, WHITE_PAT},
|
||||
WHITE,
|
||||
};
|
||||
|
||||
/// End of chapter 11 challenge.
|
||||
#[derive(StructOpt, Debug)]
|
||||
#[structopt(name = "eoc11")]
|
||||
struct Opt {
|
||||
/// Strategy for casting rays into image.
|
||||
#[structopt(long, default_value = "rayon")]
|
||||
render_strategy: RenderStrategy,
|
||||
/// Number of samples per pixel. 0 renders from the center of the pixel, 1 or more samples N
|
||||
/// times randomly across the pixel.
|
||||
#[structopt(short, long, default_value = "0")]
|
||||
samples: usize,
|
||||
/// Rendered image width in pixels.
|
||||
#[structopt(short, long, default_value = "2560")]
|
||||
width: usize,
|
||||
/// Rendered image height in pixels.
|
||||
#[structopt(short, long, default_value = "1440")]
|
||||
height: usize,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let start = Instant::now();
|
||||
let opt = Opt::from_args();
|
||||
|
||||
let light1 = PointLightBuilder::default()
|
||||
.position(point(-5., 5., -5.))
|
||||
.intensity(WHITE)
|
||||
.build()?;
|
||||
let light2 = PointLightBuilder::default()
|
||||
.position(point(5., 5., -5.))
|
||||
.intensity([0.2, 0.2, 0.6])
|
||||
.build()?;
|
||||
let light3 = PointLightBuilder::default()
|
||||
.position(point(0., 2., -5.))
|
||||
.intensity([0.2, 0.2, 0.1])
|
||||
.build()?;
|
||||
|
||||
let from = point(2., 8., -10.);
|
||||
let to = point(2., 1., -1.);
|
||||
let up = point(0., 1., 0.);
|
||||
let camera = CameraBuilder::default()
|
||||
.hsize(opt.width)
|
||||
.vsize(opt.height)
|
||||
.field_of_view(PI / 6.)
|
||||
.transform(view_transform(from, to, up))
|
||||
.render_strategy(opt.render_strategy)
|
||||
.samples_per_pixel(opt.samples)
|
||||
.build()?;
|
||||
|
||||
let floor = plane()
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color(
|
||||
checkers_pattern(
|
||||
ring_pattern([0.8, 0.8, 0.2].into(), [0.8, 0.2, 0.8].into())
|
||||
.transform(scaling(1. / 8., 1. / 8., 1. / 8.))
|
||||
.build()?,
|
||||
ring_pattern([0.2, 0.8, 0.2].into(), [0.8, 0.2, 0.2].into())
|
||||
.transform(scaling(1. / 4., 1. / 4., 1. / 4.))
|
||||
.build()?,
|
||||
)
|
||||
.transform(translation(0., 1., 0.) * scaling(2., 2., 2.))
|
||||
.build()?,
|
||||
)
|
||||
.specular(0.)
|
||||
.reflective(0.5)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
let sphere_size = scaling(0.5, 0.5, 0.5);
|
||||
|
||||
let x1y1 = sphere()
|
||||
.transform(translation(1., 1., 0.) * sphere_size)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color(
|
||||
gradient_pattern([0., 0., 1.].into(), [1., 1., 0.].into())
|
||||
.transform(scaling(2., 1., 1.) * translation(-0.5, 0., 0.))
|
||||
.build()?,
|
||||
)
|
||||
.diffuse(0.7)
|
||||
.specular(0.3)
|
||||
.reflective(0.5)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let x2y1 = sphere()
|
||||
.transform(translation(2., 1., 0.) * sphere_size)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color(stripe_pattern(WHITE_PAT, BLACK_PAT).build()?)
|
||||
.diffuse(0.7)
|
||||
.specular(0.3)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let x3y1 = sphere()
|
||||
.transform(translation(3., 1., 0.) * sphere_size)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color(
|
||||
stripe_pattern(WHITE_PAT, BLACK_PAT)
|
||||
.transform(scaling(0.2, 1., 1.))
|
||||
.build()?,
|
||||
)
|
||||
.diffuse(0.7)
|
||||
.specular(0.0)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let x1y2 = sphere()
|
||||
.transform(translation(1., 2., 0.) * sphere_size)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color(test_pattern().build()?)
|
||||
.diffuse(0.7)
|
||||
.specular(0.3)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let x2y2 = sphere()
|
||||
.transform(translation(2., 2., 0.) * sphere_size)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color(
|
||||
ring_pattern(WHITE_PAT, BLACK_PAT)
|
||||
.transform(scaling(0.2, 0.2, 0.2))
|
||||
.build()?,
|
||||
)
|
||||
.diffuse(0.7)
|
||||
.specular(0.3)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let x3y2 = sphere()
|
||||
.transform(translation(3., 2., 0.) * sphere_size)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color(
|
||||
checkers_pattern(WHITE_PAT, BLACK_PAT)
|
||||
.transform(scaling(0.5, 0.5, 0.5))
|
||||
.build()?,
|
||||
)
|
||||
.diffuse(0.7)
|
||||
.specular(0.3)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let x1y2z1 = sphere()
|
||||
.transform(translation(1., 2., -1.) * sphere_size)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color([0.8, 0.2, 0.2])
|
||||
.diffuse(0.7)
|
||||
.specular(0.3)
|
||||
.transparency(0.5)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let x2y2z1 = sphere()
|
||||
.transform(translation(2., 2., -1.) * sphere_size)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color([0.8, 0.2, 0.2])
|
||||
.diffuse(0.7)
|
||||
.specular(0.3)
|
||||
.transparency(0.9)
|
||||
.refractive_index(1.5)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let x2y4z1 = sphere()
|
||||
.transform(translation(2., 4., 0.) * sphere_size)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color([0.2, 0.2, 0.8])
|
||||
.diffuse(0.7)
|
||||
.specular(0.3)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let world = WorldBuilder::default()
|
||||
.lights(vec![light1, light2, light3])
|
||||
.objects(vec![
|
||||
floor, x1y1, x2y1, x3y1, x1y2, x2y2, x3y2, x1y2z1, x2y2z1, x2y4z1,
|
||||
])
|
||||
.build()?;
|
||||
|
||||
let image = camera.render(&world);
|
||||
|
||||
let path = "/tmp/eoc11.png";
|
||||
println!("saving output to {}", path);
|
||||
image.write_to_file(path)?;
|
||||
println!("Render time {:.3} seconds", start.elapsed().as_secs_f32());
|
||||
Ok(())
|
||||
}
|
||||
232
rtchallenge/examples/eoc12.rs
Normal file
232
rtchallenge/examples/eoc12.rs
Normal file
@@ -0,0 +1,232 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::Result;
|
||||
use structopt::StructOpt;
|
||||
|
||||
use rtchallenge::prelude::*;
|
||||
|
||||
use rtchallenge::{
|
||||
camera::RenderStrategy,
|
||||
float::consts::PI,
|
||||
patterns::{test_pattern, BLACK_PAT, WHITE_PAT},
|
||||
WHITE,
|
||||
};
|
||||
|
||||
/// End of chapter 12 challenge.
|
||||
#[derive(StructOpt, Debug)]
|
||||
#[structopt(name = "eoc12")]
|
||||
struct Opt {
|
||||
/// Strategy for casting rays into image.
|
||||
#[structopt(long, default_value = "rayon")]
|
||||
render_strategy: RenderStrategy,
|
||||
/// Number of samples per pixel. 0 renders from the center of the pixel, 1 or more samples N
|
||||
/// times randomly across the pixel.
|
||||
#[structopt(short, long, default_value = "0")]
|
||||
samples: usize,
|
||||
/// Rendered image width in pixels.
|
||||
#[structopt(short, long, default_value = "2560")]
|
||||
width: usize,
|
||||
/// Rendered image height in pixels.
|
||||
#[structopt(short, long, default_value = "1440")]
|
||||
height: usize,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let start = Instant::now();
|
||||
let opt = Opt::from_args();
|
||||
|
||||
let light1 = PointLightBuilder::default()
|
||||
.position(point(-5., 5., -5.))
|
||||
.intensity(WHITE)
|
||||
.build()?;
|
||||
let light2 = PointLightBuilder::default()
|
||||
.position(point(5., 5., -5.))
|
||||
.intensity([0.2, 0.2, 0.6])
|
||||
.build()?;
|
||||
let light3 = PointLightBuilder::default()
|
||||
.position(point(0., 2., -5.))
|
||||
.intensity([0.2, 0.2, 0.1])
|
||||
.build()?;
|
||||
|
||||
let from = point(2., 8., -10.);
|
||||
let to = point(2., 1., -1.);
|
||||
let up = point(0., 1., 0.);
|
||||
let camera = CameraBuilder::default()
|
||||
.hsize(opt.width)
|
||||
.vsize(opt.height)
|
||||
.field_of_view(PI / 6.)
|
||||
.transform(view_transform(from, to, up))
|
||||
.render_strategy(opt.render_strategy)
|
||||
.samples_per_pixel(opt.samples)
|
||||
.build()?;
|
||||
|
||||
let floor = plane()
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color(
|
||||
checkers_pattern(
|
||||
ring_pattern([0.8, 0.8, 0.2].into(), [0.8, 0.2, 0.8].into())
|
||||
.transform(scaling(1. / 8., 1. / 8., 1. / 8.))
|
||||
.build()?,
|
||||
ring_pattern([0.2, 0.8, 0.2].into(), [0.8, 0.2, 0.2].into())
|
||||
.transform(scaling(1. / 4., 1. / 4., 1. / 4.))
|
||||
.build()?,
|
||||
)
|
||||
.transform(translation(0., 1., 0.) * scaling(2., 2., 2.))
|
||||
.build()?,
|
||||
)
|
||||
.specular(0.)
|
||||
.reflective(0.5)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
let sphere_size = scaling(0.5, 0.5, 0.5);
|
||||
|
||||
let x1y1 = sphere()
|
||||
.transform(translation(1., 1., 0.) * sphere_size)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color(
|
||||
gradient_pattern([0., 0., 1.].into(), [1., 1., 0.].into())
|
||||
.transform(scaling(2., 1., 1.) * translation(-0.5, 0., 0.))
|
||||
.build()?,
|
||||
)
|
||||
.diffuse(0.7)
|
||||
.specular(0.3)
|
||||
.reflective(0.5)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let x2y1 = sphere()
|
||||
.transform(translation(2., 1., 0.) * sphere_size)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color(stripe_pattern(WHITE_PAT, BLACK_PAT).build()?)
|
||||
.diffuse(0.7)
|
||||
.specular(0.3)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let x3y1 = sphere()
|
||||
.transform(translation(3., 1., 0.) * sphere_size)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color(
|
||||
stripe_pattern(WHITE_PAT, BLACK_PAT)
|
||||
.transform(scaling(0.2, 1., 1.))
|
||||
.build()?,
|
||||
)
|
||||
.diffuse(0.7)
|
||||
.specular(0.0)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let x1y2 = sphere()
|
||||
.transform(translation(1., 2., 0.) * sphere_size)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color(test_pattern().build()?)
|
||||
.diffuse(0.7)
|
||||
.specular(0.3)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let x2y2 = sphere()
|
||||
.transform(translation(2., 2., 0.) * sphere_size)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color(
|
||||
ring_pattern(WHITE_PAT, BLACK_PAT)
|
||||
.transform(scaling(0.2, 0.2, 0.2))
|
||||
.build()?,
|
||||
)
|
||||
.diffuse(0.7)
|
||||
.specular(0.3)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let x3y2 = sphere()
|
||||
.transform(translation(3., 2., 0.) * sphere_size)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color(
|
||||
checkers_pattern(WHITE_PAT, BLACK_PAT)
|
||||
.transform(scaling(0.5, 0.5, 0.5))
|
||||
.build()?,
|
||||
)
|
||||
.diffuse(0.7)
|
||||
.specular(0.3)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let x1y2z1 = sphere()
|
||||
.transform(translation(1., 2., -1.) * sphere_size)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color([0.8, 0.2, 0.2])
|
||||
.diffuse(0.7)
|
||||
.specular(0.3)
|
||||
.transparency(0.5)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let x2y2z1 = sphere()
|
||||
.transform(translation(2., 2., -1.) * sphere_size)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color([0.8, 0.2, 0.2])
|
||||
.diffuse(0.7)
|
||||
.specular(0.3)
|
||||
.transparency(0.9)
|
||||
.refractive_index(1.5)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let x3y2z1 = cube()
|
||||
.transform(
|
||||
translation(3., 2., -1.) * (sphere_size * scaling(0.5, 0.5, 0.5)) * rotation_y(PI / 4.),
|
||||
)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color([0.8, 0.8, 0.2])
|
||||
.diffuse(0.7)
|
||||
.specular(0.3)
|
||||
.reflective(0.5)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let x2y4z1 = sphere()
|
||||
.transform(translation(2., 4., 0.) * sphere_size)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color([0.2, 0.2, 0.8])
|
||||
.diffuse(0.7)
|
||||
.specular(0.3)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let world = WorldBuilder::default()
|
||||
.lights(vec![light1, light2, light3])
|
||||
.objects(vec![
|
||||
floor, x1y1, x2y1, x3y1, x1y2, x2y2, x3y2, x1y2z1, x2y2z1, x2y4z1, x3y2z1,
|
||||
])
|
||||
.build()?;
|
||||
|
||||
let image = camera.render(&world);
|
||||
|
||||
let path = "/tmp/eoc12.png";
|
||||
println!("saving output to {}", path);
|
||||
image.write_to_file(path)?;
|
||||
println!("Render time {:.3} seconds", start.elapsed().as_secs_f32());
|
||||
Ok(())
|
||||
}
|
||||
232
rtchallenge/examples/eoc14.rs
Normal file
232
rtchallenge/examples/eoc14.rs
Normal file
@@ -0,0 +1,232 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::Result;
|
||||
use structopt::StructOpt;
|
||||
|
||||
use rtchallenge::prelude::*;
|
||||
|
||||
use rtchallenge::{
|
||||
camera::RenderStrategy,
|
||||
float::consts::PI,
|
||||
patterns::{test_pattern, BLACK_PAT, WHITE_PAT},
|
||||
WHITE,
|
||||
};
|
||||
|
||||
/// End of chapter 14 challenge.
|
||||
#[derive(StructOpt, Debug)]
|
||||
#[structopt(name = "eoc14")]
|
||||
struct Opt {
|
||||
/// Strategy for casting rays into image.
|
||||
#[structopt(long, default_value = "rayon")]
|
||||
render_strategy: RenderStrategy,
|
||||
/// Number of samples per pixel. 0 renders from the center of the pixel, 1 or more samples N
|
||||
/// times randomly across the pixel.
|
||||
#[structopt(short, long, default_value = "0")]
|
||||
samples: usize,
|
||||
/// Rendered image width in pixels.
|
||||
#[structopt(short, long, default_value = "2560")]
|
||||
width: usize,
|
||||
/// Rendered image height in pixels.
|
||||
#[structopt(short, long, default_value = "1440")]
|
||||
height: usize,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let start = Instant::now();
|
||||
let opt = Opt::from_args();
|
||||
|
||||
let light1 = PointLightBuilder::default()
|
||||
.position(point(-5., 5., -5.))
|
||||
.intensity(WHITE)
|
||||
.build()?;
|
||||
let light2 = PointLightBuilder::default()
|
||||
.position(point(5., 5., -5.))
|
||||
.intensity([0.2, 0.2, 0.6])
|
||||
.build()?;
|
||||
let light3 = PointLightBuilder::default()
|
||||
.position(point(0., 2., -5.))
|
||||
.intensity([0.2, 0.2, 0.1])
|
||||
.build()?;
|
||||
|
||||
let from = point(2., 8., -10.);
|
||||
let to = point(2., 2., -1.);
|
||||
let up = point(0., 1., 0.);
|
||||
let camera = CameraBuilder::default()
|
||||
.hsize(opt.width)
|
||||
.vsize(opt.height)
|
||||
.field_of_view(PI / 12.)
|
||||
.transform(view_transform(from, to, up))
|
||||
.render_strategy(opt.render_strategy)
|
||||
.samples_per_pixel(opt.samples)
|
||||
.build()?;
|
||||
|
||||
let floor = plane()
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color(
|
||||
checkers_pattern(
|
||||
ring_pattern([0.8, 0.8, 0.2].into(), [0.8, 0.2, 0.8].into())
|
||||
.transform(scaling(1. / 8., 1. / 8., 1. / 8.))
|
||||
.build()?,
|
||||
ring_pattern([0.2, 0.8, 0.2].into(), [0.8, 0.2, 0.2].into())
|
||||
.transform(scaling(1. / 4., 1. / 4., 1. / 4.))
|
||||
.build()?,
|
||||
)
|
||||
.transform(translation(0., 1., 0.) * scaling(2., 2., 2.))
|
||||
.build()?,
|
||||
)
|
||||
.specular(0.)
|
||||
.reflective(0.5)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
let sphere_size = scaling(0.5, 0.5, 0.5);
|
||||
|
||||
let x1y1 = sphere()
|
||||
.transform(translation(1., 1., 0.) * sphere_size)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color(
|
||||
gradient_pattern([0., 0., 1.].into(), [1., 1., 0.].into())
|
||||
.transform(scaling(2., 1., 1.) * translation(-0.5, 0., 0.))
|
||||
.build()?,
|
||||
)
|
||||
.diffuse(0.7)
|
||||
.specular(0.3)
|
||||
.reflective(0.5)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let x2y1 = sphere()
|
||||
.transform(translation(2., 1., 0.) * sphere_size)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color(stripe_pattern(WHITE_PAT, BLACK_PAT).build()?)
|
||||
.diffuse(0.7)
|
||||
.specular(0.3)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let x3y1 = sphere()
|
||||
.transform(translation(3., 1., 0.) * sphere_size)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color(
|
||||
stripe_pattern(WHITE_PAT, BLACK_PAT)
|
||||
.transform(scaling(0.2, 1., 1.))
|
||||
.build()?,
|
||||
)
|
||||
.diffuse(0.7)
|
||||
.specular(0.0)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let x1y2 = sphere()
|
||||
.transform(translation(1., 2., 0.) * sphere_size)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color(test_pattern().build()?)
|
||||
.diffuse(0.7)
|
||||
.specular(0.3)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let x2y2 = sphere()
|
||||
.transform(translation(2., 2., 0.) * sphere_size)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color(
|
||||
ring_pattern(WHITE_PAT, BLACK_PAT)
|
||||
.transform(scaling(0.2, 0.2, 0.2))
|
||||
.build()?,
|
||||
)
|
||||
.diffuse(0.7)
|
||||
.specular(0.3)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let x3y2 = sphere()
|
||||
.transform(translation(3., 2., 0.) * sphere_size)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color(
|
||||
checkers_pattern(WHITE_PAT, BLACK_PAT)
|
||||
.transform(scaling(0.5, 0.5, 0.5))
|
||||
.build()?,
|
||||
)
|
||||
.diffuse(0.7)
|
||||
.specular(0.3)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let x1y2z1 = sphere()
|
||||
.transform(translation(1., 2., -1.) * sphere_size)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color([0.8, 0.2, 0.2])
|
||||
.diffuse(0.7)
|
||||
.specular(0.3)
|
||||
.transparency(0.5)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let x2y2z1 = sphere()
|
||||
.transform(translation(2., 2., -1.) * sphere_size)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color([0.8, 0.2, 0.2])
|
||||
.diffuse(0.7)
|
||||
.specular(0.3)
|
||||
.transparency(0.9)
|
||||
.refractive_index(1.5)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let x3y2z1 = cube()
|
||||
.transform(
|
||||
translation(3., 2., -1.) * (sphere_size * scaling(0.5, 0.5, 0.5)) * rotation_y(PI / 4.),
|
||||
)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color([0.8, 0.8, 0.2])
|
||||
.diffuse(0.7)
|
||||
.specular(0.3)
|
||||
.reflective(0.5)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let x2y4z1 = sphere()
|
||||
.transform(translation(2., 4., 0.) * sphere_size)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color([0.2, 0.2, 0.8])
|
||||
.diffuse(0.7)
|
||||
.specular(0.3)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let world = WorldBuilder::default()
|
||||
.lights(vec![light1, light2, light3])
|
||||
.objects(vec![
|
||||
floor, x1y1, x2y1, x3y1, x1y2, x2y2, x3y2, x1y2z1, x2y2z1, x2y4z1, x3y2z1,
|
||||
])
|
||||
.build()?;
|
||||
|
||||
let image = camera.render(&world);
|
||||
|
||||
let path = "/tmp/eoc14.png";
|
||||
println!("saving output to {}", path);
|
||||
image.write_to_file(path)?;
|
||||
println!("Render time {:.3} seconds", start.elapsed().as_secs_f32());
|
||||
Ok(())
|
||||
}
|
||||
73
rtchallenge/examples/eoc2.rs
Normal file
73
rtchallenge/examples/eoc2.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
use rtchallenge::{
|
||||
canvas::Canvas,
|
||||
tuples::{Color, Tuple},
|
||||
Float,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Environment {
|
||||
gravity: Tuple,
|
||||
wind: Tuple,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
struct Projectile {
|
||||
position: Tuple,
|
||||
velocity: Tuple,
|
||||
}
|
||||
|
||||
fn tick(env: &Environment, proj: &Projectile) -> Projectile {
|
||||
let position = proj.position + proj.velocity;
|
||||
let velocity = proj.velocity + env.gravity + env.wind;
|
||||
Projectile { position, velocity }
|
||||
}
|
||||
|
||||
fn draw_dot(c: &mut Canvas, x: usize, y: usize) {
|
||||
let red = Color::new(1., 0., 0.);
|
||||
c.set(x.saturating_sub(1), y.saturating_sub(1), red);
|
||||
c.set(x, y.saturating_sub(1), red);
|
||||
c.set(x + 1, y.saturating_sub(1), red);
|
||||
c.set(x.saturating_sub(1), y, red);
|
||||
c.set(x, y, red);
|
||||
c.set(x + 1, y, red);
|
||||
c.set(x.saturating_sub(1), y + 1, red);
|
||||
c.set(x, y + 1, red);
|
||||
c.set(x + 1, y + 1, red);
|
||||
}
|
||||
fn main() -> Result<()> {
|
||||
let position = Tuple::point(0., 1., 0.);
|
||||
let velocity = Tuple::vector(1., 1.8, 0.).normalize() * 11.25;
|
||||
let mut p = Projectile { position, velocity };
|
||||
let gravity = Tuple::vector(0., -0.1, 0.);
|
||||
let wind = Tuple::vector(-0.01, 0., 0.);
|
||||
let e = Environment { gravity, wind };
|
||||
|
||||
let w = 800;
|
||||
let h = 600;
|
||||
let bg = Color::new(0.2, 0.2, 0.2);
|
||||
let mut c = Canvas::new(w, h, bg);
|
||||
let mut i = 0;
|
||||
let w = w as Float;
|
||||
let h = h as Float;
|
||||
while p.position.y > 0. {
|
||||
p = tick(&e, &p);
|
||||
println!("tick {}: proj {:?}", i, p);
|
||||
let x = p.position.x;
|
||||
let y = p.position.y;
|
||||
if x > 0. && x < w && y > 0. && y < h {
|
||||
let x = x as usize;
|
||||
let y = (h - y) as usize;
|
||||
draw_dot(&mut c, x, y)
|
||||
}
|
||||
i += 1;
|
||||
if i > 1000 {
|
||||
bail!("too many iterations");
|
||||
}
|
||||
}
|
||||
|
||||
let path = "/tmp/output.png";
|
||||
println!("saving output to {}", path);
|
||||
c.write_to_file(path)?;
|
||||
Ok(())
|
||||
}
|
||||
50
rtchallenge/examples/eoc4.rs
Normal file
50
rtchallenge/examples/eoc4.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use anyhow::Result;
|
||||
|
||||
use rtchallenge::{
|
||||
canvas::Canvas,
|
||||
float::consts::PI,
|
||||
matrices::Matrix4x4,
|
||||
tuples::{Color, Tuple},
|
||||
Float,
|
||||
};
|
||||
|
||||
fn draw_dot(c: &mut Canvas, x: usize, y: usize) {
|
||||
let red = Color::new(1., 0., 0.);
|
||||
c.set(x.saturating_sub(1), y.saturating_sub(1), red);
|
||||
c.set(x, y.saturating_sub(1), red);
|
||||
c.set(x + 1, y.saturating_sub(1), red);
|
||||
c.set(x.saturating_sub(1), y, red);
|
||||
c.set(x, y, red);
|
||||
c.set(x + 1, y, red);
|
||||
c.set(x.saturating_sub(1), y + 1, red);
|
||||
c.set(x, y + 1, red);
|
||||
c.set(x + 1, y + 1, red);
|
||||
}
|
||||
fn main() -> Result<()> {
|
||||
let w = 200;
|
||||
let h = w;
|
||||
let bg = Color::new(0.2, 0.2, 0.2);
|
||||
let mut c = Canvas::new(w, h, bg);
|
||||
let t = Matrix4x4::translation(0., 0.4, 0.);
|
||||
let p = Tuple::point(0., 0., 0.);
|
||||
let rot_hour = Matrix4x4::rotation_z(-PI / 6.);
|
||||
|
||||
let mut p = t * p;
|
||||
let w = w as Float;
|
||||
let h = h as Float;
|
||||
// The 'world' exists between -0.5 - 0.5 in X-Y plane.
|
||||
// To convert to screen space, we translate by 0.5, scale to canvas size,
|
||||
// and invert the Y-axis.
|
||||
let world_to_screen =
|
||||
Matrix4x4::scaling(w as Float, -h as Float, 1.0) * Matrix4x4::translation(0.5, -0.5, 0.);
|
||||
for _ in 0..12 {
|
||||
let canvas_pixel = world_to_screen * p;
|
||||
draw_dot(&mut c, canvas_pixel.x as usize, canvas_pixel.y as usize);
|
||||
p = rot_hour * p;
|
||||
}
|
||||
|
||||
let path = "/tmp/eoc4.png";
|
||||
println!("saving output to {}", path);
|
||||
c.write_to_file(path)?;
|
||||
Ok(())
|
||||
}
|
||||
45
rtchallenge/examples/eoc5.rs
Normal file
45
rtchallenge/examples/eoc5.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use anyhow::Result;
|
||||
|
||||
use rtchallenge::{
|
||||
canvas::Canvas,
|
||||
matrices::Matrix4x4,
|
||||
rays::Ray,
|
||||
shapes::{intersect, Shape},
|
||||
tuples::{Color, Tuple},
|
||||
Float,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let w = 200;
|
||||
let h = w;
|
||||
let bg = Color::new(0.2, 0.2, 0.2);
|
||||
let mut c = Canvas::new(w, h, bg);
|
||||
let ray_origin = Tuple::point(0., 0., -5.);
|
||||
let wall_z = 10.;
|
||||
let wall_size = 7.;
|
||||
let pixel_size = wall_size / w as Float;
|
||||
let half = wall_size / 2.;
|
||||
let color = Color::new(1., 0., 0.);
|
||||
let mut shape = Shape::sphere();
|
||||
shape.set_transform(
|
||||
Matrix4x4::shearing(1., 0., 0., 0., 0., 0.) * Matrix4x4::scaling(0.5, 1., 1.0),
|
||||
);
|
||||
|
||||
for y in 0..h {
|
||||
let world_y = half - pixel_size * y as Float;
|
||||
for x in 0..w {
|
||||
let world_x = -half + pixel_size * x as Float;
|
||||
let position = Tuple::point(world_x, world_y, wall_z);
|
||||
let r = Ray::new(ray_origin, (position - ray_origin).normalize());
|
||||
let xs = intersect(&shape, &r);
|
||||
if xs.hit().is_some() {
|
||||
c.set(x, y, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let path = "/tmp/eoc5.png";
|
||||
println!("saving output to {}", path);
|
||||
c.write_to_file(path)?;
|
||||
Ok(())
|
||||
}
|
||||
65
rtchallenge/examples/eoc6.rs
Normal file
65
rtchallenge/examples/eoc6.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use anyhow::Result;
|
||||
|
||||
use rtchallenge::{
|
||||
canvas::Canvas,
|
||||
lights::PointLight,
|
||||
materials::{lighting, Material},
|
||||
rays::Ray,
|
||||
shapes::{intersect, Shape},
|
||||
tuples::{Color, Tuple},
|
||||
Float, WHITE,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let w = 640;
|
||||
let h = w;
|
||||
let bg = Color::new(0.2, 0.2, 0.2);
|
||||
let mut c = Canvas::new(w, h, bg);
|
||||
let ray_origin = Tuple::point(0., 0., -5.);
|
||||
let wall_z = 10.;
|
||||
let wall_size = 7.;
|
||||
let pixel_size = wall_size / w as Float;
|
||||
let half = wall_size / 2.;
|
||||
let mut shape = Shape::sphere();
|
||||
shape.material = Material {
|
||||
color: Color::new(1., 0.2, 1.).into(),
|
||||
specular: 0.5,
|
||||
diffuse: 0.7,
|
||||
shininess: 30.,
|
||||
..Material::default()
|
||||
};
|
||||
let light_position = Tuple::point(-10., 10., -10.);
|
||||
let light_color = WHITE;
|
||||
let light = PointLight::new(light_position, light_color);
|
||||
let in_shadow = false;
|
||||
for y in 0..h {
|
||||
let world_y = half - pixel_size * y as Float;
|
||||
for x in 0..w {
|
||||
let world_x = -half + pixel_size * x as Float;
|
||||
let position = Tuple::point(world_x, world_y, wall_z);
|
||||
let direction = (position - ray_origin).normalize();
|
||||
let r = Ray::new(ray_origin, direction);
|
||||
let xs = intersect(&shape, &r);
|
||||
if let Some(hit) = xs.hit() {
|
||||
let point = r.position(hit.t);
|
||||
let normal = hit.object.normal_at(point);
|
||||
let eye = -r.direction;
|
||||
let color = lighting(
|
||||
&hit.object.material,
|
||||
&hit.object,
|
||||
&light,
|
||||
point,
|
||||
eye,
|
||||
normal,
|
||||
in_shadow,
|
||||
);
|
||||
c.set(x, y, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let path = "/tmp/eoc6.png";
|
||||
println!("saving output to {}", path);
|
||||
c.write_to_file(path)?;
|
||||
Ok(())
|
||||
}
|
||||
108
rtchallenge/examples/eoc7.rs
Normal file
108
rtchallenge/examples/eoc7.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::Result;
|
||||
use structopt::StructOpt;
|
||||
|
||||
use rtchallenge::{
|
||||
camera::{Camera, RenderStrategy},
|
||||
float::consts::PI,
|
||||
lights::PointLight,
|
||||
materials::Material,
|
||||
matrices::Matrix4x4,
|
||||
shapes::Shape,
|
||||
transformations::view_transform,
|
||||
tuples::{Color, Tuple},
|
||||
world::World,
|
||||
WHITE,
|
||||
};
|
||||
|
||||
/// End of chapter 7 challenge.
|
||||
#[derive(StructOpt, Debug)]
|
||||
#[structopt(name = "eoc7")]
|
||||
struct Opt {
|
||||
#[structopt(long, default_value = "rayon")]
|
||||
render_strategy: RenderStrategy,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let start = Instant::now();
|
||||
let opt = Opt::from_args();
|
||||
let width = 1024;
|
||||
let height = 1024;
|
||||
let light_position = Tuple::point(-10., 10., -10.);
|
||||
let light_color = WHITE;
|
||||
let light = PointLight::new(light_position, light_color);
|
||||
let mut camera = Camera::new(width, height, PI / 3.);
|
||||
let from = Tuple::point(0., 1.5, -5.);
|
||||
let to = Tuple::point(0., 1., 0.);
|
||||
let up = Tuple::point(0., 1., 0.);
|
||||
camera.set_transform(view_transform(from, to, up));
|
||||
camera.render_strategy = opt.render_strategy;
|
||||
|
||||
let mut floor = Shape::sphere();
|
||||
floor.set_transform(Matrix4x4::scaling(10., 0.01, 10.));
|
||||
floor.material = Material {
|
||||
color: Color::new(1., 0.9, 0.9).into(),
|
||||
specular: 0.,
|
||||
..Material::default()
|
||||
};
|
||||
|
||||
let mut left_wall = Shape::sphere();
|
||||
left_wall.set_transform(
|
||||
Matrix4x4::translation(0., 0., 5.)
|
||||
* Matrix4x4::rotation_y(-PI / 4.)
|
||||
* Matrix4x4::rotation_x(PI / 2.)
|
||||
* Matrix4x4::scaling(10., 0.01, 10.),
|
||||
);
|
||||
left_wall.material = floor.material.clone();
|
||||
|
||||
let mut right_wall = Shape::sphere();
|
||||
right_wall.set_transform(
|
||||
Matrix4x4::translation(0., 0., 5.)
|
||||
* Matrix4x4::rotation_y(PI / 4.)
|
||||
* Matrix4x4::rotation_x(PI / 2.)
|
||||
* Matrix4x4::scaling(10., 0.01, 10.),
|
||||
);
|
||||
right_wall.material = floor.material.clone();
|
||||
|
||||
let mut middle = Shape::sphere();
|
||||
middle.set_transform(Matrix4x4::translation(-0.5, 1., 0.5));
|
||||
middle.material = Material {
|
||||
color: Color::new(0.1, 1., 0.5).into(),
|
||||
diffuse: 0.7,
|
||||
specular: 0.3,
|
||||
..Material::default()
|
||||
};
|
||||
|
||||
let mut right = Shape::sphere();
|
||||
right.set_transform(Matrix4x4::translation(1.5, 0.5, -0.5) * Matrix4x4::scaling(0.5, 0.5, 0.5));
|
||||
right.material = Material {
|
||||
color: Color::new(0.5, 1., 0.1).into(),
|
||||
diffuse: 0.7,
|
||||
specular: 0.3,
|
||||
..Material::default()
|
||||
};
|
||||
|
||||
let mut left = Shape::sphere();
|
||||
left.set_transform(
|
||||
Matrix4x4::translation(-1.5, 0.33, -0.75) * Matrix4x4::scaling(0.33, 0.33, 0.33),
|
||||
);
|
||||
left.material = Material {
|
||||
color: Color::new(1., 0.8, 0.1).into(),
|
||||
diffuse: 0.7,
|
||||
specular: 0.3,
|
||||
..Material::default()
|
||||
};
|
||||
|
||||
let mut world = World::default();
|
||||
world.lights = vec![light];
|
||||
world.objects = vec![floor, left_wall, right_wall, middle, right, left];
|
||||
|
||||
let image = camera.render(&world);
|
||||
|
||||
let path = "/tmp/eoc7.png";
|
||||
println!("saving output to {}", path);
|
||||
image.write_to_file(path)?;
|
||||
println!("Render time {:.3} seconds", start.elapsed().as_secs_f32());
|
||||
Ok(())
|
||||
}
|
||||
120
rtchallenge/examples/eoc8.rs
Normal file
120
rtchallenge/examples/eoc8.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::Result;
|
||||
use structopt::StructOpt;
|
||||
|
||||
use rtchallenge::{
|
||||
camera::{Camera, RenderStrategy},
|
||||
float::consts::PI,
|
||||
lights::PointLight,
|
||||
materials::Material,
|
||||
matrices::Matrix4x4,
|
||||
shapes::Shape,
|
||||
transformations::view_transform,
|
||||
tuples::{Color, Tuple},
|
||||
world::World,
|
||||
WHITE,
|
||||
};
|
||||
|
||||
/// End of chapter 8 challenge.
|
||||
#[derive(StructOpt, Debug)]
|
||||
#[structopt(name = "eoc8")]
|
||||
struct Opt {
|
||||
#[structopt(long, default_value = "rayon")]
|
||||
render_strategy: RenderStrategy,
|
||||
/// Number of samples per pixel. 0 renders from the center of the pixel, 1 or more samples N
|
||||
/// times randomly across the pixel.
|
||||
#[structopt(short, long, default_value = "0")]
|
||||
samples: usize,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let start = Instant::now();
|
||||
let opt = Opt::from_args();
|
||||
let width = 2560;
|
||||
let height = 1440;
|
||||
|
||||
let light_position = Tuple::point(-10., 10., -10.);
|
||||
let light_color = WHITE;
|
||||
let light1 = PointLight::new(light_position, light_color);
|
||||
let light_position = Tuple::point(10., 10., -10.);
|
||||
let light_color = Color::new(0.2, 0.2, 0.2);
|
||||
let light2 = PointLight::new(light_position, light_color);
|
||||
let light_position = Tuple::point(0., 10., -10.);
|
||||
let light3 = PointLight::new(light_position, light_color);
|
||||
|
||||
let mut camera = Camera::new(width, height, PI / 4.);
|
||||
let from = Tuple::point(0., 1.5, -5.);
|
||||
let to = Tuple::point(0., 1., 0.);
|
||||
let up = Tuple::point(0., 1., 0.);
|
||||
camera.set_transform(view_transform(from, to, up));
|
||||
camera.render_strategy = opt.render_strategy;
|
||||
camera.samples_per_pixel = opt.samples;
|
||||
|
||||
let mut floor = Shape::sphere();
|
||||
floor.set_transform(Matrix4x4::scaling(10., 0.01, 10.));
|
||||
floor.material = Material {
|
||||
color: Color::new(1., 0.9, 0.9).into(),
|
||||
specular: 0.,
|
||||
..Material::default()
|
||||
};
|
||||
|
||||
let mut left_wall = Shape::sphere();
|
||||
left_wall.set_transform(
|
||||
Matrix4x4::translation(0., 0., 5.)
|
||||
* Matrix4x4::rotation_y(-PI / 4.)
|
||||
* Matrix4x4::rotation_x(PI / 2.)
|
||||
* Matrix4x4::scaling(10., 0.01, 10.),
|
||||
);
|
||||
left_wall.material = floor.material.clone();
|
||||
|
||||
let mut right_wall = Shape::sphere();
|
||||
right_wall.set_transform(
|
||||
Matrix4x4::translation(0., 0., 5.)
|
||||
* Matrix4x4::rotation_y(PI / 4.)
|
||||
* Matrix4x4::rotation_x(PI / 2.)
|
||||
* Matrix4x4::scaling(10., 0.00001, 10.),
|
||||
);
|
||||
right_wall.material = floor.material.clone();
|
||||
|
||||
let mut middle = Shape::sphere();
|
||||
middle.set_transform(Matrix4x4::translation(-0.5, 1., 0.5));
|
||||
middle.material = Material {
|
||||
color: Color::new(0.1, 1., 0.5).into(),
|
||||
diffuse: 0.7,
|
||||
specular: 0.3,
|
||||
..Material::default()
|
||||
};
|
||||
|
||||
let mut right = Shape::sphere();
|
||||
right.set_transform(Matrix4x4::translation(1.5, 0.5, -0.5) * Matrix4x4::scaling(0.5, 0.5, 0.5));
|
||||
right.material = Material {
|
||||
color: Color::new(1., 1., 1.).into(),
|
||||
diffuse: 0.7,
|
||||
specular: 0.0,
|
||||
..Material::default()
|
||||
};
|
||||
|
||||
let mut left = Shape::sphere();
|
||||
left.set_transform(
|
||||
Matrix4x4::translation(-1.5, 0.33, -0.75) * Matrix4x4::scaling(0.33, 0.33, 0.33),
|
||||
);
|
||||
left.material = Material {
|
||||
color: Color::new(1., 0.8, 0.1).into(),
|
||||
diffuse: 0.7,
|
||||
specular: 0.3,
|
||||
..Material::default()
|
||||
};
|
||||
|
||||
let mut world = World::default();
|
||||
world.lights = vec![light1, light2, light3];
|
||||
world.objects = vec![floor, left_wall, right_wall, middle, right, left];
|
||||
|
||||
let image = camera.render(&world);
|
||||
|
||||
let path = "/tmp/eoc8.png";
|
||||
println!("saving output to {}", path);
|
||||
image.write_to_file(path)?;
|
||||
println!("Render time {:.3} seconds", start.elapsed().as_secs_f32());
|
||||
Ok(())
|
||||
}
|
||||
122
rtchallenge/examples/eoc9-prelude.rs
Normal file
122
rtchallenge/examples/eoc9-prelude.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::Result;
|
||||
use structopt::StructOpt;
|
||||
|
||||
use rtchallenge::prelude::*;
|
||||
|
||||
use rtchallenge::{camera::RenderStrategy, float::consts::PI, WHITE};
|
||||
|
||||
/// End of chapter 9 challenge.
|
||||
#[derive(StructOpt, Debug)]
|
||||
#[structopt(name = "eoc9")]
|
||||
struct Opt {
|
||||
/// Strategy for casting rays into image.
|
||||
#[structopt(long, default_value = "rayon")]
|
||||
render_strategy: RenderStrategy,
|
||||
/// Number of samples per pixel. 0 renders from the center of the pixel, 1 or more samples N
|
||||
/// times randomly across the pixel.
|
||||
#[structopt(short, long, default_value = "0")]
|
||||
samples: usize,
|
||||
/// Rendered image width in pixels.
|
||||
#[structopt(short, long, default_value = "2560")]
|
||||
width: usize,
|
||||
/// Rendered image height in pixels.
|
||||
#[structopt(short, long, default_value = "1440")]
|
||||
height: usize,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let start = Instant::now();
|
||||
let opt = Opt::from_args();
|
||||
|
||||
let light1 = PointLightBuilder::default()
|
||||
.position(point(-5., 5., -5.))
|
||||
.intensity(WHITE)
|
||||
.build()?;
|
||||
let light2 = PointLightBuilder::default()
|
||||
.position(point(5., 5., -5.))
|
||||
.intensity([0.2, 0.2, 0.6])
|
||||
.build()?;
|
||||
let light3 = PointLightBuilder::default()
|
||||
.position(point(0., 2., -5.))
|
||||
.intensity([0.2, 0.2, 0.1])
|
||||
.build()?;
|
||||
|
||||
let from = point(0., 1.5, -5.);
|
||||
let to = point(0., 1., 0.);
|
||||
let up = point(0., 1., 0.);
|
||||
let camera = CameraBuilder::default()
|
||||
.hsize(opt.width)
|
||||
.vsize(opt.height)
|
||||
.field_of_view(PI / 4.)
|
||||
.transform(view_transform(from, to, up))
|
||||
.render_strategy(opt.render_strategy)
|
||||
.samples_per_pixel(opt.samples)
|
||||
.build()?;
|
||||
|
||||
let floor = plane()
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color([1., 0.2, 0.2])
|
||||
.specular(0.)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let ceiling = plane()
|
||||
.transform(translation(0., 6., 0.) * rotation_x(PI))
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color([0.6, 0.6, 0.8])
|
||||
.specular(0.2)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let middle = sphere()
|
||||
.transform(translation(-0.5, 0.5, 0.5))
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color([0.1, 1., 0.5])
|
||||
.diffuse(0.7)
|
||||
.specular(0.3)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let right = sphere()
|
||||
.transform(translation(1.5, 0.5, -0.5) * scaling(0.5, 0.5, 0.5))
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color([1., 1., 1.])
|
||||
.diffuse(0.7)
|
||||
.specular(0.0)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let left = sphere()
|
||||
.transform(translation(-1.5, 0.33, -0.75) * scaling(0.33, 0.33, 0.33))
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color([1., 0.8, 0.1])
|
||||
.diffuse(0.7)
|
||||
.specular(0.3)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let world = WorldBuilder::default()
|
||||
.lights(vec![light1, light2, light3])
|
||||
.objects(vec![floor, ceiling, middle, right, left])
|
||||
.build()?;
|
||||
|
||||
let image = camera.render(&world);
|
||||
|
||||
let path = "/tmp/eoc9.png";
|
||||
println!("saving output to {}", path);
|
||||
image.write_to_file(path)?;
|
||||
println!("Render time {:.3} seconds", start.elapsed().as_secs_f32());
|
||||
Ok(())
|
||||
}
|
||||
114
rtchallenge/examples/eoc9.rs
Normal file
114
rtchallenge/examples/eoc9.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::Result;
|
||||
use structopt::StructOpt;
|
||||
|
||||
use rtchallenge::{
|
||||
camera::{Camera, RenderStrategy},
|
||||
float::consts::PI,
|
||||
lights::PointLight,
|
||||
materials::Material,
|
||||
matrices::Matrix4x4,
|
||||
shapes::Shape,
|
||||
transformations::view_transform,
|
||||
tuples::{Color, Tuple},
|
||||
world::World,
|
||||
WHITE,
|
||||
};
|
||||
|
||||
/// End of chapter 9 challenge.
|
||||
#[derive(StructOpt, Debug)]
|
||||
#[structopt(name = "eoc9")]
|
||||
struct Opt {
|
||||
/// Strategy for casting rays into image.
|
||||
#[structopt(long, default_value = "rayon")]
|
||||
render_strategy: RenderStrategy,
|
||||
/// Number of samples per pixel. 0 renders from the center of the pixel, 1 or more samples N
|
||||
/// times randomly across the pixel.
|
||||
#[structopt(short, long, default_value = "0")]
|
||||
samples: usize,
|
||||
/// Rendered image width in pixels.
|
||||
#[structopt(short, long, default_value = "2560")]
|
||||
width: usize,
|
||||
/// Rendered image height in pixels.
|
||||
#[structopt(short, long, default_value = "1440")]
|
||||
height: usize,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let start = Instant::now();
|
||||
let opt = Opt::from_args();
|
||||
|
||||
let light_position = Tuple::point(-5., 5., -5.);
|
||||
let light_color = WHITE;
|
||||
let light1 = PointLight::new(light_position, light_color);
|
||||
let light_position = Tuple::point(5., 5., -5.);
|
||||
let light_color = Color::new(0.2, 0.2, 0.6);
|
||||
let light2 = PointLight::new(light_position, light_color);
|
||||
let light_position = Tuple::point(0., 2., -5.);
|
||||
let light_color = Color::new(0.2, 0.2, 0.1);
|
||||
let light3 = PointLight::new(light_position, light_color);
|
||||
|
||||
let mut camera = Camera::new(opt.width, opt.height, PI / 4.);
|
||||
let from = Tuple::point(0., 1.5, -5.);
|
||||
let to = Tuple::point(0., 1., 0.);
|
||||
let up = Tuple::point(0., 1., 0.);
|
||||
camera.set_transform(view_transform(from, to, up));
|
||||
camera.render_strategy = opt.render_strategy;
|
||||
camera.samples_per_pixel = opt.samples;
|
||||
|
||||
let mut floor = Shape::plane();
|
||||
floor.material = Material {
|
||||
color: Color::new(1., 0.2, 0.2).into(),
|
||||
specular: 0.,
|
||||
..Material::default()
|
||||
};
|
||||
let mut ceiling = Shape::plane();
|
||||
ceiling.set_transform(Matrix4x4::translation(0., 6., 0.) * Matrix4x4::rotation_x(PI));
|
||||
ceiling.material = Material {
|
||||
color: Color::new(0.6, 0.6, 0.8).into(),
|
||||
specular: 0.2,
|
||||
..Material::default()
|
||||
};
|
||||
|
||||
let mut middle = Shape::sphere();
|
||||
middle.set_transform(Matrix4x4::translation(-0.5, 0.5, 0.5));
|
||||
middle.material = Material {
|
||||
color: Color::new(0.1, 1., 0.5).into(),
|
||||
diffuse: 0.7,
|
||||
specular: 0.3,
|
||||
..Material::default()
|
||||
};
|
||||
|
||||
let mut right = Shape::sphere();
|
||||
right.set_transform(Matrix4x4::translation(1.5, 0.5, -0.5) * Matrix4x4::scaling(0.5, 0.5, 0.5));
|
||||
right.material = Material {
|
||||
color: Color::new(1., 1., 1.).into(),
|
||||
diffuse: 0.7,
|
||||
specular: 0.0,
|
||||
..Material::default()
|
||||
};
|
||||
|
||||
let mut left = Shape::sphere();
|
||||
left.set_transform(
|
||||
Matrix4x4::translation(-1.5, 0.33, -0.75) * Matrix4x4::scaling(0.33, 0.33, 0.33),
|
||||
);
|
||||
left.material = Material {
|
||||
color: Color::new(1., 0.8, 0.1).into(),
|
||||
diffuse: 0.7,
|
||||
specular: 0.3,
|
||||
..Material::default()
|
||||
};
|
||||
|
||||
let mut world = World::default();
|
||||
world.lights = vec![light1, light2, light3];
|
||||
world.objects = vec![floor, ceiling, middle, right, left];
|
||||
|
||||
let image = camera.render(&world);
|
||||
|
||||
let path = "/tmp/eoc9.png";
|
||||
println!("saving output to {}", path);
|
||||
image.write_to_file(path)?;
|
||||
println!("Render time {:.3} seconds", start.elapsed().as_secs_f32());
|
||||
Ok(())
|
||||
}
|
||||
115
rtchallenge/examples/glass.rs
Normal file
115
rtchallenge/examples/glass.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::Result;
|
||||
use structopt::StructOpt;
|
||||
|
||||
use rtchallenge::prelude::*;
|
||||
|
||||
use rtchallenge::{
|
||||
camera::RenderStrategy,
|
||||
float::consts::PI,
|
||||
patterns::{BLACK_PAT, WHITE_PAT},
|
||||
WHITE,
|
||||
};
|
||||
|
||||
/// Experiment with transparency.
|
||||
#[derive(StructOpt, Debug)]
|
||||
#[structopt(name = "glass")]
|
||||
struct Opt {
|
||||
/// Strategy for casting rays into image.
|
||||
#[structopt(long, default_value = "rayon")]
|
||||
render_strategy: RenderStrategy,
|
||||
/// Number of samples per pixel. 0 renders from the center of the pixel, 1 or more samples N
|
||||
/// times randomly across the pixel.
|
||||
#[structopt(short, long, default_value = "0")]
|
||||
samples: usize,
|
||||
/// Rendered image width in pixels.
|
||||
#[structopt(short, long, default_value = "2560")]
|
||||
width: usize,
|
||||
/// Rendered image height in pixels.
|
||||
#[structopt(short, long, default_value = "1440")]
|
||||
height: usize,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let start = Instant::now();
|
||||
let opt = Opt::from_args();
|
||||
|
||||
let light1 = PointLightBuilder::default()
|
||||
.position(point(-5., 5., -5.))
|
||||
.intensity(WHITE)
|
||||
.build()?;
|
||||
let light2 = PointLightBuilder::default()
|
||||
.position(point(5., 5., -5.))
|
||||
.intensity([0.2, 0.2, 0.6])
|
||||
.build()?;
|
||||
let light3 = PointLightBuilder::default()
|
||||
.position(point(0., 2., -5.))
|
||||
.intensity([0.2, 0.2, 0.1])
|
||||
.build()?;
|
||||
|
||||
let from = point(0., 0., -10.);
|
||||
let to = point(0., 0., 0.);
|
||||
let up = point(0., 1., 0.);
|
||||
let camera = CameraBuilder::default()
|
||||
.hsize(opt.width)
|
||||
.vsize(opt.height)
|
||||
.field_of_view(PI / 4.)
|
||||
.transform(view_transform(from, to, up))
|
||||
.render_strategy(opt.render_strategy)
|
||||
.samples_per_pixel(opt.samples)
|
||||
.build()?;
|
||||
|
||||
let floor = plane()
|
||||
.transform(rotation_x(PI / 2.))
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color(checkers_pattern(BLACK_PAT, WHITE_PAT).build()?)
|
||||
.reflective(0.5)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
let ceiling = plane()
|
||||
.transform(translation(0., 0., -20.) * rotation_x(PI / 2.))
|
||||
.material(MaterialBuilder::default().color([0.2, 0.2, 0.8]).build()?)
|
||||
.build()?;
|
||||
|
||||
let mut objects = Vec::new();
|
||||
for x in 0..5 {
|
||||
for y in 0..5 {
|
||||
let trans = y as Float / 5.;
|
||||
let index = 1. + x as Float / 5.;
|
||||
dbg!(x, y, trans, index);
|
||||
objects.push(
|
||||
sphere()
|
||||
.transform(
|
||||
translation(x as Float - 2., y as Float - 2., -2.) * scaling(0.5, 0.5, 0.5),
|
||||
)
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color([0.5, 0.5, 0.5])
|
||||
.transparency(trans)
|
||||
.refractive_index(index)
|
||||
.build()?,
|
||||
)
|
||||
.build()?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
objects.push(floor);
|
||||
objects.push(ceiling);
|
||||
|
||||
let world = WorldBuilder::default()
|
||||
.lights(vec![light1, light2, light3])
|
||||
.objects(objects)
|
||||
.build()?;
|
||||
|
||||
let image = camera.render(&world);
|
||||
|
||||
let path = "/tmp/glass.png";
|
||||
println!("saving output to {}", path);
|
||||
image.write_to_file(path)?;
|
||||
println!("Render time {:.3} seconds", start.elapsed().as_secs_f32());
|
||||
Ok(())
|
||||
}
|
||||
2
rtchallenge/rustfmt.toml
Normal file
2
rtchallenge/rustfmt.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
imports_granularity = "Crate"
|
||||
format_code_in_doc_comments = true
|
||||
436
rtchallenge/src/camera.rs
Normal file
436
rtchallenge/src/camera.rs
Normal file
@@ -0,0 +1,436 @@
|
||||
use std::{
|
||||
str::FromStr,
|
||||
sync::{
|
||||
mpsc::{sync_channel, Receiver, SyncSender},
|
||||
Arc, Mutex,
|
||||
},
|
||||
thread,
|
||||
};
|
||||
|
||||
use derive_builder::Builder;
|
||||
use rand::Rng;
|
||||
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||
use serde::Deserialize;
|
||||
use structopt::StructOpt;
|
||||
|
||||
use crate::{
|
||||
canvas::Canvas,
|
||||
matrices::Matrix4x4,
|
||||
rays::Ray,
|
||||
tuples::{Color, Tuple},
|
||||
world::World,
|
||||
Float, BLACK,
|
||||
};
|
||||
|
||||
const MAX_DEPTH_RECURSION: usize = 10;
|
||||
|
||||
#[derive(Copy, Clone, StructOpt, Debug, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum RenderStrategy {
|
||||
Serial,
|
||||
Rayon,
|
||||
WorkerPool,
|
||||
}
|
||||
|
||||
impl Default for RenderStrategy {
|
||||
fn default() -> RenderStrategy {
|
||||
RenderStrategy::Rayon
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for RenderStrategy {
|
||||
type Err = serde_json::error::Error;
|
||||
fn from_str(s: &str) -> Result<RenderStrategy, serde_json::error::Error> {
|
||||
serde_json::from_str(&format!("\"{}\"", s))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Builder, Clone, Debug, Default)]
|
||||
#[builder(setter(skip), build_fn(skip))]
|
||||
pub struct Camera {
|
||||
#[builder(setter(skip = "false"))]
|
||||
hsize: usize,
|
||||
#[builder(setter(skip = "false"))]
|
||||
vsize: usize,
|
||||
#[builder(setter(skip = "false"))]
|
||||
field_of_view: Float,
|
||||
#[builder(setter(skip = "false"))]
|
||||
transform: Matrix4x4,
|
||||
inverse_transform: Matrix4x4,
|
||||
pixel_size: Float,
|
||||
half_width: Float,
|
||||
half_height: Float,
|
||||
#[builder(setter(skip = "false"))]
|
||||
pub render_strategy: RenderStrategy,
|
||||
/// 0 renders from the center of the pixel, 1 or higher is random sampling of the pixel.
|
||||
#[builder(setter(skip = "false"))]
|
||||
pub samples_per_pixel: usize,
|
||||
}
|
||||
|
||||
impl CameraBuilder {
|
||||
pub fn build(&self) -> Result<Camera, CameraBuilderError> {
|
||||
let hsize = match self.hsize {
|
||||
Some(ref value) => Clone::clone(value),
|
||||
None => {
|
||||
return Err(Into::into(::derive_builder::UninitializedFieldError::from(
|
||||
"hsize",
|
||||
)))
|
||||
}
|
||||
};
|
||||
let vsize = match self.vsize {
|
||||
Some(ref value) => Clone::clone(value),
|
||||
None => {
|
||||
return Err(Into::into(::derive_builder::UninitializedFieldError::from(
|
||||
"vsize",
|
||||
)))
|
||||
}
|
||||
};
|
||||
let field_of_view = match self.field_of_view {
|
||||
Some(ref value) => Clone::clone(value),
|
||||
None => {
|
||||
return Err(Into::into(::derive_builder::UninitializedFieldError::from(
|
||||
"field_of_view",
|
||||
)))
|
||||
}
|
||||
};
|
||||
let mut c = Camera::new(hsize, vsize, field_of_view);
|
||||
if let Some(transform) = self.transform {
|
||||
c.set_transform(transform);
|
||||
}
|
||||
if let Some(render_strategy) = self.render_strategy {
|
||||
c.render_strategy = render_strategy;
|
||||
}
|
||||
if let Some(samples_per_pixel) = self.samples_per_pixel {
|
||||
c.samples_per_pixel = samples_per_pixel;
|
||||
}
|
||||
Ok(c)
|
||||
}
|
||||
}
|
||||
|
||||
enum Request {
|
||||
Line { width: usize, y: usize },
|
||||
}
|
||||
|
||||
enum Response {
|
||||
Line { y: usize, pixels: Canvas },
|
||||
}
|
||||
|
||||
impl Camera {
|
||||
/// Create a camera with a canvas of pixel hsize (height) and vsize (width)
|
||||
/// with the given field of view (in radians).
|
||||
pub fn new(hsize: usize, vsize: usize, field_of_view: Float) -> Camera {
|
||||
let half_view = (field_of_view / 2.).tan();
|
||||
let aspect = hsize as Float / vsize as Float;
|
||||
let (half_width, half_height) = if aspect >= 1. {
|
||||
(half_view, half_view / aspect)
|
||||
} else {
|
||||
(half_view * aspect, half_view)
|
||||
};
|
||||
let pixel_size = 2. * half_width / hsize as Float;
|
||||
Camera {
|
||||
hsize,
|
||||
vsize,
|
||||
field_of_view,
|
||||
transform: Matrix4x4::identity(),
|
||||
inverse_transform: Matrix4x4::identity(),
|
||||
pixel_size,
|
||||
half_height,
|
||||
half_width,
|
||||
render_strategy: RenderStrategy::Rayon,
|
||||
samples_per_pixel: 0,
|
||||
}
|
||||
}
|
||||
pub fn hsize(&self) -> usize {
|
||||
self.hsize
|
||||
}
|
||||
pub fn vsize(&self) -> usize {
|
||||
self.vsize
|
||||
}
|
||||
pub fn field_of_view(&self) -> Float {
|
||||
self.field_of_view
|
||||
}
|
||||
pub fn transform(&self) -> Matrix4x4 {
|
||||
self.transform
|
||||
}
|
||||
pub fn set_transform(&mut self, t: Matrix4x4) {
|
||||
self.transform = t;
|
||||
self.inverse_transform = t.inverse();
|
||||
}
|
||||
pub fn pixel_size(&self) -> Float {
|
||||
self.pixel_size
|
||||
}
|
||||
pub fn supersample_rays_for_pixel(&self, px: usize, py: usize, samples: usize) -> Vec<Ray> {
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
(0..samples)
|
||||
.map(|_| {
|
||||
// The offset from the edge of the canvas to the pixel's corner.
|
||||
let xoffset = (px as Float + rng.gen::<Float>()) * self.pixel_size;
|
||||
let yoffset = (py as Float + rng.gen::<Float>()) * self.pixel_size;
|
||||
|
||||
// The untransformed coordinates of the pixle in world space.
|
||||
// (Remember that the camera looks toward -z, so +x is to the left.)
|
||||
let world_x = self.half_width - xoffset;
|
||||
let world_y = self.half_height - yoffset;
|
||||
|
||||
// Using the camera matrix, transofmrm the canvas point and the origin,
|
||||
// and then compute the ray's direction vector.
|
||||
// (Remember that the canvas is at z>=-1).
|
||||
let pixel = self.inverse_transform * Tuple::point(world_x, world_y, -1.);
|
||||
let origin = self.inverse_transform * Tuple::point(0., 0., 0.);
|
||||
let direction = (pixel - origin).normalize();
|
||||
|
||||
Ray::new(origin, direction)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Calculate ray that starts at the camera and passes through the (x,y)
|
||||
/// pixel on the canvas.
|
||||
pub fn ray_for_pixel(&self, px: usize, py: usize) -> Ray {
|
||||
// The offset from the edge of the canvas to the pixel's corner.
|
||||
let xoffset = (px as Float + 0.5) * self.pixel_size;
|
||||
let yoffset = (py as Float + 0.5) * self.pixel_size;
|
||||
|
||||
// The untransformed coordinates of the pixel in world space.
|
||||
// (Remember that the camera looks toward -z, so +x is to the left.)
|
||||
let world_x = self.half_width - xoffset;
|
||||
let world_y = self.half_height - yoffset;
|
||||
|
||||
// Using the camera matrix, transform the canvas point and the origin,
|
||||
// and then compute the ray's direction vector.
|
||||
// (Remember that the canvas is at z>=-1).
|
||||
let pixel = self.inverse_transform * Tuple::point(world_x, world_y, -1.);
|
||||
let origin = self.inverse_transform * Tuple::point(0., 0., 0.);
|
||||
let direction = (pixel - origin).normalize();
|
||||
|
||||
Ray::new(origin, direction)
|
||||
}
|
||||
|
||||
/// Use camera to render an image of the given world.
|
||||
pub fn render(&self, w: &World) -> Canvas {
|
||||
use RenderStrategy::*;
|
||||
|
||||
match self.render_strategy {
|
||||
Serial => self.render_serial(w),
|
||||
Rayon => self.render_parallel_rayon(w),
|
||||
WorkerPool => self.render_parallel_one_tread_per_core(w),
|
||||
}
|
||||
}
|
||||
|
||||
/// This render function spins up one thread per core, and pins the thread
|
||||
/// to the core. It then sends work requests to the worker threads,
|
||||
/// requesting a full line of the image by rendered. The main thread
|
||||
/// collects results and stores them in the canvas returned to the user.
|
||||
fn render_parallel_one_tread_per_core(&self, world: &World) -> Canvas {
|
||||
let mut image = Canvas::new(self.hsize, self.vsize, BLACK);
|
||||
let num_threads = num_cpus::get();
|
||||
let (pixel_req_tx, pixel_req_rx) = sync_channel(2 * num_threads);
|
||||
let (pixel_resp_tx, pixel_resp_rx) = sync_channel(2 * num_threads);
|
||||
let pixel_req_rx = Arc::new(Mutex::new(pixel_req_rx));
|
||||
|
||||
// Create copy of world and camera we can share with all workers.
|
||||
// It's probably okay to clone camera, but world could get large (think
|
||||
// textures and high poly count models).
|
||||
// TODO(wathiede): prevent second copy of world when they start getting
|
||||
// large.
|
||||
let world = Arc::new(world.clone());
|
||||
let camera = Arc::new(self.clone());
|
||||
|
||||
let core_ids = core_affinity::get_core_ids().unwrap();
|
||||
println!("Creating {} render threads", core_ids.len());
|
||||
// Create a worker thread for each CPU core and pin the thread to the core.
|
||||
let mut handles = core_ids
|
||||
.into_iter()
|
||||
.map(|id| {
|
||||
let w = Arc::clone(&world);
|
||||
let c = Arc::clone(&camera);
|
||||
let pixel_req_rx = pixel_req_rx.clone();
|
||||
let pixel_resp_tx = pixel_resp_tx.clone();
|
||||
thread::spawn(move || {
|
||||
core_affinity::set_for_current(id);
|
||||
render_worker_task(&c, &w, pixel_req_rx, &pixel_resp_tx);
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
drop(pixel_req_rx);
|
||||
drop(pixel_resp_tx);
|
||||
|
||||
// Send render requests over channels to worker threads.
|
||||
let (w, h) = (camera.hsize, camera.vsize);
|
||||
handles.push(thread::spawn(move || {
|
||||
for y in 0..h {
|
||||
pixel_req_tx
|
||||
.send(Request::Line { width: w, y })
|
||||
.expect("failed to send line request");
|
||||
}
|
||||
drop(pixel_req_tx);
|
||||
}));
|
||||
|
||||
// Read responses from channel and blit image data.
|
||||
for resp in pixel_resp_rx {
|
||||
match resp {
|
||||
Response::Line { y, pixels } => {
|
||||
for x in 0..camera.hsize {
|
||||
image.set(x, y, pixels.get(x, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for all the threads to exit.
|
||||
for thr in handles {
|
||||
thr.join().expect("thread join");
|
||||
}
|
||||
image
|
||||
}
|
||||
|
||||
/// This renderer use rayon to split each row into a seperate thread. It
|
||||
/// seems to have more consistent performance than worker pool, equally fast
|
||||
/// as WP at WP's fastest. The downside is the flame graph looks a mess. A
|
||||
/// strength over `render_parallel_one_tread_per_core` is that it doesn't
|
||||
/// require `Camera` and `World` to be cloneable.
|
||||
fn render_parallel_rayon(&self, w: &World) -> Canvas {
|
||||
let image_mu = Mutex::new(Canvas::new(self.hsize, self.vsize, BLACK));
|
||||
|
||||
(0..self.vsize).into_par_iter().for_each(|y| {
|
||||
let mut row_image = Canvas::new(self.hsize, 1, BLACK);
|
||||
for x in 0..self.hsize {
|
||||
let color = self.sample(w, x, y);
|
||||
row_image.set(x, 0, color);
|
||||
}
|
||||
// TODO(wathiede): create a row based setter for memcpying the row as a whole.
|
||||
let mut image = image_mu.lock().expect("failed to lock image mutex");
|
||||
for x in 0..self.hsize {
|
||||
image.set(x, y, row_image.get(x, 0));
|
||||
}
|
||||
});
|
||||
image_mu
|
||||
.into_inner()
|
||||
.expect("failed to get image out of mutex")
|
||||
}
|
||||
|
||||
/// Reference render implementation from the book. Single threaded, nothing fancy.
|
||||
fn render_serial(&self, w: &World) -> Canvas {
|
||||
let mut image = Canvas::new(self.hsize, self.vsize, BLACK);
|
||||
for y in 0..self.vsize {
|
||||
for x in 0..self.hsize {
|
||||
let color = self.sample(w, x, y);
|
||||
image.set(x, y, color);
|
||||
}
|
||||
}
|
||||
image
|
||||
}
|
||||
|
||||
fn sample(&self, w: &World, x: usize, y: usize) -> Color {
|
||||
if self.samples_per_pixel > 0 {
|
||||
let color = self
|
||||
.supersample_rays_for_pixel(x, y, self.samples_per_pixel)
|
||||
.iter()
|
||||
.map(|ray| w.color_at(ray, MAX_DEPTH_RECURSION))
|
||||
.fold(BLACK, |acc, c| acc + c);
|
||||
color / self.samples_per_pixel as Float
|
||||
} else {
|
||||
let ray = self.ray_for_pixel(x, y);
|
||||
w.color_at(&ray, MAX_DEPTH_RECURSION)
|
||||
}
|
||||
}
|
||||
}
|
||||
fn render_worker_task(
|
||||
c: &Camera,
|
||||
w: &World,
|
||||
input_chan: Arc<Mutex<Receiver<Request>>>,
|
||||
output_chan: &SyncSender<Response>,
|
||||
) {
|
||||
loop {
|
||||
let job = { input_chan.lock().unwrap().recv() };
|
||||
match job {
|
||||
Err(_) => {
|
||||
// From the docs:
|
||||
// "The recv operation can only fail if the sending half of a
|
||||
// channel (or sync_channel) is disconnected, implying that no
|
||||
// further messages will ever be received."
|
||||
return;
|
||||
}
|
||||
Ok(req) => match req {
|
||||
Request::Line { width, y } => {
|
||||
let mut pixels = Canvas::new(width, 1, BLACK);
|
||||
for x in 0..width {
|
||||
let color = c.sample(w, x, y);
|
||||
pixels.set(x, 0, color);
|
||||
}
|
||||
output_chan
|
||||
.send(Response::Line { y, pixels })
|
||||
.expect("failed to send pixel response");
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
camera::Camera,
|
||||
float::consts::PI,
|
||||
matrices::Matrix4x4,
|
||||
transformations::view_transform,
|
||||
tuples::{point, vector},
|
||||
world::World,
|
||||
Float, EPSILON,
|
||||
};
|
||||
#[test]
|
||||
fn new() {
|
||||
let hsize = 160;
|
||||
let vsize = 120;
|
||||
let field_of_view = PI / 2.;
|
||||
let c = Camera::new(hsize, vsize, field_of_view);
|
||||
assert_eq!(c.hsize(), 160);
|
||||
assert_eq!(c.vsize(), 120);
|
||||
assert_eq!(c.transform(), Matrix4x4::identity());
|
||||
|
||||
// Pixel size for a horizontal canvas.
|
||||
let c = Camera::new(200, 150, PI / 2.);
|
||||
assert!((c.pixel_size() - 0.010).abs() < EPSILON);
|
||||
|
||||
// Pixel size for a horizontal canvas.
|
||||
let c = Camera::new(150, 200, PI / 2.);
|
||||
assert!((c.pixel_size() - 0.010).abs() < EPSILON);
|
||||
}
|
||||
#[test]
|
||||
fn ray_for_pixel() {
|
||||
// Constructing a ray through the center of the canvas.
|
||||
let c = Camera::new(201, 101, PI / 2.);
|
||||
let r = c.ray_for_pixel(100, 50);
|
||||
assert_eq!(r.origin, point(0., 0., 0.));
|
||||
assert_eq!(r.direction, vector(0., 0., -1.));
|
||||
|
||||
// Constructing a ray through the corner of the canvas.
|
||||
let c = Camera::new(201, 101, PI / 2.);
|
||||
let r = c.ray_for_pixel(0, 0);
|
||||
assert_eq!(r.origin, point(0., 0., 0.));
|
||||
assert_eq!(r.direction, vector(0.66519, 0.33259, -0.66851));
|
||||
|
||||
// Constructing a ray when the camera is transformed.
|
||||
let mut c = Camera::new(201, 101, PI / 2.);
|
||||
c.set_transform(Matrix4x4::rotation_y(PI / 4.) * Matrix4x4::translation(0., -2., 5.));
|
||||
let r = c.ray_for_pixel(100, 50);
|
||||
assert_eq!(r.origin, point(0., 2., -5.));
|
||||
assert_eq!(
|
||||
r.direction,
|
||||
vector((2. as Float).sqrt() / 2., 0., -(2. as Float).sqrt() / 2.)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn render() {
|
||||
// Rendering a world with a camera.
|
||||
let w = World::test_world();
|
||||
let mut c = Camera::new(11, 11, PI / 2.);
|
||||
let from = point(0., 0., -5.);
|
||||
let to = point(0., 0., 0.);
|
||||
let up = vector(0., 1., 0.);
|
||||
c.set_transform(view_transform(from, to, up));
|
||||
let image = c.render(&w);
|
||||
assert_eq!(image.get(5, 5), [0.38066, 0.47583, 0.2855].into());
|
||||
}
|
||||
}
|
||||
96
rtchallenge/src/canvas.rs
Normal file
96
rtchallenge/src/canvas.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
use std::{fs::File, io::BufWriter, path::Path};
|
||||
|
||||
use png;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::tuples::Color;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum CanvasError {
|
||||
#[error("faile to write canvas")]
|
||||
IOError(#[from] std::io::Error),
|
||||
#[error("faile to encode canvas to png")]
|
||||
EncodingError(#[from] png::EncodingError),
|
||||
}
|
||||
pub struct Canvas {
|
||||
pub height: usize,
|
||||
pub width: usize,
|
||||
pub pixels: Vec<Color>,
|
||||
}
|
||||
|
||||
impl Canvas {
|
||||
pub fn new(width: usize, height: usize, background: Color) -> Canvas {
|
||||
let pixels = vec![background; width * height];
|
||||
Canvas {
|
||||
width,
|
||||
height,
|
||||
pixels,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set(&mut self, x: usize, y: usize, c: Color) {
|
||||
if x > self.width {
|
||||
return;
|
||||
}
|
||||
if y > self.height {
|
||||
return;
|
||||
}
|
||||
self.pixels[x + y * self.width] = c;
|
||||
}
|
||||
pub fn get(&self, x: usize, y: usize) -> Color {
|
||||
self.pixels[x + y * self.width]
|
||||
}
|
||||
pub fn write_to_file<P>(&self, path: P) -> Result<(), CanvasError>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let path = Path::new(path.as_ref());
|
||||
let file = File::create(path)?;
|
||||
let w = &mut BufWriter::new(file);
|
||||
|
||||
let mut encoder = png::Encoder::new(w, self.width as u32, self.height as u32);
|
||||
encoder.set_color(png::ColorType::RGB);
|
||||
encoder.set_depth(png::BitDepth::Eight);
|
||||
let mut writer = encoder.write_header()?;
|
||||
|
||||
let data: Vec<u8> = self
|
||||
.pixels
|
||||
.iter()
|
||||
.flat_map(|p| {
|
||||
vec![
|
||||
(p.red.clamp(0., 1.) * 255.) as u8,
|
||||
(p.green.clamp(0., 1.) * 255.) as u8,
|
||||
(p.blue.clamp(0., 1.) * 255.) as u8,
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
writer.write_image_data(&data)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Canvas;
|
||||
|
||||
use crate::{tuples::Color, BLACK};
|
||||
|
||||
#[test]
|
||||
fn create_canvas() {
|
||||
let bg = BLACK;
|
||||
let c = Canvas::new(10, 20, bg);
|
||||
assert_eq!(c.width, 10);
|
||||
assert_eq!(c.height, 20);
|
||||
for (i, p) in c.pixels.iter().enumerate() {
|
||||
assert_eq!(p, &BLACK, "pixel {} not {:?}: {:?}", i, &BLACK, p);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_to_canvas() {
|
||||
let bg = Color::new(0.2, 0.2, 0.2);
|
||||
let mut c = Canvas::new(10, 20, bg);
|
||||
let red = Color::new(1., 0., 0.);
|
||||
c.set(2, 3, red);
|
||||
assert_eq!(c.get(2, 3), red);
|
||||
}
|
||||
}
|
||||
433
rtchallenge/src/intersections.rs
Normal file
433
rtchallenge/src/intersections.rs
Normal file
@@ -0,0 +1,433 @@
|
||||
use std::ops::Index;
|
||||
|
||||
use crate::{
|
||||
rays::Ray,
|
||||
shapes::Shape,
|
||||
tuples::{dot, reflect, Tuple},
|
||||
Float, EPSILON,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Intersection<'i> {
|
||||
pub t: Float,
|
||||
pub object: &'i Shape,
|
||||
}
|
||||
impl<'i> PartialEq for Intersection<'i> {
|
||||
fn eq(&self, rhs: &Intersection) -> bool {
|
||||
((self.t - rhs.t).abs() < EPSILON) && (self.object == rhs.object)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'i> Intersection<'i> {
|
||||
/// Create new `Intersection` at the given `t` that hits the given `object`.
|
||||
pub fn new(t: Float, object: &Shape) -> Intersection {
|
||||
Intersection { t, object }
|
||||
}
|
||||
}
|
||||
|
||||
/// Aggregates `Intersection`s.
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct Intersections<'i>(Vec<Intersection<'i>>);
|
||||
|
||||
/// Create an [Intersections] from a single [Intersection] to aid in tests.
|
||||
impl<'i> From<Intersection<'i>> for Intersections<'i> {
|
||||
fn from(i: Intersection<'i>) -> Intersections<'i> {
|
||||
Intersections::new(vec![i])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'i> Intersections<'i> {
|
||||
pub fn new(xs: Vec<Intersection<'i>>) -> Intersections {
|
||||
Intersections(xs)
|
||||
}
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
/// Finds nearest hit for this collection of intersections.
|
||||
pub fn hit(&self) -> Option<&Intersection> {
|
||||
self.0.iter().filter(|i| i.t > 0.).min_by(|i1, i2| {
|
||||
i1.t.partial_cmp(&i2.t)
|
||||
.expect("an intersection has a t value that is NaN")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'i> IntoIterator for Intersections<'i> {
|
||||
type Item = Intersection<'i>;
|
||||
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'i> Index<usize> for Intersections<'i> {
|
||||
type Output = Intersection<'i>;
|
||||
fn index(&self, idx: usize) -> &Self::Output {
|
||||
&self.0[idx]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PrecomputedData<'i> {
|
||||
pub t: Float,
|
||||
pub object: &'i Shape,
|
||||
pub point: Tuple,
|
||||
pub under_point: Tuple,
|
||||
pub over_point: Tuple,
|
||||
pub eyev: Tuple,
|
||||
pub normalv: Tuple,
|
||||
pub reflectv: Tuple,
|
||||
pub inside: bool,
|
||||
pub n1: Float,
|
||||
pub n2: Float,
|
||||
}
|
||||
|
||||
/// Precomputes data common to all intersections.
|
||||
pub fn prepare_computations<'i>(
|
||||
hit: &'i Intersection,
|
||||
r: &Ray,
|
||||
xs: &Intersections,
|
||||
) -> PrecomputedData<'i> {
|
||||
let point = r.position(hit.t);
|
||||
let normalv = hit.object.normal_at(point);
|
||||
let eyev = -r.direction;
|
||||
let (inside, normalv) = if dot(normalv, eyev) < 0. {
|
||||
(true, -normalv)
|
||||
} else {
|
||||
(false, normalv)
|
||||
};
|
||||
let reflectv = reflect(r.direction, normalv);
|
||||
|
||||
let mut n1 = -1.;
|
||||
let mut n2 = -1.;
|
||||
let mut containers: Vec<&Shape> = Vec::new();
|
||||
for i in xs.0.iter() {
|
||||
if hit == i {
|
||||
if containers.is_empty() {
|
||||
n1 = 1.;
|
||||
} else {
|
||||
n1 = containers.last().unwrap().material.refractive_index;
|
||||
}
|
||||
}
|
||||
|
||||
match containers.iter().position(|o| o == &i.object) {
|
||||
Some(idx) => {
|
||||
containers.remove(idx);
|
||||
}
|
||||
None => containers.push(i.object),
|
||||
}
|
||||
|
||||
if hit == i {
|
||||
if containers.is_empty() {
|
||||
n2 = 1.;
|
||||
} else {
|
||||
n2 = containers.last().unwrap().material.refractive_index;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let over_point = point + normalv * EPSILON;
|
||||
let under_point = point - normalv * EPSILON;
|
||||
PrecomputedData {
|
||||
t: hit.t,
|
||||
object: hit.object,
|
||||
point,
|
||||
over_point,
|
||||
under_point,
|
||||
normalv,
|
||||
reflectv,
|
||||
inside,
|
||||
eyev,
|
||||
n1,
|
||||
n2,
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute Schlick approximation to Fresnel's equations.
|
||||
pub fn schlick(comps: &PrecomputedData) -> Float {
|
||||
// Find the cosine of the angle between the eye and normal vectors.
|
||||
let mut cos = dot(comps.eyev, comps.normalv);
|
||||
// Total internal reflection can only occur if n1 > n2.
|
||||
if comps.n1 > comps.n2 {
|
||||
let n = comps.n1 / comps.n2;
|
||||
let sin2_t = n * n * (1. - cos * cos);
|
||||
if sin2_t > 1. {
|
||||
return 1.;
|
||||
}
|
||||
// Compute cosine of theta_t using trig identity.
|
||||
let cos_t = (1. - sin2_t).sqrt();
|
||||
// When n1 > n2 use cos(theta_t) instead.
|
||||
cos = cos_t;
|
||||
}
|
||||
|
||||
let r0 = (comps.n1 - comps.n2) / (comps.n1 + comps.n2);
|
||||
let r0 = r0 * r0;
|
||||
r0 + (1. - r0) * (1. - cos) * (1. - cos) * (1. - cos) * (1. - cos) * (1. - cos)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
intersections::{prepare_computations, schlick, Intersection, Intersections},
|
||||
materials::MaterialBuilder,
|
||||
matrices::{scaling, translation},
|
||||
rays::Ray,
|
||||
shapes::{glass_sphere, intersect, Shape},
|
||||
tuples::{point, vector},
|
||||
Float, EPSILON,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn intersection() {
|
||||
// An intersection ecapsulates t and object.
|
||||
let s = Shape::sphere();
|
||||
let i = Intersection::new(3.5, &s);
|
||||
assert_eq!(i.t, 3.5);
|
||||
assert_eq!(i.object, &s);
|
||||
}
|
||||
#[test]
|
||||
fn intersections() {
|
||||
let s = Shape::sphere();
|
||||
let i1 = Intersection::new(1., &s);
|
||||
let i2 = Intersection::new(2., &s);
|
||||
let xs = Intersections::new(vec![i1, i2]);
|
||||
assert_eq!(xs.len(), 2);
|
||||
assert_eq!(xs[0].t, 1.);
|
||||
assert_eq!(xs[1].t, 2.);
|
||||
|
||||
let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.));
|
||||
let xs = intersect(&s, &r);
|
||||
assert_eq!(xs.len(), 2);
|
||||
assert_eq!(xs[0].object, &s);
|
||||
assert_eq!(xs[1].object, &s);
|
||||
}
|
||||
|
||||
mod hit {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn when_all_intersections_have_positive_t() {
|
||||
let s = Shape::sphere();
|
||||
let i1 = Intersection::new(1., &s);
|
||||
let i2 = Intersection::new(2., &s);
|
||||
let xs = Intersections::new(vec![i2, i1.clone()]);
|
||||
let i = xs.hit();
|
||||
assert_eq!(i, Some(&i1));
|
||||
}
|
||||
#[test]
|
||||
fn when_some_intersections_have_negative_t() {
|
||||
let s = Shape::sphere();
|
||||
let i1 = Intersection::new(-1., &s);
|
||||
let i2 = Intersection::new(1., &s);
|
||||
let xs = Intersections::new(vec![i2.clone(), i1]);
|
||||
let i = xs.hit();
|
||||
assert_eq!(i, Some(&i2));
|
||||
}
|
||||
#[test]
|
||||
fn when_all_intersections_have_negative_t() {
|
||||
let s = Shape::sphere();
|
||||
let i1 = Intersection::new(-2., &s);
|
||||
let i2 = Intersection::new(-1., &s);
|
||||
let xs = Intersections::new(vec![i2, i1]);
|
||||
let i = xs.hit();
|
||||
assert_eq!(i, None);
|
||||
}
|
||||
#[test]
|
||||
fn always_the_lowest_nonnegative_intersection() {
|
||||
let s = Shape::sphere();
|
||||
let i1 = Intersection::new(5., &s);
|
||||
let i2 = Intersection::new(7., &s);
|
||||
let i3 = Intersection::new(-3., &s);
|
||||
let i4 = Intersection::new(2., &s);
|
||||
let xs = Intersections::new(vec![i1, i2, i3, i4.clone()]);
|
||||
let i = xs.hit();
|
||||
assert_eq!(i, Some(&i4));
|
||||
}
|
||||
}
|
||||
|
||||
mod prepare_computations {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn precomputing_the_state_of_intersection() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.));
|
||||
let shape = Shape::sphere();
|
||||
let xs = Intersections::from(Intersection::new(4., &shape));
|
||||
let comps = prepare_computations(&xs[0], &r, &xs);
|
||||
assert_eq!(comps.t, xs[0].t);
|
||||
assert_eq!(comps.object, xs[0].object);
|
||||
assert_eq!(comps.point, point(0., 0., -1.));
|
||||
assert_eq!(comps.eyev, vector(0., 0., -1.));
|
||||
assert_eq!(comps.normalv, vector(0., 0., -1.));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hit_when_intersection_occurs_outside() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// The hit, when an intersection occurs on the outside.
|
||||
let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.));
|
||||
let shape = Shape::sphere();
|
||||
let xs = Intersections::from(Intersection::new(4., &shape));
|
||||
let comps = prepare_computations(&xs[0], &r, &xs);
|
||||
assert_eq!(comps.inside, false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hit_when_intersection_occurs_inside() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// The hit, when an intersection occurs on the inside.
|
||||
let r = Ray::new(point(0., 0., 0.), vector(0., 0., 1.));
|
||||
let shape = Shape::sphere();
|
||||
let xs = Intersections::from(Intersection::new(1., &shape));
|
||||
let comps = prepare_computations(&xs[0], &r, &xs);
|
||||
assert_eq!(comps.point, point(0., 0., 1.));
|
||||
assert_eq!(comps.eyev, vector(0., 0., -1.));
|
||||
assert_eq!(comps.inside, true);
|
||||
// Normal would have been (0, 0, 1), but is inverted when inside.
|
||||
assert_eq!(comps.normalv, vector(0., 0., -1.));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hit_should_offset_the_point() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// The hit should offset the point.
|
||||
let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.));
|
||||
let mut shape = Shape::sphere();
|
||||
shape.set_transform(translation(0., 0., 1.));
|
||||
let xs = Intersections::from(Intersection::new(5., &shape));
|
||||
let comps = prepare_computations(&xs[0], &r, &xs);
|
||||
assert!(comps.over_point.z < -EPSILON / 2.);
|
||||
assert!(comps.point.z > comps.over_point.z);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn precomputing_the_reflection_vector() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Precomputing the reflection vector.
|
||||
let shape = Shape::plane();
|
||||
let r = Ray::new(
|
||||
point(0., 1., -1.),
|
||||
vector(0., -(2 as Float).sqrt() / 2., (2 as Float).sqrt() / 2.),
|
||||
);
|
||||
let xs = Intersections::from(Intersection::new((2 as Float).sqrt(), &shape));
|
||||
let comps = prepare_computations(&xs[0], &r, &xs);
|
||||
assert_eq!(
|
||||
comps.reflectv,
|
||||
vector(0., (2 as Float).sqrt() / 2., (2 as Float).sqrt() / 2.)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finding_n1_and_n2() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Finding n1 and n2 at various intersections.
|
||||
let a = glass_sphere()
|
||||
.transform(scaling(2., 2., 2.))
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.transparency(1.)
|
||||
.refractive_index(1.5)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
let b = glass_sphere()
|
||||
.transform(translation(0., 0., -0.25))
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.transparency(1.)
|
||||
.refractive_index(2.)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
let c = glass_sphere()
|
||||
.transform(translation(0., 0., 0.25))
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.transparency(1.)
|
||||
.refractive_index(2.5)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
let r = Ray::new(point(0., 0., -4.), vector(0., 0., 1.));
|
||||
let xs = Intersections::new(vec![
|
||||
Intersection::new(2., &a),
|
||||
Intersection::new(2.75, &b),
|
||||
Intersection::new(3.25, &c),
|
||||
Intersection::new(4.75, &b),
|
||||
Intersection::new(5.25, &c),
|
||||
Intersection::new(6., &a),
|
||||
]);
|
||||
for (index, n1, n2) in &[
|
||||
(0, 1.0, 1.5),
|
||||
(1, 1.5, 2.0),
|
||||
(2, 2.0, 2.5),
|
||||
(3, 2.5, 2.5),
|
||||
(4, 2.5, 1.5),
|
||||
(5, 1.5, 1.0),
|
||||
] {
|
||||
let comps = prepare_computations(&xs[*index], &r, &xs);
|
||||
assert_eq!(comps.n1, *n1);
|
||||
assert_eq!(comps.n2, *n2);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn under_point_offset_below_surface() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// The under point is offset below the surface.
|
||||
let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.));
|
||||
let shape = glass_sphere().transform(translation(0., 0., 1.)).build()?;
|
||||
let xs = Intersections::from(Intersection::new(5., &shape));
|
||||
let comps = prepare_computations(&xs[0], &r, &xs);
|
||||
assert!(comps.under_point.z > EPSILON / 2.);
|
||||
assert!(comps.point.z < comps.under_point.z);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
mod schlick {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn under_total_reflection() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let shape = glass_sphere().build()?;
|
||||
let r = Ray::new(point(0., 0., (2 as Float).sqrt() / 2.), vector(0., 1., 0.));
|
||||
let xs = Intersections::new(vec![
|
||||
Intersection::new(-(2 as Float).sqrt() / 2., &shape),
|
||||
Intersection::new((2 as Float).sqrt() / 2., &shape),
|
||||
]);
|
||||
let comps = prepare_computations(&xs[1], &r, &xs);
|
||||
let reflectance = schlick(&comps);
|
||||
assert_eq!(reflectance, 1.);
|
||||
Ok(())
|
||||
}
|
||||
#[test]
|
||||
fn perpendicular_viewing_angle() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let shape = glass_sphere().build()?;
|
||||
let r = Ray::new(point(0., 0., 0.), vector(0., 1., 0.));
|
||||
let xs = Intersections::new(vec![
|
||||
Intersection::new(-1., &shape),
|
||||
Intersection::new(1., &shape),
|
||||
]);
|
||||
let comps = prepare_computations(&xs[1], &r, &xs);
|
||||
let reflectance = schlick(&comps);
|
||||
assert!((reflectance - 0.04).abs() < EPSILON);
|
||||
Ok(())
|
||||
}
|
||||
#[test]
|
||||
fn small_angle_n2_greater_n1() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let shape = glass_sphere().build()?;
|
||||
let r = Ray::new(point(0., 0.99, -2.), vector(0., 0., 1.));
|
||||
let xs = Intersections::new(vec![Intersection::new(1.8589, &shape)]);
|
||||
let comps = prepare_computations(&xs[0], &r, &xs);
|
||||
let reflectance = schlick(&comps);
|
||||
assert!((reflectance - 0.48873).abs() < EPSILON);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
55
rtchallenge/src/lib.rs
Normal file
55
rtchallenge/src/lib.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
pub mod camera;
|
||||
pub mod canvas;
|
||||
pub mod intersections;
|
||||
pub mod lights;
|
||||
pub mod materials;
|
||||
pub mod matrices;
|
||||
pub mod patterns;
|
||||
pub mod rays;
|
||||
pub mod shapes;
|
||||
pub mod transformations;
|
||||
pub mod tuples;
|
||||
pub mod world;
|
||||
|
||||
/// Value considered close enough for PartialEq implementations.
|
||||
pub const EPSILON: Float = 0.00001;
|
||||
|
||||
pub const BLACK: tuples::Color = tuples::Color::new(0., 0., 0.);
|
||||
pub const WHITE: tuples::Color = tuples::Color::new(1., 1., 1.);
|
||||
|
||||
#[cfg(feature = "float-as-double")]
|
||||
/// submodule to defined types, constants and methods when `Float` is defined as a `f64` using the
|
||||
/// "float-as-double" cargo feature.
|
||||
pub mod float {
|
||||
pub use std::f64::*;
|
||||
/// Alias of the `f64` type, to be used through out the codebase anywhere a default sized
|
||||
/// `Float` is necessary.
|
||||
pub type Float = f64;
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "float-as-double"))]
|
||||
/// submodule to defined types, constants and methods when `Float` is defined as a `f32` when not using the
|
||||
/// "float-as-double" cargo feature.
|
||||
pub mod float {
|
||||
pub use std::f32::*;
|
||||
/// Alias of the `f32` type, to be used through out the codebase anywhere a default sized
|
||||
/// `Float` is necessary.
|
||||
pub type Float = f32;
|
||||
}
|
||||
|
||||
pub use float::Float;
|
||||
|
||||
pub mod prelude {
|
||||
pub use crate::{
|
||||
camera::{Camera, CameraBuilder},
|
||||
lights::{PointLight, PointLightBuilder},
|
||||
materials::{Material, MaterialBuilder},
|
||||
matrices::{identity, rotation_x, rotation_y, rotation_z, scaling, shearing, translation},
|
||||
patterns::{checkers_pattern, gradient_pattern, ring_pattern, stripe_pattern},
|
||||
shapes::{cube, plane, sphere, test_shape},
|
||||
transformations::view_transform,
|
||||
tuples::{point, vector, Color},
|
||||
world::{World, WorldBuilder},
|
||||
Float,
|
||||
};
|
||||
}
|
||||
39
rtchallenge/src/lights.rs
Normal file
39
rtchallenge/src/lights.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use derive_builder::Builder;
|
||||
|
||||
use crate::tuples::{Color, Tuple};
|
||||
|
||||
#[derive(Builder, Clone, Debug, Default, PartialEq)]
|
||||
#[builder(default)]
|
||||
pub struct PointLight {
|
||||
pub position: Tuple,
|
||||
#[builder(setter(into))]
|
||||
pub intensity: Color,
|
||||
}
|
||||
|
||||
impl PointLight {
|
||||
/// Creates a new `PositionLight` at the given `position` and with the given
|
||||
/// `intensity`.
|
||||
pub fn new<C>(position: Tuple, intensity: C) -> PointLight
|
||||
where
|
||||
C: Into<Color>,
|
||||
{
|
||||
PointLight {
|
||||
position,
|
||||
intensity: intensity.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{lights::PointLight, tuples::point, WHITE};
|
||||
|
||||
#[test]
|
||||
fn new() {
|
||||
let intensity = WHITE;
|
||||
let position = point(0., 0., 0.);
|
||||
let light = PointLight::new(position, intensity);
|
||||
assert_eq!(light.position, position);
|
||||
assert_eq!(light.intensity, intensity);
|
||||
}
|
||||
}
|
||||
3
rtchallenge/src/main.rs
Normal file
3
rtchallenge/src/main.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
236
rtchallenge/src/materials.rs
Normal file
236
rtchallenge/src/materials.rs
Normal file
@@ -0,0 +1,236 @@
|
||||
use derive_builder::Builder;
|
||||
|
||||
use crate::{
|
||||
lights::PointLight,
|
||||
patterns::Pattern,
|
||||
shapes::Shape,
|
||||
tuples::{dot, reflect, Color, Tuple},
|
||||
Float, BLACK, WHITE,
|
||||
};
|
||||
|
||||
#[derive(Builder, Debug, PartialEq, Clone)]
|
||||
#[builder(default)]
|
||||
pub struct Material {
|
||||
#[builder(setter(into))]
|
||||
pub color: Pattern,
|
||||
pub ambient: Float,
|
||||
pub diffuse: Float,
|
||||
pub specular: Float,
|
||||
pub shininess: Float,
|
||||
pub reflective: Float,
|
||||
pub transparency: Float,
|
||||
pub refractive_index: Float,
|
||||
}
|
||||
|
||||
impl Default for Material {
|
||||
/// Creates the default material.
|
||||
fn default() -> Material {
|
||||
Material {
|
||||
color: WHITE.into(),
|
||||
ambient: 0.1,
|
||||
diffuse: 0.9,
|
||||
specular: 0.9,
|
||||
shininess: 200.,
|
||||
reflective: 0.0,
|
||||
transparency: 0.0,
|
||||
refractive_index: 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute lighting contributions using the Phong reflection model.
|
||||
pub fn lighting(
|
||||
material: &Material,
|
||||
object: &Shape,
|
||||
light: &PointLight,
|
||||
point: Tuple,
|
||||
eyev: Tuple,
|
||||
normalv: Tuple,
|
||||
in_shadow: bool,
|
||||
) -> Color {
|
||||
// Combine the surface color with the light's color.
|
||||
let color = material.color.pattern_at_object(object, point);
|
||||
let effective_color = color * light.intensity;
|
||||
// Find the direciton of the light source.
|
||||
let lightv = (light.position - point).normalize();
|
||||
// Compute the ambient distribution.
|
||||
let ambient = effective_color * material.ambient;
|
||||
// This is the cosine of the angle between the light vector an the normal
|
||||
// vector. A negative number means the light is on the other side of the
|
||||
// surface.
|
||||
let light_dot_normal = dot(lightv, normalv);
|
||||
let (diffuse, specular) = if light_dot_normal < 0. {
|
||||
(BLACK, BLACK)
|
||||
} else {
|
||||
// Compute the diffuse contribution.
|
||||
let diffuse = effective_color * material.diffuse * light_dot_normal;
|
||||
// This represents the cosine of the angle between the relfection vector
|
||||
// and the eye vector. A negative number means the light reflects away
|
||||
// from the eye.
|
||||
let reflectv = reflect(-lightv, normalv);
|
||||
let reflect_dot_eye = dot(reflectv, eyev);
|
||||
let specular = if reflect_dot_eye <= 0. {
|
||||
BLACK
|
||||
} else {
|
||||
// Compute the specular contribution.
|
||||
let factor = reflect_dot_eye.powf(material.shininess);
|
||||
light.intensity * material.specular * factor
|
||||
};
|
||||
(diffuse, specular)
|
||||
};
|
||||
if in_shadow {
|
||||
ambient
|
||||
} else {
|
||||
ambient + diffuse + specular
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{materials::Material, WHITE};
|
||||
|
||||
#[test]
|
||||
fn default() {
|
||||
let m = Material::default();
|
||||
assert_eq!(
|
||||
m,
|
||||
Material {
|
||||
color: WHITE.into(),
|
||||
ambient: 0.1,
|
||||
diffuse: 0.9,
|
||||
specular: 0.9,
|
||||
shininess: 200.,
|
||||
reflective: 0.0,
|
||||
transparency: 0.0,
|
||||
refractive_index: 1.0,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
mod lighting {
|
||||
use crate::{
|
||||
lights::PointLight,
|
||||
materials::{lighting, Material},
|
||||
patterns::{Pattern, BLACK_PAT, WHITE_PAT},
|
||||
shapes::Shape,
|
||||
tuples::{point, vector, Color},
|
||||
Float, BLACK, WHITE,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn eye_between_light_and_surface() {
|
||||
let in_shadow = false;
|
||||
let m = Material::default();
|
||||
let position = point(0., 0., 0.);
|
||||
let object = Shape::sphere();
|
||||
|
||||
// Lighting with the eye between the light and the surface.
|
||||
let eyev = vector(0., 0., -1.);
|
||||
let normalv = vector(0., 0., -1.);
|
||||
let light = PointLight::new(point(0., 0., -10.), WHITE);
|
||||
let result = lighting(&m, &object, &light, position, eyev, normalv, in_shadow);
|
||||
assert_eq!(result, Color::new(1.9, 1.9, 1.9));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eye_between_light_and_surface_offset_45() {
|
||||
// Lighting with the eye between the light and the surface, eye offset 45°.
|
||||
let in_shadow = false;
|
||||
let m = Material::default();
|
||||
let position = point(0., 0., 0.);
|
||||
let object = Shape::sphere();
|
||||
let eyev = vector(0., (2. as Float).sqrt() / 2., -(2. as Float).sqrt() / 2.);
|
||||
let normalv = vector(0., 0., -1.);
|
||||
let light = PointLight::new(point(0., 0., -10.), WHITE);
|
||||
let result = lighting(&m, &object, &light, position, eyev, normalv, in_shadow);
|
||||
assert_eq!(result, WHITE);
|
||||
}
|
||||
#[test]
|
||||
fn eye_opposite_surface_light_offset_45() {
|
||||
// Lighting with the eye opposite surface, light offset 45°.
|
||||
let in_shadow = false;
|
||||
let m = Material::default();
|
||||
let position = point(0., 0., 0.);
|
||||
let object = Shape::sphere();
|
||||
let eyev = vector(0., 0., -1.);
|
||||
let normalv = vector(0., 0., -1.);
|
||||
let light = PointLight::new(point(0., 10., -10.), WHITE);
|
||||
let result = lighting(&m, &object, &light, position, eyev, normalv, in_shadow);
|
||||
assert_eq!(result, Color::new(0.7364, 0.7364, 0.7364));
|
||||
}
|
||||
#[test]
|
||||
fn eye_in_path_of_reflection_vector() {
|
||||
// Lighting with the eye in the path of the reflection vector.
|
||||
let in_shadow = false;
|
||||
let m = Material::default();
|
||||
let position = point(0., 0., 0.);
|
||||
let object = Shape::sphere();
|
||||
let eyev = vector(0., -(2.0 as Float).sqrt() / 2., -(2.0 as Float).sqrt() / 2.);
|
||||
let normalv = vector(0., 0., -1.);
|
||||
let light = PointLight::new(point(0., 10., -10.), WHITE);
|
||||
let result = lighting(&m, &object, &light, position, eyev, normalv, in_shadow);
|
||||
assert_eq!(result, Color::new(1.63639, 1.63639, 1.63639));
|
||||
}
|
||||
#[test]
|
||||
fn light_behind_surface() {
|
||||
// Lighting with the light behind the surface.
|
||||
let in_shadow = false;
|
||||
let m = Material::default();
|
||||
let position = point(0., 0., 0.);
|
||||
let object = Shape::sphere();
|
||||
let eyev = vector(0., 0., -1.);
|
||||
let normalv = vector(0., 0., -1.);
|
||||
let light = PointLight::new(point(0., 0., 10.), WHITE);
|
||||
let result = lighting(&m, &object, &light, position, eyev, normalv, in_shadow);
|
||||
assert_eq!(result, Color::new(0.1, 0.1, 0.1));
|
||||
}
|
||||
#[test]
|
||||
fn surface_in_shadow() {
|
||||
// Lighting with the surface in shadow.
|
||||
let m = Material::default();
|
||||
let position = point(0., 0., 0.);
|
||||
let object = Shape::sphere();
|
||||
let in_shadow = true;
|
||||
let eyev = vector(0., 0., -1.);
|
||||
let normalv = vector(0., 0., -1.);
|
||||
let light = PointLight::new(point(0., 0., -10.), WHITE);
|
||||
let result = lighting(&m, &object, &light, position, eyev, normalv, in_shadow);
|
||||
assert_eq!(result, Color::new(0.1, 0.1, 0.1));
|
||||
}
|
||||
#[test]
|
||||
fn pattern_applied() {
|
||||
// Lighting with a pattern applied.
|
||||
let object = Shape::sphere();
|
||||
let m = Material {
|
||||
color: Pattern::stripe(WHITE_PAT, BLACK_PAT),
|
||||
ambient: 1.,
|
||||
diffuse: 0.,
|
||||
specular: 0.,
|
||||
..Material::default()
|
||||
};
|
||||
let eyev = vector(0., 0., -1.);
|
||||
let normalv = vector(0., 0., -1.);
|
||||
let light = PointLight::new(point(0., 0., -10.), WHITE);
|
||||
let c1 = lighting(
|
||||
&m,
|
||||
&object,
|
||||
&light,
|
||||
point(0.9, 0., 0.),
|
||||
eyev,
|
||||
normalv,
|
||||
false,
|
||||
);
|
||||
let c2 = lighting(
|
||||
&m,
|
||||
&object,
|
||||
&light,
|
||||
point(1.1, 0., 0.),
|
||||
eyev,
|
||||
normalv,
|
||||
false,
|
||||
);
|
||||
assert_eq!(c1, WHITE);
|
||||
assert_eq!(c2, BLACK);
|
||||
}
|
||||
}
|
||||
}
|
||||
922
rtchallenge/src/matrices.rs
Normal file
922
rtchallenge/src/matrices.rs
Normal file
@@ -0,0 +1,922 @@
|
||||
use std::{
|
||||
fmt,
|
||||
ops::{Index, IndexMut, Mul, Sub},
|
||||
};
|
||||
|
||||
use crate::{tuples::Tuple, Float, EPSILON};
|
||||
|
||||
/// Short hand for creating a Matrix4x4 set to the identity matrix.
|
||||
pub fn identity() -> Matrix4x4 {
|
||||
Matrix4x4::identity()
|
||||
}
|
||||
/// Short hand for creating a Matrix4x4 for rotating around the X-axis.
|
||||
pub fn rotation_x(radians: Float) -> Matrix4x4 {
|
||||
Matrix4x4::rotation_x(radians)
|
||||
}
|
||||
/// Short hand for creating a Matrix4x4 for rotating around the Y-axis.
|
||||
pub fn rotation_y(radians: Float) -> Matrix4x4 {
|
||||
Matrix4x4::rotation_y(radians)
|
||||
}
|
||||
/// Short hand for creating a Matrix4x4 for rotating around the Z-axis.
|
||||
pub fn rotation_z(radians: Float) -> Matrix4x4 {
|
||||
Matrix4x4::rotation_z(radians)
|
||||
}
|
||||
/// Short hand for creating a Matrix4x4 that scales in the given x,y,z axis.
|
||||
pub fn scaling(x: Float, y: Float, z: Float) -> Matrix4x4 {
|
||||
Matrix4x4::scaling(x, y, z)
|
||||
}
|
||||
/// Short hand for creating a Matrix4x4 that shears across the given axis pairs.
|
||||
pub fn shearing(xy: Float, xz: Float, yx: Float, yz: Float, zx: Float, zy: Float) -> Matrix4x4 {
|
||||
Matrix4x4::shearing(xy, xz, yx, yz, zx, zy)
|
||||
}
|
||||
/// Short hand for creating a Matrix4x4 that translations along the given x,y,z axis.
|
||||
pub fn translation(x: Float, y: Float, z: Float) -> Matrix4x4 {
|
||||
Matrix4x4::translation(x, y, z)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Matrix2x2 {
|
||||
m: [[Float; 2]; 2],
|
||||
}
|
||||
impl Matrix2x2 {
|
||||
/// Create a `Matrix2x2` with each of the given rows.
|
||||
pub fn new(r0: [Float; 2], r1: [Float; 2]) -> Matrix2x2 {
|
||||
Matrix2x2 { m: [r0, r1] }
|
||||
}
|
||||
|
||||
/// Calculate the determinant of a 2x2.
|
||||
pub fn determinant(&self) -> Float {
|
||||
let m = self;
|
||||
m[(0, 0)] * m[(1, 1)] - m[(0, 1)] * m[(1, 0)]
|
||||
}
|
||||
}
|
||||
impl Index<(usize, usize)> for Matrix2x2 {
|
||||
type Output = Float;
|
||||
fn index(&self, (row, col): (usize, usize)) -> &Self::Output {
|
||||
&self.m[row][col]
|
||||
}
|
||||
}
|
||||
impl PartialEq for Matrix2x2 {
|
||||
fn eq(&self, rhs: &Matrix2x2) -> bool {
|
||||
let l = self.m;
|
||||
let r = rhs.m;
|
||||
for i in 0..2 {
|
||||
for j in 0..2 {
|
||||
let d = (l[i][j] - r[i][j]).abs();
|
||||
if d > EPSILON {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Matrix3x3 {
|
||||
m: [[Float; 3]; 3],
|
||||
}
|
||||
impl Matrix3x3 {
|
||||
/// Create a `Matrix3x2` with each of the given rows.
|
||||
pub fn new(r0: [Float; 3], r1: [Float; 3], r2: [Float; 3]) -> Matrix3x3 {
|
||||
Matrix3x3 { m: [r0, r1, r2] }
|
||||
}
|
||||
/// submatrix extracts a 2x2 matrix ignoring the 0-based `row` and `col` given.
|
||||
pub fn submatrix(&self, row: usize, col: usize) -> Matrix2x2 {
|
||||
assert!(row < 3);
|
||||
assert!(col < 3);
|
||||
let mut rows = vec![];
|
||||
for r in 0..3 {
|
||||
if r != row {
|
||||
let mut v = vec![];
|
||||
for c in 0..3 {
|
||||
if c != col {
|
||||
v.push(self[(r, c)]);
|
||||
}
|
||||
}
|
||||
rows.push(v);
|
||||
}
|
||||
}
|
||||
let m = [[rows[0][0], rows[0][1]], [rows[1][0], rows[1][1]]];
|
||||
Matrix2x2 { m }
|
||||
}
|
||||
|
||||
/// Compute minor of a 3x3 matrix.
|
||||
pub fn minor(&self, row: usize, col: usize) -> Float {
|
||||
self.submatrix(row, col).determinant()
|
||||
}
|
||||
|
||||
/// Compute cofactor of a 3x3 matrix.
|
||||
pub fn cofactor(&self, row: usize, col: usize) -> Float {
|
||||
let negate = if (row + col) % 2 == 0 { 1. } else { -1. };
|
||||
self.submatrix(row, col).determinant() * negate
|
||||
}
|
||||
|
||||
/// Compute determinant of a 3x3 matrix.
|
||||
pub fn determinant(&self) -> Float {
|
||||
(0..3).map(|i| self.cofactor(0, i) * self[(0, i)]).sum()
|
||||
}
|
||||
}
|
||||
impl Index<(usize, usize)> for Matrix3x3 {
|
||||
type Output = Float;
|
||||
fn index(&self, (row, col): (usize, usize)) -> &Self::Output {
|
||||
&self.m[row][col]
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Matrix3x3 {
|
||||
fn eq(&self, rhs: &Matrix3x3) -> bool {
|
||||
let l = self.m;
|
||||
let r = rhs.m;
|
||||
for i in 0..3 {
|
||||
for j in 0..3 {
|
||||
let d = (l[i][j] - r[i][j]).abs();
|
||||
if d > EPSILON {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Matrix4x4 represents a 4x4 matrix in row-major form. So, element `m[i][j]` corresponds to m<sub>i,j</sub>
|
||||
/// where `i` is the row number and `j` is the column number.
|
||||
#[derive(Copy, Clone, Default)]
|
||||
pub struct Matrix4x4 {
|
||||
m: [[Float; 4]; 4],
|
||||
}
|
||||
|
||||
impl From<[Float; 16]> for Matrix4x4 {
|
||||
fn from(t: [Float; 16]) -> Self {
|
||||
Matrix4x4 {
|
||||
m: [
|
||||
[t[0], t[1], t[2], t[3]],
|
||||
[t[4], t[5], t[6], t[7]],
|
||||
[t[8], t[9], t[10], t[11]],
|
||||
[t[12], t[13], t[14], t[15]],
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Matrix4x4 {
|
||||
/// Create a `Matrix4x4` containing the identity, all zeros with ones along the diagonal.
|
||||
pub const fn identity() -> Matrix4x4 {
|
||||
Matrix4x4::new(
|
||||
[1., 0., 0., 0.],
|
||||
[0., 1., 0., 0.],
|
||||
[0., 0., 1., 0.],
|
||||
[0., 0., 0., 1.],
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a `Matrix4x4` with each of the given rows.
|
||||
pub const fn new(r0: [Float; 4], r1: [Float; 4], r2: [Float; 4], r3: [Float; 4]) -> Matrix4x4 {
|
||||
Matrix4x4 {
|
||||
m: [r0, r1, r2, r3],
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a 4x4 matrix representing a translation of x,y,z.
|
||||
pub fn translation(x: Float, y: Float, z: Float) -> Matrix4x4 {
|
||||
Matrix4x4::new(
|
||||
[1., 0., 0., x],
|
||||
[0., 1., 0., y],
|
||||
[0., 0., 1., z],
|
||||
[0., 0., 0., 1.],
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a 4x4 matrix representing a scaling of x,y,z.
|
||||
pub fn scaling(x: Float, y: Float, z: Float) -> Matrix4x4 {
|
||||
Matrix4x4::new(
|
||||
[x, 0., 0., 0.],
|
||||
[0., y, 0., 0.],
|
||||
[0., 0., z, 0.],
|
||||
[0., 0., 0., 1.],
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a 4x4 matrix representing a rotation around the x-axis.
|
||||
pub fn rotation_x(radians: Float) -> Matrix4x4 {
|
||||
let r = radians;
|
||||
Matrix4x4::new(
|
||||
[1., 0., 0., 0.],
|
||||
[0., r.cos(), -r.sin(), 0.],
|
||||
[0., r.sin(), r.cos(), 0.],
|
||||
[0., 0., 0., 1.],
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a 4x4 matrix representing a rotation around the y-axis.
|
||||
pub fn rotation_y(radians: Float) -> Matrix4x4 {
|
||||
let r = radians;
|
||||
Matrix4x4::new(
|
||||
[r.cos(), 0., r.sin(), 0.],
|
||||
[0., 1., 0., 0.],
|
||||
[-r.sin(), 0., r.cos(), 0.],
|
||||
[0., 0., 0., 1.],
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a 4x4 matrix representing a rotation around the z-axis.
|
||||
pub fn rotation_z(radians: Float) -> Matrix4x4 {
|
||||
let r = radians;
|
||||
Matrix4x4::new(
|
||||
[r.cos(), -r.sin(), 0., 0.],
|
||||
[r.sin(), r.cos(), 0., 0.],
|
||||
[0., 0., 1., 0.],
|
||||
[0., 0., 0., 1.],
|
||||
)
|
||||
}
|
||||
|
||||
/// Transpose self, returning a new matrix that has been reflected across the diagonal.
|
||||
pub fn transpose(&self) -> Matrix4x4 {
|
||||
let m = self.m;
|
||||
Matrix4x4 {
|
||||
m: [
|
||||
[m[0][0], m[1][0], m[2][0], m[3][0]],
|
||||
[m[0][1], m[1][1], m[2][1], m[3][1]],
|
||||
[m[0][2], m[1][2], m[2][2], m[3][2]],
|
||||
[m[0][3], m[1][3], m[2][3], m[3][3]],
|
||||
],
|
||||
}
|
||||
}
|
||||
/// Create a transform matrix that will shear (skew) points.
|
||||
/// # Examples
|
||||
pub fn shearing(xy: Float, xz: Float, yx: Float, yz: Float, zx: Float, zy: Float) -> Matrix4x4 {
|
||||
Matrix4x4::new(
|
||||
[1., xy, xz, 0.],
|
||||
[yx, 1., yz, 0.],
|
||||
[zx, zy, 1., 0.],
|
||||
[0., 0., 0., 1.],
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns a new matrix that is the inverse of self. If self is A, inverse returns A<sup>-1</sup>, where
|
||||
/// AA<sup>-1</sup> = I.
|
||||
/// This implementation uses a numerically stable Gauss–Jordan elimination routine to compute the inverse.
|
||||
pub fn inverse_rtiow(&self) -> Matrix4x4 {
|
||||
// TODO(wathiede): how come the C++ version doesn't need to deal with non-invertable
|
||||
// matrix.
|
||||
let mut indxc: [usize; 4] = Default::default();
|
||||
let mut indxr: [usize; 4] = Default::default();
|
||||
let mut ipiv: [usize; 4] = Default::default();
|
||||
let mut minv = self.m;
|
||||
|
||||
for i in 0..4 {
|
||||
let mut irow: usize = 0;
|
||||
let mut icol: usize = 0;
|
||||
let mut big: Float = 0.;
|
||||
// Choose pivot
|
||||
for j in 0..4 {
|
||||
if ipiv[j] != 1 {
|
||||
for (k, ipivk) in ipiv.iter().enumerate() {
|
||||
if *ipivk == 0 {
|
||||
if minv[j][k].abs() >= big {
|
||||
big = minv[j][k].abs();
|
||||
irow = j;
|
||||
icol = k;
|
||||
}
|
||||
} else if *ipivk > 1 {
|
||||
eprintln!("Singular matrix in MatrixInvert");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ipiv[icol] += 1;
|
||||
// Swap rows _irow_ and _icol_ for pivot
|
||||
if irow != icol {
|
||||
// Can't figure out how to make swap work here.
|
||||
#[allow(clippy::manual_swap)]
|
||||
for k in 0..4 {
|
||||
let tmp = minv[irow][k];
|
||||
minv[irow][k] = minv[icol][k];
|
||||
minv[icol][k] = tmp;
|
||||
}
|
||||
}
|
||||
indxr[i] = irow;
|
||||
indxc[i] = icol;
|
||||
if minv[icol][icol] == 0. {
|
||||
eprintln!("Singular matrix in MatrixInvert");
|
||||
}
|
||||
|
||||
// Set $m[icol][icol]$ to one by scaling row _icol_ appropriately
|
||||
let pivinv: Float = minv[icol][icol].recip();
|
||||
minv[icol][icol] = 1.;
|
||||
for j in 0..4 {
|
||||
minv[icol][j] *= pivinv;
|
||||
}
|
||||
|
||||
// Subtract this row from others to zero out their columns
|
||||
for j in 0..4 {
|
||||
if j != icol {
|
||||
let save = minv[j][icol];
|
||||
minv[j][icol] = 0.;
|
||||
for k in 0..4 {
|
||||
minv[j][k] -= minv[icol][k] * save;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Swap columns to reflect permutation
|
||||
for j in (0..4).rev() {
|
||||
if indxr[j] != indxc[j] {
|
||||
for mi in &mut minv {
|
||||
mi.swap(indxr[j], indxc[j])
|
||||
}
|
||||
}
|
||||
}
|
||||
Matrix4x4 { m: minv }
|
||||
}
|
||||
/// submatrix extracts a 3x3 matrix ignoring the 0-based `row` and `col` given.
|
||||
pub fn submatrix(&self, row: usize, col: usize) -> Matrix3x3 {
|
||||
assert!(row < 4);
|
||||
assert!(col < 4);
|
||||
let mut rows = vec![];
|
||||
for r in 0..4 {
|
||||
if r != row {
|
||||
let mut v = vec![];
|
||||
for c in 0..4 {
|
||||
if c != col {
|
||||
v.push(self[(r, c)]);
|
||||
}
|
||||
}
|
||||
rows.push(v);
|
||||
}
|
||||
}
|
||||
let m = [
|
||||
[rows[0][0], rows[0][1], rows[0][2]],
|
||||
[rows[1][0], rows[1][1], rows[1][2]],
|
||||
[rows[2][0], rows[2][1], rows[2][2]],
|
||||
];
|
||||
Matrix3x3 { m }
|
||||
}
|
||||
|
||||
/// Compute minor of a 4x4 matrix.
|
||||
pub fn minor(&self, row: usize, col: usize) -> Float {
|
||||
self.submatrix(row, col).determinant()
|
||||
}
|
||||
/// Compute cofactor of a 4x4 matrix.
|
||||
pub fn cofactor(&self, row: usize, col: usize) -> Float {
|
||||
let negate = if (row + col) % 2 == 0 { 1. } else { -1. };
|
||||
self.submatrix(row, col).determinant() * negate
|
||||
}
|
||||
/// Compute determinant of a 4x4 matrix.
|
||||
pub fn determinant(&self) -> Float {
|
||||
(0..4).map(|i| self.cofactor(0, i) * self[(0, i)]).sum()
|
||||
}
|
||||
|
||||
/// Compute invertibility of matrix (i.e. non-zero determinant.
|
||||
pub fn invertable(&self) -> bool {
|
||||
self.determinant() != 0.
|
||||
}
|
||||
|
||||
/// Compute the inverse of a 4x4 matrix.
|
||||
pub fn inverse(&self) -> Matrix4x4 {
|
||||
self.inverse_rtc()
|
||||
}
|
||||
pub fn inverse_rtc(&self) -> Matrix4x4 {
|
||||
let m = self;
|
||||
if !m.invertable() {
|
||||
panic!("Matrix4x4::inverse called on matrix with determinant() == 0");
|
||||
}
|
||||
let mut m2 = Matrix4x4::identity();
|
||||
let det = m.determinant();
|
||||
for row in 0..4 {
|
||||
for col in 0..4 {
|
||||
let c = m.cofactor(row, col);
|
||||
m2[(col, row)] = c / det;
|
||||
}
|
||||
}
|
||||
m2
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Matrix4x4 {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if f.alternate() {
|
||||
write!(
|
||||
f,
|
||||
"\n {:8.5?}\n {:8.5?}\n {:8.5?}\n {:8.5?}",
|
||||
self.m[0], self.m[1], self.m[2], self.m[3]
|
||||
)
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"[{:?} {:?} {:?} {:?}]",
|
||||
self.m[0], self.m[1], self.m[2], self.m[3]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Matrix4x4> for Matrix4x4 {
|
||||
type Output = Matrix4x4;
|
||||
|
||||
/// Implement matrix multiplication for `Matrix4x4`.
|
||||
fn mul(self, m2: Matrix4x4) -> Matrix4x4 {
|
||||
let m1 = self;
|
||||
let mut r: Matrix4x4 = Default::default();
|
||||
for i in 0..4 {
|
||||
for j in 0..4 {
|
||||
r.m[i][j] = m1.m[i][0] * m2.m[0][j]
|
||||
+ m1.m[i][1] * m2.m[1][j]
|
||||
+ m1.m[i][2] * m2.m[2][j]
|
||||
+ m1.m[i][3] * m2.m[3][j];
|
||||
}
|
||||
}
|
||||
r
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Tuple> for Matrix4x4 {
|
||||
type Output = Tuple;
|
||||
|
||||
/// Implement matrix multiplication for `Matrix4x4` * `Tuple`.
|
||||
fn mul(self, t: Tuple) -> Tuple {
|
||||
let m = self;
|
||||
Tuple {
|
||||
x: m.m[0][0] * t.x + m.m[0][1] * t.y + m.m[0][2] * t.z + m.m[0][3] * t.w,
|
||||
y: m.m[1][0] * t.x + m.m[1][1] * t.y + m.m[1][2] * t.z + m.m[1][3] * t.w,
|
||||
z: m.m[2][0] * t.x + m.m[2][1] * t.y + m.m[2][2] * t.z + m.m[2][3] * t.w,
|
||||
w: m.m[3][0] * t.x + m.m[3][1] * t.y + m.m[3][2] * t.z + m.m[3][3] * t.w,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for Matrix4x4 {
|
||||
type Output = Matrix4x4;
|
||||
|
||||
fn sub(self, m2: Matrix4x4) -> Matrix4x4 {
|
||||
let m1 = self;
|
||||
let mut r: Matrix4x4 = Default::default();
|
||||
for i in 0..4 {
|
||||
for j in 0..4 {
|
||||
r.m[i][j] = m1.m[i][j] - m2.m[i][j];
|
||||
}
|
||||
}
|
||||
r
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Matrix4x4 {
|
||||
fn eq(&self, rhs: &Matrix4x4) -> bool {
|
||||
let l = self.m;
|
||||
let r = rhs.m;
|
||||
for i in 0..4 {
|
||||
for j in 0..4 {
|
||||
let d = (l[i][j] - r[i][j]).abs();
|
||||
if d > EPSILON {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<(usize, usize)> for Matrix4x4 {
|
||||
type Output = Float;
|
||||
fn index(&self, (row, col): (usize, usize)) -> &Self::Output {
|
||||
&self.m[row][col]
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexMut<(usize, usize)> for Matrix4x4 {
|
||||
fn index_mut(&mut self, (row, col): (usize, usize)) -> &mut Self::Output {
|
||||
&mut self.m[row][col]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::{
|
||||
float::consts::PI,
|
||||
tuples::{point, vector},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn example4x4() {
|
||||
// Individual transformations are applied in sequence.
|
||||
let p = point(1., 0., 1.);
|
||||
let a = Matrix4x4::rotation_x(PI / 2.);
|
||||
let b = Matrix4x4::scaling(5., 5., 5.);
|
||||
let c = Matrix4x4::translation(10., 5., 7.);
|
||||
// Apply rotation first.
|
||||
let p2 = a * p;
|
||||
assert_eq!(p2, point(1., -1., 0.));
|
||||
// Then apply scaling.
|
||||
let p3 = b * p2;
|
||||
assert_eq!(p3, point(5., -5., 0.));
|
||||
// Then apply translation.
|
||||
let p4 = c * p3;
|
||||
assert_eq!(p4, point(15., 0., 7.));
|
||||
|
||||
// Chained transformations must be applied in reverse order.
|
||||
let p = point(1., 0., 1.);
|
||||
let a = Matrix4x4::rotation_x(PI / 2.);
|
||||
let b = Matrix4x4::scaling(5., 5., 5.);
|
||||
let c = Matrix4x4::translation(10., 5., 7.);
|
||||
let t = c * b * a;
|
||||
assert_eq!(t * p, point(15., 0., 7.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn translation() {
|
||||
let transform = Matrix4x4::translation(5., -3., 2.);
|
||||
let p = point(-3., 4., 5.);
|
||||
assert_eq!(transform * p, point(2., 1., 7.));
|
||||
|
||||
let inv = transform.inverse();
|
||||
assert_eq!(inv * p, point(-8., 7., 3.));
|
||||
|
||||
let v = vector(-3., 4., 5.);
|
||||
assert_eq!(transform * v, v);
|
||||
}
|
||||
#[test]
|
||||
fn scaling() {
|
||||
// A scaling matrix applied to a point.
|
||||
let transform = Matrix4x4::scaling(2., 3., 4.);
|
||||
let p = point(-4., 6., 8.);
|
||||
assert_eq!(transform * p, point(-8., 18., 32.));
|
||||
|
||||
// A scaling matrix applied to a vector.
|
||||
let v = vector(-4., 6., 8.);
|
||||
assert_eq!(transform * v, vector(-8., 18., 32.));
|
||||
|
||||
// Multiplying by the inverse of a scaling matrix.
|
||||
let inv = transform.inverse();
|
||||
assert_eq!(inv * v, vector(-2., 2., 2.));
|
||||
|
||||
// Reflection is scaling by a negative value.
|
||||
let transform = Matrix4x4::scaling(-1., 1., 1.);
|
||||
let p = point(2., 3., 4.);
|
||||
assert_eq!(transform * p, point(-2., 3., 4.));
|
||||
}
|
||||
#[test]
|
||||
fn rotation_x() {
|
||||
// A scaling matrix applied to a point.
|
||||
let p = point(0., 1., 0.);
|
||||
let half_quarter = Matrix4x4::rotation_x(PI / 4.);
|
||||
let full_quarter = Matrix4x4::rotation_x(PI / 2.);
|
||||
|
||||
assert_eq!(
|
||||
half_quarter * p,
|
||||
point(0., (2.0 as Float).sqrt() / 2., (2.0 as Float).sqrt() / 2.)
|
||||
);
|
||||
assert_eq!(full_quarter * p, point(0., 0., 1.),);
|
||||
}
|
||||
#[test]
|
||||
fn rotation_y() {
|
||||
// A scaling matrix applied to a point.
|
||||
let p = point(0., 0., 1.);
|
||||
let half_quarter = Matrix4x4::rotation_y(PI / 4.);
|
||||
let full_quarter = Matrix4x4::rotation_y(PI / 2.);
|
||||
|
||||
assert_eq!(
|
||||
half_quarter * p,
|
||||
point((2.0 as Float).sqrt() / 2., 0., (2.0 as Float).sqrt() / 2.)
|
||||
);
|
||||
assert_eq!(full_quarter * p, point(1., 0., 0.,),);
|
||||
}
|
||||
#[test]
|
||||
fn rotation_z() {
|
||||
// A scaling matrix applied to a point.
|
||||
let p = point(0., 1., 0.);
|
||||
let half_quarter = Matrix4x4::rotation_z(PI / 4.);
|
||||
let full_quarter = Matrix4x4::rotation_z(PI / 2.);
|
||||
|
||||
assert_eq!(
|
||||
half_quarter * p,
|
||||
point(-(2.0 as Float).sqrt() / 2., (2.0 as Float).sqrt() / 2., 0.)
|
||||
);
|
||||
assert_eq!(full_quarter * p, point(-1., 0., 0.,),);
|
||||
}
|
||||
#[test]
|
||||
fn transpose() {
|
||||
let m = Matrix4x4::new(
|
||||
[2., 0., 0., 0.],
|
||||
[3., 1., 0., 0.],
|
||||
[4., 0., 1., 0.],
|
||||
[5., 6., 7., 1.],
|
||||
);
|
||||
let m_t = Matrix4x4::new(
|
||||
[2., 3., 4., 5.],
|
||||
[0., 1., 0., 6.],
|
||||
[0., 0., 1., 7.],
|
||||
[0., 0., 0., 1.],
|
||||
);
|
||||
assert_eq!(m.transpose(), m_t);
|
||||
|
||||
assert_eq!(Matrix4x4::identity(), Matrix4x4::identity().transpose());
|
||||
}
|
||||
#[test]
|
||||
fn shearing() {
|
||||
// A shearing transform moves x in proportion to y.
|
||||
let transform = Matrix4x4::shearing(1., 0., 0., 0., 0., 0.);
|
||||
let p = point(2., 3., 4.);
|
||||
assert_eq!(transform * p, point(5., 3., 4.));
|
||||
|
||||
// A shearing transform moves x in proportion to z.
|
||||
let transform = Matrix4x4::shearing(0., 1., 0., 0., 0., 0.);
|
||||
let p = point(2., 3., 4.);
|
||||
assert_eq!(transform * p, point(6., 3., 4.));
|
||||
|
||||
// A shearing transform moves y in proportion to x.
|
||||
let transform = Matrix4x4::shearing(0., 0., 1., 0., 0., 0.);
|
||||
let p = point(2., 3., 4.);
|
||||
assert_eq!(transform * p, point(2., 5., 4.));
|
||||
|
||||
// A shearing transform moves y in proportion to z.
|
||||
let transform = Matrix4x4::shearing(0., 0., 0., 1., 0., 0.);
|
||||
let p = point(2., 3., 4.);
|
||||
assert_eq!(transform * p, point(2., 7., 4.));
|
||||
|
||||
// A shearing transform moves z in proportion to x.
|
||||
let transform = Matrix4x4::shearing(0., 0., 0., 0., 1., 0.);
|
||||
let p = point(2., 3., 4.);
|
||||
assert_eq!(transform * p, point(2., 3., 6.));
|
||||
|
||||
// A shearing transform moves z in proportion to y.
|
||||
let transform = Matrix4x4::shearing(0., 0., 0., 0., 0., 1.);
|
||||
let p = point(2., 3., 4.);
|
||||
assert_eq!(transform * p, point(2., 3., 7.));
|
||||
}
|
||||
#[test]
|
||||
fn inverse_rtiow() {
|
||||
let i = Matrix4x4::identity();
|
||||
assert_eq!(i.inverse_rtiow() * i, i);
|
||||
|
||||
let m = Matrix4x4::new(
|
||||
[2., 0., 0., 0.],
|
||||
[0., 3., 0., 0.],
|
||||
[0., 0., 4., 0.],
|
||||
[0., 0., 0., 1.],
|
||||
);
|
||||
assert_eq!(m.inverse_rtiow() * m, i);
|
||||
assert_eq!(m * m.inverse_rtiow(), i);
|
||||
}
|
||||
#[test]
|
||||
fn determinant_2x2() {
|
||||
let a = Matrix2x2::new([1., 5.], [-3., 2.]);
|
||||
|
||||
assert_eq!(a.determinant(), 17.);
|
||||
}
|
||||
#[test]
|
||||
fn determinant_3x3() {
|
||||
let a = Matrix3x3::new([1., 2., 6.], [-5., 8., -4.], [2., 6., 4.]);
|
||||
assert_eq!(a.cofactor(0, 0), 56.);
|
||||
assert_eq!(a.cofactor(0, 1), 12.);
|
||||
assert_eq!(a.cofactor(0, 2), -46.);
|
||||
assert_eq!(a.determinant(), -196.);
|
||||
}
|
||||
#[test]
|
||||
fn determinant_4x4() {
|
||||
let a = Matrix4x4::new(
|
||||
[-2., -8., 3., 5.],
|
||||
[-3., 1., 7., 3.],
|
||||
[1., 2., -9., 6.],
|
||||
[-6., 7., 7., -9.],
|
||||
);
|
||||
assert_eq!(a.cofactor(0, 0), 690.);
|
||||
assert_eq!(a.cofactor(0, 1), 447.);
|
||||
assert_eq!(a.cofactor(0, 2), 210.);
|
||||
assert_eq!(a.cofactor(0, 3), 51.);
|
||||
assert_eq!(a.determinant(), -4071.);
|
||||
}
|
||||
#[test]
|
||||
fn submatrix_3x3() {
|
||||
assert_eq!(
|
||||
Matrix3x3::new([1., 5., 0.], [-3., 2., 7.], [0., 6., -3.],).submatrix(0, 2),
|
||||
Matrix2x2::new([-3., 2.], [0., 6.])
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn submatrix_4x4() {
|
||||
assert_eq!(
|
||||
Matrix4x4::new(
|
||||
[-6., 1., 1., 6.],
|
||||
[-8., 5., 8., 6.],
|
||||
[-1., 0., 8., 2.],
|
||||
[-7., 1., -1., 1.],
|
||||
)
|
||||
.submatrix(2, 1),
|
||||
Matrix3x3::new([-6., 1., 6.], [-8., 8., 6.], [-7., -1., 1.],)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn minor_3x3() {
|
||||
let a = Matrix3x3::new([3., 5., 0.], [2., -1., -7.], [6., -1., 5.]);
|
||||
let b = a.submatrix(1, 0);
|
||||
assert_eq!(b.determinant(), 25.0);
|
||||
assert_eq!(b.determinant(), a.minor(1, 0));
|
||||
}
|
||||
#[test]
|
||||
fn cofactor_3x3() {
|
||||
let a = Matrix3x3::new([3., 5., 0.], [2., -1., -7.], [6., -1., 5.]);
|
||||
assert_eq!(a.minor(0, 0), -12.);
|
||||
assert_eq!(a.cofactor(0, 0), -12.);
|
||||
assert_eq!(a.minor(1, 0), 25.);
|
||||
assert_eq!(a.cofactor(1, 0), -25.);
|
||||
}
|
||||
#[test]
|
||||
fn construct2x2() {
|
||||
let m = Matrix2x2::new([-3., 5.], [1., -2.]);
|
||||
|
||||
assert_eq!(m[(0, 0)], -3.);
|
||||
assert_eq!(m[(0, 1)], 5.);
|
||||
assert_eq!(m[(1, 0)], 1.);
|
||||
assert_eq!(m[(1, 1)], -2.);
|
||||
}
|
||||
#[test]
|
||||
fn construct3x3() {
|
||||
let m = Matrix3x3::new([-3., 5., 0.], [1., -2., -7.], [0., 1., 1.]);
|
||||
|
||||
assert_eq!(m[(0, 0)], -3.);
|
||||
assert_eq!(m[(1, 1)], -2.);
|
||||
assert_eq!(m[(2, 2)], 1.);
|
||||
}
|
||||
#[test]
|
||||
fn invertable() {
|
||||
let a = Matrix4x4::new(
|
||||
[6., 4., 4., 4.],
|
||||
[5., 5., 7., 6.],
|
||||
[4., -9., 3., -7.],
|
||||
[9., 1., 7., -6.],
|
||||
);
|
||||
assert_eq!(a.determinant(), -2120.);
|
||||
assert_eq!(a.invertable(), true);
|
||||
|
||||
let a = Matrix4x4::new(
|
||||
[-4., 2., -2., -3.],
|
||||
[9., 6., 2., 6.],
|
||||
[0., -5., 1., -5.],
|
||||
[0., 0., 0., 0.],
|
||||
);
|
||||
assert_eq!(a.determinant(), 0.);
|
||||
assert_eq!(a.invertable(), false);
|
||||
}
|
||||
#[test]
|
||||
fn inverse() {
|
||||
let a = Matrix4x4::new(
|
||||
[-5., 2., 6., -8.],
|
||||
[1., -5., 1., 8.],
|
||||
[7., 7., -6., -7.],
|
||||
[1., -3., 7., 4.],
|
||||
);
|
||||
let b = a.inverse();
|
||||
|
||||
assert_eq!(a.determinant(), 532.);
|
||||
assert_eq!(a.cofactor(2, 3), -160.);
|
||||
assert_eq!(b[(3, 2)], -160. / 532.);
|
||||
assert_eq!(a.cofactor(3, 2), 105.);
|
||||
assert_eq!(b[(2, 3)], 105. / 532.);
|
||||
assert_eq!(
|
||||
b,
|
||||
Matrix4x4::new(
|
||||
[0.21804512, 0.45112783, 0.24060151, -0.04511278],
|
||||
[-0.8082707, -1.456767, -0.44360903, 0.5206767],
|
||||
[-0.078947365, -0.2236842, -0.05263158, 0.19736843],
|
||||
[-0.52255636, -0.81390977, -0.30075186, 0.30639097]
|
||||
)
|
||||
);
|
||||
|
||||
// Second test case
|
||||
assert_eq!(
|
||||
Matrix4x4::new(
|
||||
[8., -5., 9., 2.],
|
||||
[7., 5., 6., 1.],
|
||||
[-6., 0., 9., 6.],
|
||||
[-3., 0., -9., -4.],
|
||||
)
|
||||
.inverse(),
|
||||
Matrix4x4::new(
|
||||
[-0.15384616, -0.15384616, -0.2820513, -0.53846157],
|
||||
[-0.07692308, 0.12307692, 0.025641026, 0.03076923],
|
||||
[0.35897437, 0.35897437, 0.43589744, 0.9230769],
|
||||
[-0.6923077, -0.6923077, -0.7692308, -1.9230769]
|
||||
),
|
||||
);
|
||||
|
||||
// Third test case
|
||||
assert_eq!(
|
||||
Matrix4x4::new(
|
||||
[9., 3., 0., 9.],
|
||||
[-5., -2., -6., -3.],
|
||||
[-4., 9., 6., 4.],
|
||||
[-7., 6., 6., 2.],
|
||||
)
|
||||
.inverse(),
|
||||
Matrix4x4::new(
|
||||
[-0.04074074, -0.07777778, 0.14444445, -0.22222222],
|
||||
[-0.07777778, 0.033333335, 0.36666667, -0.33333334],
|
||||
[-0.029012345, -0.14629629, -0.10925926, 0.12962963],
|
||||
[0.17777778, 0.06666667, -0.26666668, 0.33333334]
|
||||
),
|
||||
);
|
||||
|
||||
let a = Matrix4x4::new(
|
||||
[3., -9., 7., 3.],
|
||||
[3., -8., 2., -9.],
|
||||
[-4., 4., 4., 1.],
|
||||
[-6., 5., -1., 1.],
|
||||
);
|
||||
let b = Matrix4x4::new(
|
||||
[8., 2., 2., 2.],
|
||||
[3., -1., 7., 0.],
|
||||
[7., 0., 5., 4.],
|
||||
[6., -2., 0., 5.],
|
||||
);
|
||||
let c = a * b;
|
||||
assert_eq!(c * b.inverse(), a);
|
||||
}
|
||||
#[test]
|
||||
fn construct4x4() {
|
||||
let m = Matrix4x4::new(
|
||||
[1., 2., 3., 4.],
|
||||
[5.5, 6.5, 7.5, 8.5],
|
||||
[9., 10., 11., 12.],
|
||||
[13.5, 14.5, 15.5, 16.5],
|
||||
);
|
||||
|
||||
assert_eq!(m[(0, 0)], 1.);
|
||||
assert_eq!(m[(0, 3)], 4.);
|
||||
assert_eq!(m[(1, 0)], 5.5);
|
||||
assert_eq!(m[(1, 2)], 7.5);
|
||||
assert_eq!(m[(2, 2)], 11.);
|
||||
assert_eq!(m[(3, 0)], 13.5);
|
||||
assert_eq!(m[(3, 2)], 15.5);
|
||||
}
|
||||
#[test]
|
||||
fn equality4x4() {
|
||||
let a = Matrix4x4::new(
|
||||
[1., 2., 3., 4.],
|
||||
[5., 6., 7., 8.],
|
||||
[9., 8., 7., 6.],
|
||||
[5., 4., 3., 2.],
|
||||
);
|
||||
let b = Matrix4x4::new(
|
||||
[1., 2., 3., 4.],
|
||||
[5., 6., 7., 8.],
|
||||
[9., 8., 7., 6.],
|
||||
[5., 4., 3., 2.],
|
||||
);
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
#[test]
|
||||
fn inequality4x4() {
|
||||
let a = Matrix4x4::new(
|
||||
[1., 2., 3., 4.],
|
||||
[5., 6., 7., 8.],
|
||||
[9., 8., 7., 6.],
|
||||
[5., 4., 3., 2.],
|
||||
);
|
||||
let b = Matrix4x4::new(
|
||||
[2., 3., 4., 5.],
|
||||
[6., 7., 8., 9.],
|
||||
[8., 7., 6., 5.],
|
||||
[4., 3., 2., 1.],
|
||||
);
|
||||
assert_ne!(a, b);
|
||||
}
|
||||
#[test]
|
||||
fn mul4x4() {
|
||||
let a = Matrix4x4::new(
|
||||
[1., 2., 3., 4.],
|
||||
[5., 6., 7., 8.],
|
||||
[9., 8., 7., 6.],
|
||||
[5., 4., 3., 2.],
|
||||
);
|
||||
let b = Matrix4x4::new(
|
||||
[-2., 1., 2., 3.],
|
||||
[3., 2., 1., -1.],
|
||||
[4., 3., 6., 5.],
|
||||
[1., 2., 7., 8.],
|
||||
);
|
||||
assert_eq!(
|
||||
a * b,
|
||||
Matrix4x4::new(
|
||||
[20., 22., 50., 48.],
|
||||
[44., 54., 114., 108.],
|
||||
[40., 58., 110., 102.],
|
||||
[16., 26., 46., 42.],
|
||||
)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn mul4x4_tuple() {
|
||||
let a = Matrix4x4::new(
|
||||
[1., 2., 3., 4.],
|
||||
[2., 4., 4., 2.],
|
||||
[8., 6., 4., 1.],
|
||||
[0., 0., 0., 1.],
|
||||
);
|
||||
let b = Tuple::new(1., 2., 3., 1.);
|
||||
|
||||
assert_eq!(a * b, Tuple::new(18., 24., 33., 1.));
|
||||
}
|
||||
}
|
||||
444
rtchallenge/src/patterns.rs
Normal file
444
rtchallenge/src/patterns.rs
Normal file
@@ -0,0 +1,444 @@
|
||||
use derive_builder::Builder;
|
||||
|
||||
use crate::{
|
||||
matrices::Matrix4x4,
|
||||
shapes::Shape,
|
||||
tuples::{Color, Tuple},
|
||||
BLACK, WHITE,
|
||||
};
|
||||
|
||||
pub const BLACK_PAT: Pattern = Pattern {
|
||||
color: ColorMapper::Constant(BLACK),
|
||||
transform: Matrix4x4::identity(),
|
||||
inverse_transform: Matrix4x4::identity(),
|
||||
};
|
||||
pub const WHITE_PAT: Pattern = Pattern {
|
||||
color: ColorMapper::Constant(WHITE),
|
||||
transform: Matrix4x4::identity(),
|
||||
inverse_transform: Matrix4x4::identity(),
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum ColorMapper {
|
||||
/// TestPattern the color returned is the pattern space point after going through world->object and object->pattern space translation.
|
||||
TestPattern,
|
||||
/// Solid color, the same sampled every where.
|
||||
Constant(Color),
|
||||
/// Pattern that alternates between the given patterns along each unit of the X-axis. The
|
||||
/// strip extends infinitely in the positive and negative Y and Z axes.
|
||||
Stripe { a: Box<Pattern>, b: Box<Pattern> },
|
||||
/// Linear blend between `a` and `b` along the X-axis.
|
||||
Gradient { a: Box<Pattern>, b: Box<Pattern> },
|
||||
/// Bullseye pattern in the XZ plane.
|
||||
Ring { a: Box<Pattern>, b: Box<Pattern> },
|
||||
/// Traditional ray tracer tile floor pattern.
|
||||
Checkers { a: Box<Pattern>, b: Box<Pattern> },
|
||||
}
|
||||
|
||||
impl From<Color> for ColorMapper {
|
||||
fn from(c: Color) -> ColorMapper {
|
||||
ColorMapper::Constant(c)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for Box<Pattern> {
|
||||
fn from(c: Color) -> Box<Pattern> {
|
||||
Box::new(Pattern {
|
||||
color: ColorMapper::Constant(c),
|
||||
..Pattern::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Builder, Debug, PartialEq, Clone)]
|
||||
#[builder(default, pattern = "immutable")]
|
||||
pub struct Pattern {
|
||||
pub color: ColorMapper,
|
||||
transform: Matrix4x4,
|
||||
#[builder(private, default = "self.default_inverse_transform()?")]
|
||||
inverse_transform: Matrix4x4,
|
||||
}
|
||||
|
||||
impl PatternBuilder {
|
||||
fn default_inverse_transform(&self) -> Result<Matrix4x4, String> {
|
||||
Ok(self.transform.unwrap_or(Matrix4x4::identity()).inverse())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Pattern {
|
||||
fn default() -> Pattern {
|
||||
Pattern {
|
||||
color: ColorMapper::Constant(WHITE),
|
||||
transform: Matrix4x4::identity(),
|
||||
inverse_transform: Matrix4x4::identity(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a [Pattern] with a color type of [ColorMapper::Constant] from the given [Color]
|
||||
impl<C> From<C> for Pattern
|
||||
where
|
||||
C: Into<Color>,
|
||||
{
|
||||
fn from(c: C) -> Self {
|
||||
Pattern {
|
||||
color: ColorMapper::Constant(c.into()),
|
||||
..Pattern::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for creating a material pattern used for testing. The color returned is the pattern space point
|
||||
/// after going through world->object and object->pattern space translation.
|
||||
pub fn test_pattern() -> PatternBuilder {
|
||||
PatternBuilder::default().color(ColorMapper::TestPattern)
|
||||
}
|
||||
|
||||
/// Builder for creating a material pattern that alternates between the given colors along each unit of the
|
||||
/// X-axis. The strip extends infinitely in the positive and negative Y and Z axes.
|
||||
pub fn stripe_pattern(a: Pattern, b: Pattern) -> PatternBuilder {
|
||||
PatternBuilder::default().color(ColorMapper::Stripe {
|
||||
a: a.into(),
|
||||
b: b.into(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Builder for creating a material pattern that gradually blends between the given colors along
|
||||
/// the X-axis.
|
||||
pub fn gradient_pattern(a: Pattern, b: Pattern) -> PatternBuilder {
|
||||
PatternBuilder::default().color(ColorMapper::Gradient {
|
||||
a: a.into(),
|
||||
b: b.into(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Builder for creating a material pattern that alternates between the given colors in a ring
|
||||
/// shape in the XZ plane.
|
||||
pub fn ring_pattern(a: Pattern, b: Pattern) -> PatternBuilder {
|
||||
PatternBuilder::default().color(ColorMapper::Ring {
|
||||
a: a.into(),
|
||||
b: b.into(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Builder for creating a material pattern that alternates between the given colors along the X, Y
|
||||
/// and Z pattern. Creates traditional ray tracer tile floor pattern.
|
||||
pub fn checkers_pattern(a: Pattern, b: Pattern) -> PatternBuilder {
|
||||
PatternBuilder::default().color(ColorMapper::Checkers {
|
||||
a: a.into(),
|
||||
b: b.into(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Generic implementation for mapping points to colors according to the given [ColorMapper].
|
||||
impl Pattern {
|
||||
/// Create a pattern used for testing. The color returned is the pattern space point
|
||||
/// after going through world->object and object->pattern space translation.
|
||||
pub fn test() -> Pattern {
|
||||
Pattern {
|
||||
color: ColorMapper::TestPattern,
|
||||
..Pattern::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a pattern that alternates between the given Patterns along each unit of the
|
||||
/// X-axis. The strip extends infinitely in the positive and negative Y and Z axes.
|
||||
pub fn stripe(a: Pattern, b: Pattern) -> Pattern {
|
||||
Pattern {
|
||||
color: ColorMapper::Stripe {
|
||||
a: a.into(),
|
||||
b: b.into(),
|
||||
},
|
||||
..Pattern::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a pattern that gradually blends between the given Patterns along the X-axis.
|
||||
pub fn gradient(a: Pattern, b: Pattern) -> Pattern {
|
||||
Pattern {
|
||||
color: ColorMapper::Gradient {
|
||||
a: a.into(),
|
||||
b: b.into(),
|
||||
},
|
||||
..Pattern::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a pattern that alternates between the given Patterns in a ring in the XZ plane.
|
||||
pub fn ring(a: Pattern, b: Pattern) -> Pattern {
|
||||
Pattern {
|
||||
color: ColorMapper::Ring {
|
||||
a: a.into(),
|
||||
b: b.into(),
|
||||
},
|
||||
..Pattern::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a pattern that alternates between the given Patterns along the X, Y and Z axis.
|
||||
pub fn checkers(a: Pattern, b: Pattern) -> Pattern {
|
||||
Pattern {
|
||||
color: ColorMapper::Checkers {
|
||||
a: a.into(),
|
||||
b: b.into(),
|
||||
},
|
||||
..Pattern::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Sample the color at the given point in untranslated object space.
|
||||
pub fn pattern_at(&self, object_point: Tuple) -> Color {
|
||||
let point = self.inverse_transform * object_point;
|
||||
match &self.color {
|
||||
ColorMapper::TestPattern => [point.x, point.y, point.z].into(),
|
||||
ColorMapper::Constant(c) => *c,
|
||||
ColorMapper::Stripe { a, b } => {
|
||||
let x = point.x.floor();
|
||||
if x % 2. == 0. {
|
||||
a.pattern_at(point)
|
||||
} else {
|
||||
b.pattern_at(point)
|
||||
}
|
||||
}
|
||||
ColorMapper::Gradient { a, b } => {
|
||||
let a = a.pattern_at(point);
|
||||
let b = b.pattern_at(point);
|
||||
let distance = b - a;
|
||||
let fraction = point.x - point.x.floor();
|
||||
a + distance * fraction
|
||||
}
|
||||
ColorMapper::Ring { a, b } => {
|
||||
let a = a.pattern_at(point);
|
||||
let b = b.pattern_at(point);
|
||||
let px = point.x;
|
||||
let pz = point.z;
|
||||
if (px * px + pz * pz).sqrt().floor() % 2. == 0. {
|
||||
a
|
||||
} else {
|
||||
b
|
||||
}
|
||||
}
|
||||
ColorMapper::Checkers { a, b } => {
|
||||
let a = a.pattern_at(point);
|
||||
let b = b.pattern_at(point);
|
||||
let d = point.x.floor() + point.y.floor() + point.z.floor();
|
||||
if d % 2. == 0. {
|
||||
a
|
||||
} else {
|
||||
b
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Sample the color at the given world point on the given object.
|
||||
/// This function respects the object and the pattern's transform matrix.
|
||||
pub fn pattern_at_object(&self, object: &Shape, world_point: Tuple) -> Color {
|
||||
let object_point = object.inverse_transform() * world_point;
|
||||
self.pattern_at(object_point)
|
||||
}
|
||||
pub fn transform(&self) -> Matrix4x4 {
|
||||
self.transform
|
||||
}
|
||||
|
||||
pub fn inverse_transform(&self) -> Matrix4x4 {
|
||||
self.inverse_transform
|
||||
}
|
||||
|
||||
pub fn set_transform(&mut self, t: Matrix4x4) {
|
||||
self.transform = t;
|
||||
self.inverse_transform = t.inverse();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
matrices::identity,
|
||||
patterns::{ColorMapper, Pattern, BLACK_PAT, WHITE_PAT},
|
||||
BLACK, WHITE,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_create() {
|
||||
let pattern = Pattern::test();
|
||||
assert_eq!(pattern.transform(), identity());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stripe_create() {
|
||||
let pattern = Pattern::stripe(BLACK_PAT, WHITE_PAT);
|
||||
assert_eq!(
|
||||
pattern.color,
|
||||
ColorMapper::Stripe {
|
||||
a: BLACK.into(),
|
||||
b: WHITE.into(),
|
||||
}
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn gradient_create() {
|
||||
println!("SHOE ME SOMETHING");
|
||||
println!("* * * 1");
|
||||
let pattern = Pattern::gradient(BLACK_PAT, WHITE_PAT);
|
||||
println!("* * * 2");
|
||||
assert_eq!(
|
||||
pattern.color,
|
||||
ColorMapper::Gradient {
|
||||
a: BLACK.into(),
|
||||
b: WHITE.into(),
|
||||
}
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn ring_create() {
|
||||
let pattern = Pattern::ring(BLACK_PAT, WHITE_PAT);
|
||||
assert_eq!(
|
||||
pattern.color,
|
||||
ColorMapper::Ring {
|
||||
a: BLACK.into(),
|
||||
b: WHITE.into()
|
||||
}
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn checkers_create() {
|
||||
let pattern = Pattern::checkers(BLACK_PAT, WHITE_PAT);
|
||||
assert_eq!(
|
||||
pattern.color,
|
||||
ColorMapper::Checkers {
|
||||
a: BLACK.into(),
|
||||
b: WHITE.into()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
mod pattern_at {
|
||||
use super::*;
|
||||
use crate::tuples::point;
|
||||
|
||||
#[test]
|
||||
fn test_returns_coordinates() {
|
||||
// A test returns the pattern space coordinates of the point.
|
||||
let pattern = Pattern::test();
|
||||
let p = point(1., 2., 3.);
|
||||
assert_eq!(pattern.pattern_at(p), [p.x, p.y, p.z].into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stripe_alternates_between_two_colors() {
|
||||
// A stripe alternates between two colors.
|
||||
let pattern = Pattern::stripe(WHITE_PAT, BLACK_PAT);
|
||||
for (p, want) in &[
|
||||
// A stripe pattern is constant in y.
|
||||
(point(0., 0., 0.), WHITE),
|
||||
(point(0., 1., 0.), WHITE),
|
||||
(point(0., 2., 0.), WHITE),
|
||||
// A stripe pattern is constant in z.
|
||||
(point(0., 0., 0.), WHITE),
|
||||
(point(0., 0., 1.), WHITE),
|
||||
(point(0., 0., 2.), WHITE),
|
||||
// A stripe pattern alternates in z.
|
||||
(point(0., 0., 0.), WHITE),
|
||||
(point(0.9, 0., 0.), WHITE),
|
||||
(point(1., 0., 0.), BLACK),
|
||||
(point(-0.1, 0., 0.), BLACK),
|
||||
(point(-1., 0., 0.), BLACK),
|
||||
(point(-1.1, 0., 0.), WHITE),
|
||||
] {
|
||||
assert_eq!(pattern.pattern_at(*p), *want, "{:?}", p);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gradient_linearly_interpolates_between_colors() {
|
||||
// A gradient linearly interpolates between two colors.
|
||||
let pattern = Pattern::gradient(WHITE_PAT, BLACK_PAT);
|
||||
assert_eq!(pattern.pattern_at(point(0., 0., 0.)), WHITE);
|
||||
assert_eq!(
|
||||
pattern.pattern_at(point(0.25, 0., 0.)),
|
||||
[0.75, 0.75, 0.75].into()
|
||||
);
|
||||
assert_eq!(
|
||||
pattern.pattern_at(point(0.5, 0., 0.)),
|
||||
[0.5, 0.5, 0.5].into()
|
||||
);
|
||||
assert_eq!(
|
||||
pattern.pattern_at(point(0.75, 0., 0.)),
|
||||
[0.25, 0.25, 0.25].into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ring_extend_in_x_and_z() {
|
||||
// A ring should extend both in x and z.
|
||||
let pattern = Pattern::ring(WHITE_PAT, BLACK_PAT);
|
||||
assert_eq!(pattern.pattern_at(point(0., 0., 0.)), WHITE);
|
||||
assert_eq!(pattern.pattern_at(point(1., 0., 0.)), BLACK);
|
||||
assert_eq!(pattern.pattern_at(point(0., 0., 1.)), BLACK);
|
||||
// 0.708 is slight more than 2.sqrt()/2.
|
||||
assert_eq!(pattern.pattern_at(point(0.708, 0., 0.708)), BLACK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checkers_repeat_along_x_axis() {
|
||||
// Checkers should repeat along X-axis.
|
||||
let pattern = Pattern::checkers(WHITE_PAT, BLACK_PAT);
|
||||
assert_eq!(pattern.pattern_at(point(0., 0., 0.)), WHITE);
|
||||
assert_eq!(pattern.pattern_at(point(0.99, 0., 0.)), WHITE);
|
||||
assert_eq!(pattern.pattern_at(point(1.01, 0., 0.)), BLACK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checkers_repeat_along_y_axis() {
|
||||
// Checkers should repeat along Y-axis.
|
||||
let pattern = Pattern::checkers(WHITE_PAT, BLACK_PAT);
|
||||
assert_eq!(pattern.pattern_at(point(0., 0., 0.)), WHITE);
|
||||
assert_eq!(pattern.pattern_at(point(0., 0.99, 0.)), WHITE);
|
||||
assert_eq!(pattern.pattern_at(point(0., 1.01, 0.)), BLACK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checkers_repeat_along_z_axis() {
|
||||
// Checkers should repeat along Z-axis.
|
||||
let pattern = Pattern::checkers(WHITE_PAT, BLACK_PAT);
|
||||
assert_eq!(pattern.pattern_at(point(0., 0., 0.)), WHITE);
|
||||
assert_eq!(pattern.pattern_at(point(0., 0., 0.99)), WHITE);
|
||||
assert_eq!(pattern.pattern_at(point(0., 0., 1.01)), BLACK);
|
||||
}
|
||||
}
|
||||
|
||||
mod pattern_at_object {
|
||||
use std::error::Error;
|
||||
|
||||
use crate::{
|
||||
matrices::scaling,
|
||||
patterns::{stripe_pattern, BLACK_PAT, WHITE_PAT},
|
||||
shapes::{Shape, ShapeBuilder},
|
||||
tuples::point,
|
||||
WHITE,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn stripes_with_an_object_transformation() -> Result<(), Box<dyn Error>> {
|
||||
// Stripes with an object transformation.
|
||||
let object = ShapeBuilder::sphere()
|
||||
.transform(scaling(2., 2., 2.))
|
||||
.build()?;
|
||||
let pattern = stripe_pattern(WHITE_PAT, BLACK_PAT).build()?;
|
||||
let c = pattern.pattern_at_object(&object, point(1.5, 0., 0.));
|
||||
assert_eq!(c, WHITE);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stripes_with_a_pattern_transformation() -> Result<(), Box<dyn Error>> {
|
||||
// Stripes with a pattern transformation.
|
||||
let object = Shape::sphere();
|
||||
let mut pattern = stripe_pattern(WHITE_PAT, BLACK_PAT).build()?;
|
||||
pattern.set_transform(scaling(2., 2., 2.));
|
||||
let c = pattern.pattern_at_object(&object, point(1.5, 0., 0.));
|
||||
assert_eq!(c, WHITE);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
74
rtchallenge/src/rays.rs
Normal file
74
rtchallenge/src/rays.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
use crate::{matrices::Matrix4x4, tuples::Tuple, Float};
|
||||
|
||||
/// Rays have an origin and a direction. This datatype is the 'ray' in 'raytracer'.
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct Ray {
|
||||
pub origin: Tuple,
|
||||
pub direction: Tuple,
|
||||
}
|
||||
|
||||
impl Ray {
|
||||
/// Create a ray with the given origin point and direction vector.
|
||||
/// Will panic if origin not a point or direction not a vector.
|
||||
pub fn new(origin: Tuple, direction: Tuple) -> Ray {
|
||||
assert!(origin.is_point(), "Ray origin must be a point");
|
||||
assert!(direction.is_vector(), "Ray direction must be a vector");
|
||||
Ray { origin, direction }
|
||||
}
|
||||
|
||||
/// Compute a point from the given distance along the `Ray`.
|
||||
pub fn position(&self, t: Float) -> Tuple {
|
||||
self.origin + self.direction * t
|
||||
}
|
||||
|
||||
/// Apply Matrix4x4 transforms to Ray.
|
||||
pub fn transform(&self, m: Matrix4x4) -> Ray {
|
||||
Ray {
|
||||
origin: m * self.origin,
|
||||
direction: m * self.direction,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
matrices::Matrix4x4,
|
||||
rays::Ray,
|
||||
tuples::{point, vector},
|
||||
};
|
||||
#[test]
|
||||
fn create() {
|
||||
let origin = point(1., 2., 3.);
|
||||
let direction = vector(4., 5., 6.);
|
||||
let r = Ray::new(origin, direction);
|
||||
assert_eq!(r.origin, origin);
|
||||
assert_eq!(r.direction, direction);
|
||||
}
|
||||
#[test]
|
||||
fn position() {
|
||||
let r = Ray::new(point(2., 3., 4.), vector(1., 0., 0.));
|
||||
assert_eq!(r.position(0.), point(2., 3., 4.));
|
||||
assert_eq!(r.position(1.), point(3., 3., 4.));
|
||||
assert_eq!(r.position(-1.), point(1., 3., 4.));
|
||||
assert_eq!(r.position(2.5), point(4.5, 3., 4.));
|
||||
}
|
||||
#[test]
|
||||
fn transform_translating_ray() {
|
||||
// Translating a ray
|
||||
let r = Ray::new(point(1., 2., 3.), vector(0., 1., 0.));
|
||||
let m = Matrix4x4::translation(3., 4., 5.);
|
||||
let r2 = r.transform(m);
|
||||
assert_eq!(r2.origin, point(4., 6., 8.));
|
||||
assert_eq!(r2.direction, vector(0., 1., 0.));
|
||||
}
|
||||
#[test]
|
||||
fn transform_scaling_ray() {
|
||||
// Scaling a ray
|
||||
let r = Ray::new(point(1., 2., 3.), vector(0., 1., 0.));
|
||||
let m = Matrix4x4::scaling(2., 3., 4.);
|
||||
let r2 = r.transform(m);
|
||||
assert_eq!(r2.origin, point(2., 6., 12.));
|
||||
assert_eq!(r2.direction, vector(0., 3., 0.));
|
||||
}
|
||||
}
|
||||
825
rtchallenge/src/shapes.rs
Normal file
825
rtchallenge/src/shapes.rs
Normal file
@@ -0,0 +1,825 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use derive_builder::Builder;
|
||||
|
||||
use crate::{
|
||||
intersections::Intersections,
|
||||
materials::{Material, MaterialBuilder},
|
||||
matrices::Matrix4x4,
|
||||
rays::Ray,
|
||||
tuples::Tuple,
|
||||
};
|
||||
|
||||
#[derive(Default, PartialEq, Debug, Clone)]
|
||||
pub struct TestData {
|
||||
pub saved_ray: Option<Ray>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Geometry {
|
||||
/// Shape with predictable normals useful for debugging.
|
||||
TestShape(Arc<Mutex<TestData>>),
|
||||
/// Sphere represents the unit-sphere (radius of unit 1.) at the origin 0., 0., 0.
|
||||
Sphere,
|
||||
/// Flat surface that extends infinitely in the XZ axes.
|
||||
Plane,
|
||||
/// AABB cube at origin from -1,1 in each direction.
|
||||
Cube,
|
||||
/// Takes shape from children held within.
|
||||
Group(Vec<Shape>),
|
||||
}
|
||||
|
||||
impl Default for Geometry {
|
||||
fn default() -> Geometry {
|
||||
Geometry::Sphere
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Geometry {
|
||||
fn eq(&self, rhs: &Geometry) -> bool {
|
||||
use Geometry::*;
|
||||
match (self, rhs) {
|
||||
(TestShape(l), TestShape(r)) => *l.lock().unwrap() == *r.lock().unwrap(),
|
||||
(Sphere, Sphere) => true,
|
||||
(Plane, Plane) => true,
|
||||
(Cube, Cube) => true,
|
||||
(Group(v1), Group(v2)) => v1 == v2,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Shape represents visible objects. A signal instance of Shape can generically represent one of
|
||||
/// many different shapes based on the value of it's geometry field. Users chose the shape by
|
||||
/// calling the appropriate constructor, i.e. [Shape::sphere].
|
||||
#[derive(Builder, Debug, Clone, PartialEq)]
|
||||
#[builder(default, pattern = "owned", build_fn(skip))]
|
||||
pub struct Shape {
|
||||
transform: Matrix4x4,
|
||||
inverse_transform: Matrix4x4,
|
||||
pub material: Material,
|
||||
geometry: Geometry,
|
||||
}
|
||||
|
||||
/// Short hand for creating a ShapeBuilder with a plane geometry.
|
||||
pub fn plane() -> ShapeBuilder {
|
||||
ShapeBuilder::plane()
|
||||
}
|
||||
/// Short hand for creating a ShapeBuilder with a sphere geometry.
|
||||
pub fn sphere() -> ShapeBuilder {
|
||||
ShapeBuilder::sphere()
|
||||
}
|
||||
|
||||
/// Short hand for creating a ShapeBuilder with a test shape geometry.
|
||||
pub fn test_shape() -> ShapeBuilder {
|
||||
ShapeBuilder::test_shape()
|
||||
}
|
||||
|
||||
/// Short hand for creating a ShapeBuilder with a cube geometry.
|
||||
pub fn cube() -> ShapeBuilder {
|
||||
ShapeBuilder::cube()
|
||||
}
|
||||
|
||||
/// Short hand for creating a ShapeBuilder with a group geometry.
|
||||
pub fn group() -> ShapeBuilder {
|
||||
ShapeBuilder::group()
|
||||
}
|
||||
|
||||
/// Helper for producing a sphere with a glassy material.
|
||||
pub fn glass_sphere() -> ShapeBuilder {
|
||||
ShapeBuilder::sphere().material(
|
||||
MaterialBuilder::default()
|
||||
.transparency(1.)
|
||||
.refractive_index(1.5)
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
impl ShapeBuilder {
|
||||
/// Short hand for creating a ShapeBuilder with a plane geometry.
|
||||
pub fn plane() -> ShapeBuilder {
|
||||
ShapeBuilder::default().geometry(Geometry::Plane)
|
||||
}
|
||||
/// Short hand for creating a ShapeBuilder with a sphere geometry.
|
||||
pub fn sphere() -> ShapeBuilder {
|
||||
ShapeBuilder::default().geometry(Geometry::Sphere)
|
||||
}
|
||||
/// Short hand for creating a ShapeBuilder with a test shape geometry.
|
||||
pub fn test_shape() -> ShapeBuilder {
|
||||
ShapeBuilder::default().geometry(Geometry::TestShape(Arc::new(Mutex::new(
|
||||
TestData::default(),
|
||||
))))
|
||||
}
|
||||
/// Short hand for creating a ShapeBuilder with a cube geometry.
|
||||
pub fn cube() -> ShapeBuilder {
|
||||
ShapeBuilder::default().geometry(Geometry::Cube)
|
||||
}
|
||||
/// Short hand for creating a ShapeBuilder with a group geometry.
|
||||
pub fn group() -> ShapeBuilder {
|
||||
ShapeBuilder::default().geometry(Geometry::Group(Vec::new()))
|
||||
}
|
||||
|
||||
/// Add child shapes, only valid for Group::Geometry.
|
||||
pub fn add_child(mut self, child: Shape) -> ShapeBuilder {
|
||||
if let Some(Geometry::Group(ref mut children)) = &mut self.geometry {
|
||||
children.push(child);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(&self) -> Result<Shape, ShapeBuilderError> {
|
||||
let mut s = Shape::default();
|
||||
|
||||
if let Some(transform) = &self.transform {
|
||||
s.set_transform(*transform);
|
||||
}
|
||||
if let Some(material) = &self.material {
|
||||
s.material = material.clone();
|
||||
};
|
||||
if let Some(geometry) = &self.geometry {
|
||||
s.geometry = geometry.clone();
|
||||
};
|
||||
let transform = s.transform();
|
||||
if let Geometry::Group(ref mut children) = s.geometry {
|
||||
children
|
||||
.iter_mut()
|
||||
.for_each(|c| c.set_transform(transform * c.transform()));
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Shape {
|
||||
fn default() -> Shape {
|
||||
Shape {
|
||||
transform: Matrix4x4::identity(),
|
||||
inverse_transform: Matrix4x4::identity(),
|
||||
material: Material::default(),
|
||||
geometry: Geometry::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Shape {
|
||||
/// Create a test shape useful for debugging.
|
||||
pub fn test_shape() -> Shape {
|
||||
Shape {
|
||||
transform: Matrix4x4::identity(),
|
||||
inverse_transform: Matrix4x4::identity(),
|
||||
material: Material::default(),
|
||||
geometry: Geometry::TestShape(Arc::new(Mutex::new(TestData::default()))),
|
||||
}
|
||||
}
|
||||
pub fn sphere() -> Shape {
|
||||
Shape {
|
||||
transform: Matrix4x4::identity(),
|
||||
inverse_transform: Matrix4x4::identity(),
|
||||
material: Material::default(),
|
||||
geometry: Geometry::Sphere,
|
||||
}
|
||||
}
|
||||
pub fn plane() -> Shape {
|
||||
Shape {
|
||||
transform: Matrix4x4::identity(),
|
||||
inverse_transform: Matrix4x4::identity(),
|
||||
material: Material::default(),
|
||||
geometry: Geometry::Plane,
|
||||
}
|
||||
}
|
||||
pub fn cube() -> Shape {
|
||||
Shape {
|
||||
transform: Matrix4x4::identity(),
|
||||
inverse_transform: Matrix4x4::identity(),
|
||||
material: Material::default(),
|
||||
geometry: Geometry::Cube,
|
||||
}
|
||||
}
|
||||
pub fn group() -> Shape {
|
||||
Shape {
|
||||
transform: Matrix4x4::identity(),
|
||||
inverse_transform: Matrix4x4::identity(),
|
||||
material: Material::default(),
|
||||
geometry: Geometry::Group(Vec::new()),
|
||||
}
|
||||
}
|
||||
/// Find the normal at the point on the sphere.
|
||||
pub fn normal_at(&self, world_point: Tuple) -> Tuple {
|
||||
let object_point = self.inverse_transform * world_point;
|
||||
let object_normal = match self.geometry {
|
||||
Geometry::Sphere => object_point - Tuple::point(0., 0., 0.),
|
||||
Geometry::Plane => Tuple::vector(0., 1., 0.),
|
||||
Geometry::TestShape(_) => object_point,
|
||||
Geometry::Cube => cube::local_normal_at(object_point),
|
||||
Geometry::Group(_) => todo!("normal_at"),
|
||||
};
|
||||
let mut world_normal = self.inverse_transform.transpose() * object_normal;
|
||||
world_normal.w = 0.;
|
||||
world_normal.normalize()
|
||||
}
|
||||
|
||||
pub fn transform(&self) -> Matrix4x4 {
|
||||
self.transform
|
||||
}
|
||||
|
||||
pub fn inverse_transform(&self) -> Matrix4x4 {
|
||||
self.inverse_transform
|
||||
}
|
||||
|
||||
pub fn set_transform(&mut self, t: Matrix4x4) {
|
||||
self.transform = t;
|
||||
self.inverse_transform = t.inverse();
|
||||
}
|
||||
pub fn geometry(&self) -> &Geometry {
|
||||
&self.geometry
|
||||
}
|
||||
}
|
||||
|
||||
/// Intersect a ray with a shapes.
|
||||
pub fn intersect<'s>(shape: &'s Shape, ray: &Ray) -> Intersections<'s> {
|
||||
let local_ray = ray.transform(shape.inverse_transform);
|
||||
match shape.geometry {
|
||||
Geometry::Sphere => sphere::intersect(shape, &local_ray),
|
||||
Geometry::Plane => plane::intersect(shape, &local_ray),
|
||||
Geometry::TestShape(_) => test_shape::intersect(shape, &local_ray),
|
||||
Geometry::Cube => cube::intersect(shape, &local_ray),
|
||||
Geometry::Group(_) => group::intersect(shape, ray),
|
||||
}
|
||||
}
|
||||
|
||||
mod test_shape {
|
||||
use crate::{
|
||||
intersections::Intersections,
|
||||
rays::Ray,
|
||||
shapes::{Geometry, Shape},
|
||||
};
|
||||
pub fn intersect<'s>(shape: &'s Shape, ray: &Ray) -> Intersections<'s> {
|
||||
if let Geometry::TestShape(s) = &shape.geometry {
|
||||
s.lock()
|
||||
.expect("couldn't grab mutex for TestData")
|
||||
.saved_ray = Some(ray.clone());
|
||||
}
|
||||
Intersections::default()
|
||||
}
|
||||
}
|
||||
|
||||
mod sphere {
|
||||
use crate::{
|
||||
intersections::{Intersection, Intersections},
|
||||
rays::Ray,
|
||||
shapes::Shape,
|
||||
tuples::{dot, Tuple},
|
||||
};
|
||||
pub fn intersect<'s>(shape: &'s Shape, ray: &Ray) -> Intersections<'s> {
|
||||
let sphere_to_ray = ray.origin - Tuple::point(0., 0., 0.);
|
||||
let a = dot(ray.direction, ray.direction);
|
||||
let b = 2. * dot(ray.direction, sphere_to_ray);
|
||||
let c = dot(sphere_to_ray, sphere_to_ray) - 1.;
|
||||
let discriminant = b * b - 4. * a * c;
|
||||
if discriminant < 0. {
|
||||
return Intersections::default();
|
||||
}
|
||||
Intersections::new(vec![
|
||||
Intersection::new((-b - discriminant.sqrt()) / (2. * a), shape),
|
||||
Intersection::new((-b + discriminant.sqrt()) / (2. * a), shape),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
mod plane {
|
||||
use crate::{
|
||||
intersections::{Intersection, Intersections},
|
||||
rays::Ray,
|
||||
shapes::Shape,
|
||||
EPSILON,
|
||||
};
|
||||
pub fn intersect<'s>(shape: &'s Shape, ray: &Ray) -> Intersections<'s> {
|
||||
if (ray.direction.y).abs() < EPSILON {
|
||||
return Intersections::default();
|
||||
}
|
||||
Intersections::new(vec![Intersection::new(
|
||||
-ray.origin.y / ray.direction.y,
|
||||
shape,
|
||||
)])
|
||||
}
|
||||
}
|
||||
|
||||
mod cube {
|
||||
use crate::{
|
||||
intersections::{Intersection, Intersections},
|
||||
rays::Ray,
|
||||
shapes::Shape,
|
||||
tuples::{vector, Tuple},
|
||||
Float, EPSILON,
|
||||
};
|
||||
|
||||
fn check_axis(origin: Float, direction: Float) -> (Float, Float) {
|
||||
let tmin_numerator = -1. - origin;
|
||||
let tmax_numerator = 1. - origin;
|
||||
|
||||
let (tmin, tmax) = if direction.abs() >= EPSILON {
|
||||
(tmin_numerator / direction, tmax_numerator / direction)
|
||||
} else {
|
||||
(
|
||||
tmin_numerator * Float::INFINITY,
|
||||
tmax_numerator * Float::INFINITY,
|
||||
)
|
||||
};
|
||||
|
||||
if tmin > tmax {
|
||||
return (tmax, tmin);
|
||||
}
|
||||
(tmin, tmax)
|
||||
}
|
||||
|
||||
pub fn intersect<'s>(shape: &'s Shape, ray: &Ray) -> Intersections<'s> {
|
||||
let (xtmin, xtmax) = check_axis(ray.origin.x, ray.direction.x);
|
||||
let (ytmin, ytmax) = check_axis(ray.origin.y, ray.direction.y);
|
||||
let (ztmin, ztmax) = check_axis(ray.origin.z, ray.direction.z);
|
||||
|
||||
let tmin = xtmin.max(ytmin).max(ztmin);
|
||||
let tmax = xtmax.min(ytmax).min(ztmax);
|
||||
|
||||
if tmin > tmax {
|
||||
return Intersections::default();
|
||||
}
|
||||
Intersections::new(vec![
|
||||
Intersection::new(tmin, shape),
|
||||
Intersection::new(tmax, shape),
|
||||
])
|
||||
}
|
||||
pub fn local_normal_at(point: Tuple) -> Tuple {
|
||||
let x = point.x.abs();
|
||||
let y = point.y.abs();
|
||||
let z = point.z.abs();
|
||||
let maxc = x.max(y).max(z);
|
||||
if maxc == x {
|
||||
return vector(point.x, 0., 0.);
|
||||
}
|
||||
if maxc == y {
|
||||
return vector(0., point.y, 0.);
|
||||
}
|
||||
vector(0., 0., point.z)
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
rays::Ray,
|
||||
shapes::Shape,
|
||||
tuples::{point, vector},
|
||||
EPSILON,
|
||||
};
|
||||
|
||||
use super::{intersect, local_normal_at};
|
||||
|
||||
#[test]
|
||||
fn ray_intersects_cube() {
|
||||
let c = Shape::cube();
|
||||
for (name, o, d, t1, t2) in [
|
||||
("+x", point(5., 0.5, 0.), vector(-1., 0., 0.), 4., 6.),
|
||||
("-x", point(-5., 0.5, 0.), vector(1., 0., 0.), 4., 6.),
|
||||
("+y", point(0.5, 5., 0.), vector(0., -1., 0.), 4., 6.),
|
||||
("-y", point(0.5, -5., 0.), vector(0., 1., 0.), 4., 6.),
|
||||
("+z", point(0.5, 0., 5.), vector(0., 0., -1.), 4., 6.),
|
||||
("-z", point(0.5, 0., -5.), vector(0., 0., 1.), 4., 6.),
|
||||
("inside", point(0., 0.5, 0.), vector(0., 0., 1.), -1., 1.),
|
||||
] {
|
||||
let r = Ray::new(o, d);
|
||||
let xs = intersect(&c, &r);
|
||||
assert_eq!(xs.len(), 2, "{}", name);
|
||||
assert!((xs[0].t - t1).abs() < EPSILON, "{} t1 {}", name, xs[0].t);
|
||||
assert!((xs[1].t - t2).abs() < EPSILON, "{} t2 {}", name, xs[1].t);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ray_misses_cube() {
|
||||
let c = Shape::cube();
|
||||
for (o, d) in [
|
||||
(point(-2., 0., 0.), vector(0.2673, 0.5345, 0.8018)),
|
||||
(point(0., -2., 0.), vector(0.8018, 0.2673, 0.5345)),
|
||||
(point(0., 0., -2.), vector(0.5345, 0.8018, 0.2673)),
|
||||
(point(2., 0., 2.), vector(0., 0., -1.)),
|
||||
(point(0., 2., 2.), vector(0., -1., 0.)),
|
||||
(point(2., 2., -5.), vector(-1., 0., 0.)),
|
||||
] {
|
||||
let r = Ray::new(o, d);
|
||||
let xs = intersect(&c, &r);
|
||||
assert_eq!(xs.len(), 0, "({:?}, {:?})", o, d);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn normal_cube_surface() {
|
||||
for (p, n) in [
|
||||
(point(1., 0.8, -0.8), vector(1., 0., 0.)),
|
||||
(point(-1., -0.2, 0.9), vector(-1., 0., 0.)),
|
||||
(point(-0.4, 1., -0.1), vector(0., 1., 0.)),
|
||||
(point(0.3, -1., -0.7), vector(0., -1., 0.)),
|
||||
(point(-0.6, 0.3, 1.), vector(0., 0., 1.)),
|
||||
(point(0.4, 0.4, -1.), vector(0., 0., -1.)),
|
||||
(point(1., 1., 1.), vector(1., 0., 0.)),
|
||||
(point(-1., -1., -1.), vector(-1., 0., 0.)),
|
||||
] {
|
||||
let normal = local_normal_at(p);
|
||||
assert_eq!(n, normal);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod group {
|
||||
use crate::{intersections::Intersections, rays::Ray, shapes::Shape};
|
||||
|
||||
use super::Geometry;
|
||||
|
||||
pub fn intersect<'s>(shape: &'s Shape, ray: &Ray) -> Intersections<'s> {
|
||||
if let Geometry::Group(children) = &shape.geometry {
|
||||
let mut intersections: Vec<_> = children
|
||||
.iter()
|
||||
.flat_map(|c| super::intersect(c, ray))
|
||||
.collect();
|
||||
intersections.sort_by(|a, b| {
|
||||
a.t.partial_cmp(&b.t)
|
||||
.expect("an intersection has a t value that is NaN")
|
||||
});
|
||||
return Intersections::new(intersections);
|
||||
}
|
||||
unreachable!();
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
matrices::{scaling, translation},
|
||||
rays::Ray,
|
||||
shapes::{intersect, Shape, ShapeBuilder},
|
||||
tuples::{point, vector},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn intersecting_empty_group() {
|
||||
let g = Shape::group();
|
||||
let r = Ray::new(point(0., 0., 0.), vector(0., 0., 1.));
|
||||
let xs = intersect(&g, &r);
|
||||
assert_eq!(xs.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersecting_nonempty_group() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let s1 = Shape::sphere();
|
||||
let s2 = ShapeBuilder::sphere()
|
||||
.transform(translation(0., 0., -3.))
|
||||
.build()?;
|
||||
let s3 = ShapeBuilder::sphere()
|
||||
.transform(translation(5., 0., 0.))
|
||||
.build()?;
|
||||
let g = ShapeBuilder::group()
|
||||
.add_child(s1.clone())
|
||||
.add_child(s2.clone())
|
||||
.add_child(s3.clone())
|
||||
.build()?;
|
||||
let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.));
|
||||
let xs = intersect(&g, &r);
|
||||
assert_eq!(xs.len(), 4);
|
||||
assert_eq!(xs[0].object, &s2);
|
||||
assert_eq!(xs[1].object, &s2);
|
||||
assert_eq!(xs[2].object, &s1);
|
||||
assert_eq!(xs[3].object, &s1);
|
||||
Ok(())
|
||||
}
|
||||
#[test]
|
||||
fn intersecting_transformed_group() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let g = ShapeBuilder::group()
|
||||
.transform(scaling(2., 2., 2.))
|
||||
.add_child(
|
||||
ShapeBuilder::sphere()
|
||||
.transform(translation(5., 0., 0.))
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
let r = Ray::new(point(10., 0., -10.), vector(0., 0., 1.));
|
||||
let xs = intersect(&g, &r);
|
||||
assert_eq!(xs.len(), 2);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
mod shape_builder {
|
||||
use std::error::Error;
|
||||
|
||||
use crate::shapes::{plane, sphere, test_shape, Shape};
|
||||
|
||||
#[test]
|
||||
fn plane_builder() -> Result<(), Box<dyn Error>> {
|
||||
assert_eq!(plane().build()?, Shape::plane());
|
||||
Ok(())
|
||||
}
|
||||
#[test]
|
||||
fn sphere_builder() -> Result<(), Box<dyn Error>> {
|
||||
assert_eq!(sphere().build()?, Shape::sphere());
|
||||
Ok(())
|
||||
}
|
||||
#[test]
|
||||
fn test_shape_builder() -> Result<(), Box<dyn Error>> {
|
||||
assert_eq!(test_shape().build()?, Shape::test_shape());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
mod shape {
|
||||
use crate::{
|
||||
materials::Material,
|
||||
matrices::{identity, translation},
|
||||
shapes::Shape,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_shape() {
|
||||
let mut s = Shape::test_shape();
|
||||
// The default transform.
|
||||
assert_eq!(s.transform(), identity());
|
||||
// The default material.
|
||||
assert_eq!(s.material, Material::default());
|
||||
// Assigning a material.
|
||||
let m = Material {
|
||||
ambient: 1.,
|
||||
..Material::default()
|
||||
};
|
||||
s.material = m.clone();
|
||||
assert_eq!(s.material, m);
|
||||
}
|
||||
#[test]
|
||||
fn sphere() {
|
||||
// A sphere's default transform is the identity matrix.
|
||||
let s = Shape::sphere();
|
||||
assert_eq!(s.transform(), identity());
|
||||
|
||||
// It can be changed by directly setting the transform member.
|
||||
let mut s = Shape::sphere();
|
||||
let t = translation(2., 3., 4.);
|
||||
s.set_transform(t.clone());
|
||||
assert_eq!(s.transform(), t);
|
||||
|
||||
// Default Sphere has the default material.
|
||||
assert_eq!(s.material, Material::default());
|
||||
// It can be overridden.
|
||||
let mut s = Shape::sphere();
|
||||
let mut m = Material::default();
|
||||
m.ambient = 1.;
|
||||
s.material = m.clone();
|
||||
assert_eq!(s.material, m);
|
||||
}
|
||||
}
|
||||
mod normal_at {
|
||||
use crate::{float::consts::PI, matrices::Matrix4x4, shapes::Shape, tuples::Tuple, Float};
|
||||
#[test]
|
||||
fn compute_normal_on_translated_shape() {
|
||||
// Computing the normal on a translated shape.
|
||||
let mut s = Shape::test_shape();
|
||||
s.set_transform(Matrix4x4::translation(0., 1., 0.));
|
||||
let n = s.normal_at(Tuple::point(0., 1.70711, -0.70711));
|
||||
assert_eq!(n, Tuple::vector(0., 0.70711, -0.70711));
|
||||
}
|
||||
#[test]
|
||||
fn compute_normal_on_scaled_shape() {
|
||||
// Computing the normal on a scaled shape.
|
||||
let mut s = Shape::test_shape();
|
||||
s.set_transform(Matrix4x4::scaling(1., 0.5, 1.) * Matrix4x4::rotation_z(PI / 5.));
|
||||
let n = s.normal_at(Tuple::point(
|
||||
0.,
|
||||
(2. as Float).sqrt() / 2.,
|
||||
-(2. as Float).sqrt() / 2.,
|
||||
));
|
||||
assert_eq!(n, Tuple::vector(0., 0.97014, -0.24254));
|
||||
}
|
||||
#[test]
|
||||
fn sphere_normal_on_x_axis() {
|
||||
// Normal on X-axis
|
||||
let s = Shape::sphere();
|
||||
let n = s.normal_at(Tuple::point(1., 0., 0.));
|
||||
assert_eq!(n, Tuple::vector(1., 0., 0.));
|
||||
}
|
||||
#[test]
|
||||
fn sphere_normal_on_y_axis() {
|
||||
// Normal on Y-axis
|
||||
let s = Shape::sphere();
|
||||
let n = s.normal_at(Tuple::point(0., 1., 0.));
|
||||
assert_eq!(n, Tuple::vector(0., 1., 0.));
|
||||
}
|
||||
#[test]
|
||||
fn sphere_normal_on_z_axis() {
|
||||
// Normal on Z-axis
|
||||
let s = Shape::sphere();
|
||||
let n = s.normal_at(Tuple::point(0., 0., 1.));
|
||||
assert_eq!(n, Tuple::vector(0., 0., 1.));
|
||||
}
|
||||
#[test]
|
||||
fn sphere_normal_non_axial() {
|
||||
// Normal on a sphere at a nonaxial point.
|
||||
let s = Shape::sphere();
|
||||
let n = s.normal_at(Tuple::point(
|
||||
(3. as Float).sqrt() / 3.,
|
||||
(3. as Float).sqrt() / 3.,
|
||||
(3. as Float).sqrt() / 3.,
|
||||
));
|
||||
assert_eq!(
|
||||
n,
|
||||
Tuple::vector(
|
||||
(3. as Float).sqrt() / 3.,
|
||||
(3. as Float).sqrt() / 3.,
|
||||
(3. as Float).sqrt() / 3.,
|
||||
)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn normals_are_normalized() {
|
||||
// Normals returned are normalized.
|
||||
let s = Shape::sphere();
|
||||
let n = s.normal_at(Tuple::point(
|
||||
(3. as Float).sqrt() / 3.,
|
||||
(3. as Float).sqrt() / 3.,
|
||||
(3. as Float).sqrt() / 3.,
|
||||
));
|
||||
assert_eq!(n, n.normalize());
|
||||
}
|
||||
#[test]
|
||||
fn compute_normal_on_translated_sphere() {
|
||||
// Compute the normal on a translated sphere.
|
||||
let mut s = Shape::sphere();
|
||||
s.set_transform(Matrix4x4::translation(0., 1., 0.));
|
||||
let n = s.normal_at(Tuple::point(0., 1.70711, -0.70711));
|
||||
assert_eq!(n, Tuple::vector(0., 0.70711, -0.70711));
|
||||
}
|
||||
#[test]
|
||||
fn compute_normal_on_scaled_sphere() {
|
||||
// Compute the normal on a transformed sphere.
|
||||
let mut s = Shape::sphere();
|
||||
s.set_transform(Matrix4x4::scaling(1., 0.5, 1.) * Matrix4x4::rotation_z(PI / 5.));
|
||||
let n = s.normal_at(Tuple::point(
|
||||
0.,
|
||||
(2. as Float).sqrt() / 2.,
|
||||
-(2. as Float).sqrt() / 2.,
|
||||
));
|
||||
assert_eq!(n, Tuple::vector(0., 0.97014, -0.24254));
|
||||
}
|
||||
#[test]
|
||||
fn nomal_of_plane_constant() {
|
||||
// Normal of a plane is constant everywhere.
|
||||
let p = Shape::plane();
|
||||
assert_eq!(
|
||||
p.normal_at(Tuple::point(0., 0., 0.)),
|
||||
Tuple::vector(0., 1., 0.)
|
||||
);
|
||||
assert_eq!(
|
||||
p.normal_at(Tuple::point(10., 0., -10.)),
|
||||
Tuple::vector(0., 1., 0.)
|
||||
);
|
||||
assert_eq!(
|
||||
p.normal_at(Tuple::point(-5., 0., 150.)),
|
||||
Tuple::vector(0., 1., 0.)
|
||||
);
|
||||
}
|
||||
}
|
||||
mod intersect {
|
||||
use crate::{
|
||||
intersections::{Intersection, Intersections},
|
||||
matrices::Matrix4x4,
|
||||
rays::Ray,
|
||||
shapes::{intersect, Geometry, Shape},
|
||||
tuples::Tuple,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn scaled_shape_with_ray() {
|
||||
// Intersecting a scaled shape with a ray.
|
||||
let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
|
||||
let mut s = Shape::test_shape();
|
||||
s.set_transform(Matrix4x4::scaling(2., 2., 2.));
|
||||
let _xs = intersect(&s, &r);
|
||||
if let Geometry::TestShape(data) = s.geometry() {
|
||||
if let Some(ray) = &data.lock().unwrap().saved_ray {
|
||||
assert_eq!(ray.origin, Tuple::point(0., 0., -2.5));
|
||||
assert_eq!(ray.direction, Tuple::vector(0., 0., 0.5));
|
||||
} else {
|
||||
panic!("ray wasn't set");
|
||||
};
|
||||
} else {
|
||||
panic!("test_shape returned a non-TestShape geometry")
|
||||
};
|
||||
}
|
||||
#[test]
|
||||
fn translated_shape_with_ray() {
|
||||
// Intersecting a translated shape with a ray.
|
||||
let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
|
||||
let mut s = Shape::test_shape();
|
||||
s.set_transform(Matrix4x4::translation(5., 0., 0.));
|
||||
let _xs = intersect(&s, &r);
|
||||
if let Geometry::TestShape(data) = s.geometry() {
|
||||
if let Some(ray) = &data.lock().unwrap().saved_ray {
|
||||
assert_eq!(ray.origin, Tuple::point(-5., 0., -5.));
|
||||
assert_eq!(ray.direction, Tuple::vector(0., 0., 1.));
|
||||
} else {
|
||||
panic!("ray wasn't set");
|
||||
};
|
||||
} else {
|
||||
panic!("test_shape returned a non-TestShape geometry")
|
||||
};
|
||||
}
|
||||
#[test]
|
||||
fn ray_intersects_sphere_two_points() {
|
||||
// A ray intersects a sphere in two points.
|
||||
let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
|
||||
let s = Shape::sphere();
|
||||
let xs = intersect(&s, &r);
|
||||
assert_eq!(
|
||||
xs,
|
||||
Intersections::new(vec![Intersection::new(4., &s), Intersection::new(6., &s)])
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn ray_intersects_at_tangent() {
|
||||
// A ray intersects a sphere at a tangent.
|
||||
let r = Ray::new(Tuple::point(0., 2., -5.), Tuple::vector(0., 0., 1.));
|
||||
let s = Shape::sphere();
|
||||
let xs = intersect(&s, &r);
|
||||
assert_eq!(xs, Intersections::default());
|
||||
}
|
||||
#[test]
|
||||
fn ray_originates_inside_sphere() {
|
||||
// A ray originates inside a sphere.
|
||||
let r = Ray::new(Tuple::point(0., 0., 0.), Tuple::vector(0., 0., 1.));
|
||||
let s = Shape::sphere();
|
||||
let xs = intersect(&s, &r);
|
||||
assert_eq!(
|
||||
xs,
|
||||
Intersections::new(vec![Intersection::new(-1., &s), Intersection::new(1., &s)])
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn sphere_behind_ray() {
|
||||
// A sphere is behind a ray.
|
||||
let r = Ray::new(Tuple::point(0., 0., 5.), Tuple::vector(0., 0., 1.));
|
||||
let s = Shape::sphere();
|
||||
let xs = intersect(&s, &r);
|
||||
assert_eq!(
|
||||
xs,
|
||||
Intersections::new(vec![Intersection::new(-6., &s), Intersection::new(-4., &s)])
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn ray_intersects_scaled_sphere() {
|
||||
// Intersect a scaled sphere with a ray.
|
||||
let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
|
||||
let mut s = Shape::sphere();
|
||||
s.set_transform(Matrix4x4::scaling(2., 2., 2.));
|
||||
let xs = intersect(&s, &r);
|
||||
assert_eq!(xs.len(), 2, "xs {:?}", xs);
|
||||
assert_eq!(xs[0].t, 3., "xs {:?}", xs);
|
||||
assert_eq!(xs[1].t, 7., "xs {:?}", xs);
|
||||
}
|
||||
#[test]
|
||||
fn ray_intersects_translated_sphere() {
|
||||
// Intersect a translated sphere with a ray.
|
||||
let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
|
||||
let mut s = Shape::sphere();
|
||||
s.set_transform(Matrix4x4::translation(5., 0., 0.));
|
||||
let xs = intersect(&s, &r);
|
||||
assert_eq!(xs.len(), 0);
|
||||
}
|
||||
#[test]
|
||||
fn ray_parallel_to_plane() {
|
||||
// Intersect with a ray parallel to the plane.
|
||||
let p = Shape::plane();
|
||||
let r = Ray::new(Tuple::point(0., 10., 0.), Tuple::vector(0., 0., 1.));
|
||||
let xs = intersect(&p, &r);
|
||||
assert_eq!(xs.len(), 0);
|
||||
}
|
||||
#[test]
|
||||
fn ray_coplanar_to_plane() {
|
||||
// Intersect with a coplanar.
|
||||
let p = Shape::plane();
|
||||
let r = Ray::new(Tuple::point(0., 0., 0.), Tuple::vector(0., 0., 1.));
|
||||
let xs = intersect(&p, &r);
|
||||
assert_eq!(xs.len(), 0);
|
||||
}
|
||||
#[test]
|
||||
fn ray_intersects_plane_from_above() {
|
||||
// A ray intersecting a plane from above.
|
||||
let p = Shape::plane();
|
||||
let r = Ray::new(Tuple::point(0., 1., 0.), Tuple::vector(0., -1., 0.));
|
||||
let xs = intersect(&p, &r);
|
||||
assert_eq!(xs.len(), 1);
|
||||
assert_eq!(xs[0].t, 1.);
|
||||
assert_eq!(xs[0].object, &p);
|
||||
}
|
||||
#[test]
|
||||
fn ray_intersects_plane_from_below() {
|
||||
// A ray intersecting a plane from below.
|
||||
let p = Shape::plane();
|
||||
let r = Ray::new(Tuple::point(0., -1., 0.), Tuple::vector(0., 1., 0.));
|
||||
let xs = intersect(&p, &r);
|
||||
assert_eq!(xs.len(), 1);
|
||||
assert_eq!(xs[0].t, 1.);
|
||||
assert_eq!(xs[0].object, &p);
|
||||
}
|
||||
}
|
||||
}
|
||||
71
rtchallenge/src/transformations.rs
Normal file
71
rtchallenge/src/transformations.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use crate::{
|
||||
matrices::Matrix4x4,
|
||||
tuples::{cross, Tuple},
|
||||
};
|
||||
|
||||
/// Create a matrix representing a eye at `from` looking at `to`, with an `up`
|
||||
/// as the up vector.
|
||||
pub fn view_transform(from: Tuple, to: Tuple, up: Tuple) -> Matrix4x4 {
|
||||
let forward = (to - from).normalize();
|
||||
let left = cross(forward, up.normalize());
|
||||
let true_up = cross(left, forward);
|
||||
Matrix4x4::new(
|
||||
[left.x, left.y, left.z, 0.],
|
||||
[true_up.x, true_up.y, true_up.z, 0.],
|
||||
[-forward.x, -forward.y, -forward.z, 0.],
|
||||
[0., 0., 0., 1.],
|
||||
) * Matrix4x4::translation(-from.x, -from.y, -from.z)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
matrices::{identity, scaling, translation, Matrix4x4},
|
||||
transformations::view_transform,
|
||||
tuples::{point, vector},
|
||||
};
|
||||
#[test]
|
||||
fn default_orientation() {
|
||||
// The transformation matrix for the default orientation.
|
||||
let from = point(0., 0., 0.);
|
||||
let to = point(0., 0., -1.);
|
||||
let up = vector(0., 1., 0.);
|
||||
let t = view_transform(from, to, up);
|
||||
assert_eq!(t, identity());
|
||||
}
|
||||
#[test]
|
||||
fn looking_positive_z() {
|
||||
// A view transformation matrix looking in positive z direction.
|
||||
let from = point(0., 0., 0.);
|
||||
let to = point(0., 0., 1.);
|
||||
let up = vector(0., 1., 0.);
|
||||
let t = view_transform(from, to, up);
|
||||
assert_eq!(t, scaling(-1., 1., -1.));
|
||||
}
|
||||
#[test]
|
||||
fn transformation_moves_world() {
|
||||
// The view transformation moves the world.
|
||||
let from = point(0., 0., 8.);
|
||||
let to = point(0., 0., 0.);
|
||||
let up = vector(0., 1., 0.);
|
||||
let t = view_transform(from, to, up);
|
||||
assert_eq!(t, translation(0., 0., -8.));
|
||||
}
|
||||
#[test]
|
||||
fn arbitrary_view() {
|
||||
// An arbitrary view transformation.
|
||||
let from = point(1., 3., 2.);
|
||||
let to = point(4., -2., 8.);
|
||||
let up = vector(1., 1., 0.);
|
||||
let t = view_transform(from, to, up);
|
||||
assert_eq!(
|
||||
t,
|
||||
Matrix4x4::new(
|
||||
[-0.50709, 0.50709, 0.67612, -2.36643],
|
||||
[0.76772, 0.60609, 0.12122, -2.82843],
|
||||
[-0.35857, 0.59761, -0.71714, 0.],
|
||||
[0., 0., 0., 1.],
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
439
rtchallenge/src/tuples.rs
Normal file
439
rtchallenge/src/tuples.rs
Normal file
@@ -0,0 +1,439 @@
|
||||
use std::ops::{Add, Div, Mul, Neg, Sub};
|
||||
|
||||
use crate::{Float, EPSILON};
|
||||
|
||||
/// Short hand for creating a Tuple that represents a point, w=1.
|
||||
pub fn point(x: Float, y: Float, z: Float) -> Tuple {
|
||||
Tuple::point(x, y, z)
|
||||
}
|
||||
|
||||
/// Short hand for creating a Tuple that represents a vector, w=0.
|
||||
pub fn vector(x: Float, y: Float, z: Float) -> Tuple {
|
||||
Tuple::vector(x, y, z)
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Copy, Clone)]
|
||||
pub struct Tuple {
|
||||
pub x: Float,
|
||||
pub y: Float,
|
||||
pub z: Float,
|
||||
pub w: Float,
|
||||
}
|
||||
|
||||
impl Tuple {
|
||||
pub fn point(x: Float, y: Float, z: Float) -> Tuple {
|
||||
Tuple::new(x, y, z, 1.0)
|
||||
}
|
||||
|
||||
pub fn vector(x: Float, y: Float, z: Float) -> Tuple {
|
||||
Tuple::new(x, y, z, 0.0)
|
||||
}
|
||||
|
||||
pub fn new(x: Float, y: Float, z: Float, w: Float) -> Tuple {
|
||||
Tuple { x, y, z, w }
|
||||
}
|
||||
|
||||
pub fn is_point(&self) -> bool {
|
||||
self.w == 1.0
|
||||
}
|
||||
|
||||
pub fn is_vector(&self) -> bool {
|
||||
self.w == 0.0
|
||||
}
|
||||
|
||||
pub fn magnitude(&self) -> Float {
|
||||
(self.x * self.x + self.y * self.y + self.z * self.z + self.w * self.w).sqrt()
|
||||
}
|
||||
|
||||
pub fn normalize(&self) -> Tuple {
|
||||
let m = self.magnitude();
|
||||
Tuple {
|
||||
x: self.x / m,
|
||||
y: self.y / m,
|
||||
z: self.z / m,
|
||||
w: self.w / m,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reflects vector v across normal n.
|
||||
pub fn reflect(v: Tuple, n: Tuple) -> Tuple {
|
||||
v - n * 2. * dot(v, n)
|
||||
}
|
||||
|
||||
impl Add for Tuple {
|
||||
type Output = Self;
|
||||
fn add(self, other: Self) -> Self {
|
||||
Self {
|
||||
x: self.x + other.x,
|
||||
y: self.y + other.y,
|
||||
z: self.z + other.z,
|
||||
w: self.w + other.w,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Div<Float> for Tuple {
|
||||
type Output = Self;
|
||||
fn div(self, rhs: Float) -> Self::Output {
|
||||
Self::Output {
|
||||
x: self.x / rhs,
|
||||
y: self.y / rhs,
|
||||
z: self.z / rhs,
|
||||
w: self.w / rhs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Float> for Tuple {
|
||||
type Output = Self;
|
||||
fn mul(self, rhs: Float) -> Self::Output {
|
||||
Self::Output {
|
||||
x: self.x * rhs,
|
||||
y: self.y * rhs,
|
||||
z: self.z * rhs,
|
||||
w: self.w * rhs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Tuple> for Float {
|
||||
type Output = Tuple;
|
||||
fn mul(self, rhs: Tuple) -> Self::Output {
|
||||
Self::Output {
|
||||
x: self * rhs.x,
|
||||
y: self * rhs.y,
|
||||
z: self * rhs.z,
|
||||
w: self * rhs.w,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Neg for Tuple {
|
||||
type Output = Self;
|
||||
fn neg(self) -> Self::Output {
|
||||
Self {
|
||||
x: -self.x,
|
||||
y: -self.y,
|
||||
z: -self.z,
|
||||
w: -self.w,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Sub for Tuple {
|
||||
type Output = Self;
|
||||
fn sub(self, other: Self) -> Self {
|
||||
Self {
|
||||
x: self.x - other.x,
|
||||
y: self.y - other.y,
|
||||
z: self.z - other.z,
|
||||
w: self.w - other.w,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Tuple {
|
||||
fn eq(&self, rhs: &Tuple) -> bool {
|
||||
((self.x - rhs.x).abs() < EPSILON)
|
||||
&& ((self.y - rhs.y).abs() < EPSILON)
|
||||
&& ((self.z - rhs.z).abs() < EPSILON)
|
||||
&& ((self.w - rhs.w).abs() < EPSILON)
|
||||
}
|
||||
}
|
||||
pub fn dot(a: Tuple, b: Tuple) -> Float {
|
||||
a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w
|
||||
}
|
||||
pub fn cross(a: Tuple, b: Tuple) -> Tuple {
|
||||
Tuple::vector(
|
||||
a.y * b.z - a.z * b.y,
|
||||
a.z * b.x - a.x * b.z,
|
||||
a.x * b.y - a.y * b.x,
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
pub struct Color {
|
||||
pub red: Float,
|
||||
pub green: Float,
|
||||
pub blue: Float,
|
||||
}
|
||||
|
||||
impl Color {
|
||||
pub const fn new(red: Float, green: Float, blue: Float) -> Color {
|
||||
Color { red, green, blue }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[Float; 3]> for Color {
|
||||
fn from(rgb: [Float; 3]) -> Self {
|
||||
Color {
|
||||
red: rgb[0],
|
||||
green: rgb[1],
|
||||
blue: rgb[2],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Color {
|
||||
fn eq(&self, rhs: &Color) -> bool {
|
||||
((self.red - rhs.red).abs() < EPSILON)
|
||||
&& ((self.green - rhs.green).abs() < EPSILON)
|
||||
&& ((self.blue - rhs.blue).abs() < EPSILON)
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for Color {
|
||||
type Output = Self;
|
||||
fn add(self, other: Self) -> Self {
|
||||
Self {
|
||||
red: self.red + other.red,
|
||||
green: self.green + other.green,
|
||||
blue: self.blue + other.blue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Div<Float> for Color {
|
||||
type Output = Self;
|
||||
fn div(self, rhs: Float) -> Self::Output {
|
||||
Self::Output {
|
||||
red: self.red / rhs,
|
||||
green: self.green / rhs,
|
||||
blue: self.blue / rhs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Float> for Color {
|
||||
type Output = Self;
|
||||
fn mul(self, rhs: Float) -> Self::Output {
|
||||
Self::Output {
|
||||
red: self.red * rhs,
|
||||
green: self.green * rhs,
|
||||
blue: self.blue * rhs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Color> for Float {
|
||||
type Output = Color;
|
||||
fn mul(self, rhs: Color) -> Self::Output {
|
||||
Self::Output {
|
||||
red: self * rhs.red,
|
||||
green: self * rhs.green,
|
||||
blue: self * rhs.blue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Color> for Color {
|
||||
type Output = Color;
|
||||
fn mul(self, rhs: Color) -> Self::Output {
|
||||
Self::Output {
|
||||
red: self.red * rhs.red,
|
||||
green: self.green * rhs.green,
|
||||
blue: self.blue * rhs.blue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Neg for Color {
|
||||
type Output = Self;
|
||||
fn neg(self) -> Self::Output {
|
||||
Self {
|
||||
red: -self.red,
|
||||
green: -self.green,
|
||||
blue: -self.blue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for Color {
|
||||
type Output = Self;
|
||||
fn sub(self, other: Self) -> Self {
|
||||
Self {
|
||||
red: self.red - other.red,
|
||||
green: self.green - other.green,
|
||||
blue: self.blue - other.blue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{cross, dot, reflect, Color, Float, Tuple, EPSILON};
|
||||
#[test]
|
||||
fn is_point() {
|
||||
// A tuple with w = 1 is a point
|
||||
let a = Tuple::new(4.3, -4.2, 3.1, 1.0);
|
||||
assert_eq!(a.x, 4.3);
|
||||
assert_eq!(a.y, -4.2);
|
||||
assert_eq!(a.z, 3.1);
|
||||
assert_eq!(a.w, 1.0);
|
||||
assert!(a.is_point());
|
||||
assert!(!a.is_vector());
|
||||
}
|
||||
#[test]
|
||||
fn is_vector() {
|
||||
// A tuple with w = 0 is a point
|
||||
let a = Tuple::new(4.3, -4.2, 3.1, 0.0);
|
||||
assert_eq!(a.x, 4.3);
|
||||
assert_eq!(a.y, -4.2);
|
||||
assert_eq!(a.z, 3.1);
|
||||
assert_eq!(a.w, 0.0);
|
||||
assert!(!a.is_point());
|
||||
assert!(a.is_vector());
|
||||
}
|
||||
#[test]
|
||||
fn point_tuple() {
|
||||
assert_eq!(Tuple::point(4., -4., 3.), Tuple::new(4., -4., 3., 1.))
|
||||
}
|
||||
#[test]
|
||||
fn vector_tuple() {
|
||||
assert_eq!(Tuple::vector(4., -4., 3.), Tuple::new(4., -4., 3., 0.))
|
||||
}
|
||||
#[test]
|
||||
fn add_two_tuples() {
|
||||
let a1 = Tuple::new(3., -2., 5., 1.);
|
||||
let a2 = Tuple::new(-2., 3., 1., 0.);
|
||||
assert_eq!(a1 + a2, Tuple::new(1., 1., 6., 1.));
|
||||
}
|
||||
#[test]
|
||||
fn sub_two_points() {
|
||||
let p1 = Tuple::point(3., 2., 1.);
|
||||
let p2 = Tuple::point(5., 6., 7.);
|
||||
assert_eq!(p1 - p2, Tuple::vector(-2., -4., -6.));
|
||||
}
|
||||
#[test]
|
||||
fn sub_vector_point() {
|
||||
let p = Tuple::point(3., 2., 1.);
|
||||
let v = Tuple::vector(5., 6., 7.);
|
||||
assert_eq!(p - v, Tuple::point(-2., -4., -6.));
|
||||
}
|
||||
#[test]
|
||||
fn sub_two_vectors() {
|
||||
let v1 = Tuple::vector(3., 2., 1.);
|
||||
let v2 = Tuple::vector(5., 6., 7.);
|
||||
assert_eq!(v1 - v2, Tuple::vector(-2., -4., -6.));
|
||||
}
|
||||
#[test]
|
||||
fn sub_zero_vector() {
|
||||
let zero = Tuple::vector(0., 0., 0.);
|
||||
let v = Tuple::vector(1., -2., 3.);
|
||||
assert_eq!(zero - v, Tuple::vector(-1., 2., -3.));
|
||||
}
|
||||
#[test]
|
||||
fn negate_tuple() {
|
||||
let a = Tuple::new(1., -2., 3., -4.);
|
||||
assert_eq!(-a, Tuple::new(-1., 2., -3., 4.));
|
||||
}
|
||||
#[test]
|
||||
fn mul_tuple_scalar() {
|
||||
let a = Tuple::new(1., -2., 3., -4.);
|
||||
assert_eq!(a * 3.5, Tuple::new(3.5, -7., 10.5, -14.));
|
||||
assert_eq!(3.5 * a, Tuple::new(3.5, -7., 10.5, -14.));
|
||||
}
|
||||
#[test]
|
||||
fn mul_tuple_fraction() {
|
||||
let a = Tuple::new(1., -2., 3., -4.);
|
||||
assert_eq!(a * 0.5, Tuple::new(0.5, -1., 1.5, -2.));
|
||||
assert_eq!(0.5 * a, Tuple::new(0.5, -1., 1.5, -2.));
|
||||
}
|
||||
#[test]
|
||||
fn div_tuple_scalar() {
|
||||
let a = Tuple::new(1., -2., 3., -4.);
|
||||
assert_eq!(a / 2., Tuple::new(0.5, -1., 1.5, -2.));
|
||||
}
|
||||
#[test]
|
||||
fn vector_magnitude() {
|
||||
assert_eq!(1., Tuple::vector(1., 0., 0.).magnitude());
|
||||
assert_eq!(1., Tuple::vector(0., 1., 0.).magnitude());
|
||||
assert_eq!(1., Tuple::vector(0., 0., 1.).magnitude());
|
||||
assert_eq!((14. as Float).sqrt(), Tuple::vector(1., 2., 3.).magnitude());
|
||||
assert_eq!(
|
||||
(14. as Float).sqrt(),
|
||||
Tuple::vector(-1., -2., -3.).magnitude()
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn vector_normalize() {
|
||||
assert_eq!(
|
||||
Tuple::vector(1., 0., 0.),
|
||||
Tuple::vector(4., 0., 0.).normalize()
|
||||
);
|
||||
assert_eq!(
|
||||
Tuple::vector(
|
||||
1. / (14. as Float).sqrt(),
|
||||
2. / (14. as Float).sqrt(),
|
||||
3. / (14. as Float).sqrt()
|
||||
),
|
||||
Tuple::vector(1., 2., 3.).normalize()
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn vector_normalize_magnitude() {
|
||||
let len = Tuple::vector(1., 2., 3.).normalize().magnitude();
|
||||
assert!((1. - len).abs() < EPSILON);
|
||||
}
|
||||
#[test]
|
||||
fn dot_two_tuples() {
|
||||
let a = Tuple::vector(1., 2., 3.);
|
||||
let b = Tuple::vector(2., 3., 4.);
|
||||
assert_eq!(20., dot(a, b));
|
||||
}
|
||||
#[test]
|
||||
fn cross_two_tuples() {
|
||||
let a = Tuple::vector(1., 2., 3.);
|
||||
let b = Tuple::vector(2., 3., 4.);
|
||||
assert_eq!(Tuple::vector(-1., 2., -1.), cross(a, b));
|
||||
assert_eq!(Tuple::vector(1., -2., 1.), cross(b, a));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_rgb() {
|
||||
let c = Color::new(-0.5, 0.4, 1.7);
|
||||
assert_eq!(c.red, -0.5);
|
||||
assert_eq!(c.green, 0.4);
|
||||
assert_eq!(c.blue, 1.7);
|
||||
}
|
||||
#[test]
|
||||
fn add_color() {
|
||||
let c1 = Color::new(0.9, 0.6, 0.75);
|
||||
let c2 = Color::new(0.7, 0.1, 0.25);
|
||||
assert_eq!(c1 + c2, Color::new(0.9 + 0.7, 0.6 + 0.1, 0.75 + 0.25));
|
||||
}
|
||||
#[test]
|
||||
fn sub_color() {
|
||||
let c1 = Color::new(0.9, 0.6, 0.75);
|
||||
let c2 = Color::new(0.7, 0.1, 0.25);
|
||||
assert_eq!(c1 - c2, Color::new(0.9 - 0.7, 0.6 - 0.1, 0.75 - 0.25));
|
||||
}
|
||||
#[test]
|
||||
fn mul_color_scalar() {
|
||||
let c = Color::new(0.2, 0.3, 0.4);
|
||||
assert_eq!(c * 2., Color::new(0.2 * 2., 0.3 * 2., 0.4 * 2.));
|
||||
assert_eq!(2. * c, Color::new(0.2 * 2., 0.3 * 2., 0.4 * 2.));
|
||||
}
|
||||
#[test]
|
||||
fn mul_colors() {
|
||||
let c1 = Color::new(1., 0.2, 0.4);
|
||||
let c2 = Color::new(0.9, 1., 0.1);
|
||||
assert_eq!(c1 * c2, Color::new(1.0 * 0.9, 0.2 * 1., 0.4 * 0.1));
|
||||
}
|
||||
#[test]
|
||||
fn reflect_approaching_at_45() {
|
||||
// Reflecting a vector approaching at 45°
|
||||
let v = Tuple::vector(1., -1., 0.);
|
||||
let n = Tuple::vector(0., 1., 0.);
|
||||
let r = reflect(v, n);
|
||||
assert_eq!(r, Tuple::vector(1., 1., 0.));
|
||||
}
|
||||
#[test]
|
||||
fn reflect_slanted_surface() {
|
||||
// Reflecting off a slanted surface.
|
||||
let v = Tuple::vector(0., -1., 0.);
|
||||
let n = Tuple::vector((2. as Float).sqrt() / 2., (2. as Float).sqrt() / 2., 0.);
|
||||
let r = reflect(v, n);
|
||||
assert_eq!(r, Tuple::vector(1., 0., 0.));
|
||||
}
|
||||
}
|
||||
555
rtchallenge/src/world.rs
Normal file
555
rtchallenge/src/world.rs
Normal file
@@ -0,0 +1,555 @@
|
||||
use derive_builder::Builder;
|
||||
|
||||
use crate::{
|
||||
intersections::{prepare_computations, schlick, Intersections, PrecomputedData},
|
||||
lights::PointLight,
|
||||
materials::{lighting, Material},
|
||||
matrices::Matrix4x4,
|
||||
rays::Ray,
|
||||
shapes::{intersect, Shape},
|
||||
tuples::{dot, Color, Tuple},
|
||||
BLACK, WHITE,
|
||||
};
|
||||
|
||||
/// World holds all drawable objects and the light(s) that illuminate them.
|
||||
#[derive(Builder, Clone, Debug, Default)]
|
||||
#[builder(default)]
|
||||
pub struct World {
|
||||
pub lights: Vec<PointLight>,
|
||||
pub objects: Vec<Shape>,
|
||||
}
|
||||
|
||||
impl World {
|
||||
/// Creates a world suitable for use across multiple tests in from the book.
|
||||
pub fn test_world() -> World {
|
||||
let light = PointLight::new(Tuple::point(-10., 10., -10.), WHITE);
|
||||
let mut s1 = Shape::sphere();
|
||||
s1.material = Material {
|
||||
color: [0.8, 1., 0.6].into(),
|
||||
diffuse: 0.7,
|
||||
specular: 0.2,
|
||||
..Material::default()
|
||||
};
|
||||
let mut s2 = Shape::sphere();
|
||||
s2.set_transform(Matrix4x4::scaling(0.5, 0.5, 0.5));
|
||||
World {
|
||||
lights: vec![light],
|
||||
objects: vec![s1, s2],
|
||||
}
|
||||
}
|
||||
|
||||
/// Intersects the ray with this world.
|
||||
pub fn intersect(&self, r: &Ray) -> Intersections {
|
||||
let mut xs: Vec<_> = self.objects.iter().flat_map(|o| intersect(o, r)).collect();
|
||||
xs.sort_by(|i1, i2| {
|
||||
i1.t.partial_cmp(&i2.t)
|
||||
.expect("an intersection has a t value that is NaN")
|
||||
});
|
||||
Intersections::new(xs)
|
||||
}
|
||||
|
||||
/// Compute shaded value for given precomputation.
|
||||
pub fn shade_hit(&self, comps: &PrecomputedData, remaining: usize) -> Color {
|
||||
let surface = self
|
||||
.lights
|
||||
.iter()
|
||||
.fold(Color::new(0., 0., 0.), |acc, light| {
|
||||
let shadowed = self.is_shadowed(comps.over_point, light);
|
||||
let surface = lighting(
|
||||
&comps.object.material,
|
||||
comps.object,
|
||||
light,
|
||||
comps.over_point,
|
||||
comps.eyev,
|
||||
comps.normalv,
|
||||
shadowed,
|
||||
);
|
||||
acc + surface
|
||||
});
|
||||
let reflected = self.reflected_color(comps, remaining);
|
||||
let refracted = self.refracted_color(comps, remaining);
|
||||
let material = &comps.object.material;
|
||||
if material.reflective > 0. && material.transparency > 0. {
|
||||
let reflectance = schlick(comps);
|
||||
surface + reflected * reflectance + refracted * (1. - reflectance)
|
||||
} else {
|
||||
surface + reflected + refracted
|
||||
}
|
||||
}
|
||||
/// Compute color for given ray fired at the world.
|
||||
pub fn color_at(&self, r: &Ray, remaining: usize) -> Color {
|
||||
let xs = self.intersect(r);
|
||||
match xs.hit() {
|
||||
Some(hit) => {
|
||||
let comps = prepare_computations(hit, r, &xs);
|
||||
self.shade_hit(&comps, remaining)
|
||||
}
|
||||
None => BLACK,
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine if point in world is in a shadow.
|
||||
pub fn is_shadowed(&self, point: Tuple, light: &PointLight) -> bool {
|
||||
let v = light.position - point;
|
||||
let distance = v.magnitude();
|
||||
let direction = v.normalize();
|
||||
|
||||
let r = Ray::new(point, direction);
|
||||
let intersections = self.intersect(&r);
|
||||
if let Some(h) = intersections.hit() {
|
||||
return h.t < distance;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Compute reflected color for the given precomputation.
|
||||
pub fn reflected_color(&self, comps: &PrecomputedData, remaining: usize) -> Color {
|
||||
if remaining == 0 {
|
||||
return BLACK;
|
||||
}
|
||||
if comps.object.material.reflective == 0. {
|
||||
return BLACK;
|
||||
}
|
||||
// Fire reflected ray to figure out color.
|
||||
let reflect_ray = Ray::new(comps.over_point, comps.reflectv);
|
||||
let color = self.color_at(&reflect_ray, remaining - 1);
|
||||
color * comps.object.material.reflective
|
||||
}
|
||||
|
||||
/// Compute refracted color for the given precomputation.
|
||||
pub fn refracted_color(&self, comps: &PrecomputedData, remaining: usize) -> Color {
|
||||
if remaining == 0 {
|
||||
return BLACK;
|
||||
}
|
||||
if comps.object.material.transparency == 0. {
|
||||
return BLACK;
|
||||
}
|
||||
// Find the ratio of the first index of refraction to the secon
|
||||
// (This is inverted from the definition of Snell's Law.)
|
||||
let n_ratio = comps.n1 / comps.n2;
|
||||
|
||||
// cos(theta_i) is the same as the dot product of the two vectors
|
||||
// TODO(wathiede): is cosine faster than doc productings?
|
||||
let cos_i = dot(comps.eyev, comps.normalv);
|
||||
|
||||
// Find the sin(theta_t)^2 via trigonometric identity.
|
||||
let sin2_t = n_ratio * n_ratio * (1. - cos_i * cos_i);
|
||||
|
||||
if sin2_t > 1. {
|
||||
// Total internal reflection.
|
||||
return BLACK;
|
||||
}
|
||||
// Find cos(theta_t) via trigonometric identity.
|
||||
let cos_t = (1. - sin2_t).sqrt();
|
||||
// Compute the direction of the refracted ray.
|
||||
let direction = comps.normalv * (n_ratio * cos_i - cos_t) - comps.eyev * n_ratio;
|
||||
// Create the refracted ray.
|
||||
let refract_ray = Ray::new(comps.under_point, direction);
|
||||
// Find he color of the refracted ray, making sure to multiply by the transparency value to
|
||||
// account for any opacity.
|
||||
self.color_at(&refract_ray, remaining - 1) * comps.object.material.transparency
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
float::consts::SQRT_2,
|
||||
intersections::{prepare_computations, Intersection, Intersections},
|
||||
lights::PointLight,
|
||||
materials::MaterialBuilder,
|
||||
matrices::translation,
|
||||
patterns::test_pattern,
|
||||
rays::Ray,
|
||||
shapes::{plane, sphere, Shape, ShapeBuilder},
|
||||
tuples::{point, vector},
|
||||
world::{World, WorldBuilder},
|
||||
Float, BLACK, WHITE,
|
||||
};
|
||||
|
||||
mod intersect {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn intersection_with_test_world() {
|
||||
let w = World::test_world();
|
||||
let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.));
|
||||
let xs = w.intersect(&r);
|
||||
assert_eq!(xs.len(), 4);
|
||||
assert_eq!(xs[0].t, 4.);
|
||||
assert_eq!(xs[1].t, 4.5);
|
||||
assert_eq!(xs[2].t, 5.5);
|
||||
assert_eq!(xs[3].t, 6.);
|
||||
}
|
||||
}
|
||||
mod world {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_world() {
|
||||
let w = World::default();
|
||||
assert!(w.objects.is_empty());
|
||||
assert_eq!(w.lights.len(), 0);
|
||||
}
|
||||
#[test]
|
||||
fn test_world() {
|
||||
let w = World::test_world();
|
||||
assert_eq!(w.objects.len(), 2);
|
||||
assert!(!w.lights.is_empty());
|
||||
}
|
||||
}
|
||||
mod shade_hit {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn shading_an_intersection() {
|
||||
// Shading an intersection.
|
||||
let w = World::test_world();
|
||||
let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.));
|
||||
let s = &w.objects[0];
|
||||
let xs = Intersections::from(Intersection::new(4., &s));
|
||||
let comps = prepare_computations(&xs[0], &r, &xs);
|
||||
let c = w.shade_hit(&comps, 1);
|
||||
assert_eq!(c, [0.38066, 0.47583, 0.2855].into());
|
||||
}
|
||||
#[test]
|
||||
fn shading_an_intersection_from_inside() {
|
||||
// Shading an intersection from the inside.
|
||||
let mut w = World::test_world();
|
||||
w.lights = vec![PointLight::new(point(0., 0.25, 0.), WHITE)];
|
||||
let r = Ray::new(point(0., 0., 0.), vector(0., 0., 1.));
|
||||
let s = &w.objects[1];
|
||||
let xs = Intersections::from(Intersection::new(0.5, &s));
|
||||
let comps = prepare_computations(&xs[0], &r, &xs);
|
||||
let c = w.shade_hit(&comps, 1);
|
||||
assert_eq!(c, [0.90498, 0.90498, 0.90498].into());
|
||||
}
|
||||
#[test]
|
||||
fn shading_an_intersection_in_shadow() {
|
||||
// Shading with an intersection in shadow.
|
||||
let mut w = World::default();
|
||||
w.lights = vec![PointLight::new(point(0., 0., -10.), WHITE)];
|
||||
let s1 = Shape::sphere();
|
||||
let mut s2 = Shape::sphere();
|
||||
s2.set_transform(translation(0., 0., 10.));
|
||||
w.objects = vec![s1, s2.clone()];
|
||||
let r = Ray::new(point(0., 0., 5.), vector(0., 0., 1.));
|
||||
let xs = Intersections::from(Intersection::new(4., &s2));
|
||||
let comps = prepare_computations(&xs[0], &r, &xs);
|
||||
let c = w.shade_hit(&comps, 1);
|
||||
assert_eq!(c, [0.1, 0.1, 0.1].into());
|
||||
}
|
||||
#[test]
|
||||
fn shading_with_a_reflective_material() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Shading with a reflective material.
|
||||
let mut w = World::test_world();
|
||||
let shape = ShapeBuilder::plane()
|
||||
.material(MaterialBuilder::default().reflective(0.5).build()?)
|
||||
.transform(translation(0., -1., 0.))
|
||||
.build()?;
|
||||
w.objects.push(shape.clone());
|
||||
let r = Ray::new(
|
||||
point(0., 0., -3.),
|
||||
vector(0., -(2 as Float).sqrt() / 2., (2 as Float).sqrt() / 2.),
|
||||
);
|
||||
let xs = Intersections::from(Intersection::new((2 as Float).sqrt(), &shape));
|
||||
let comps = prepare_computations(&xs[0], &r, &xs);
|
||||
let c = w.shade_hit(&comps, 1);
|
||||
assert_eq!(c, [0.87676, 0.92434, 0.82917].into());
|
||||
Ok(())
|
||||
}
|
||||
#[test]
|
||||
fn shading_with_a_transparent_material() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// shade_hit() with a transparent material.
|
||||
let mut w = World::test_world();
|
||||
let floor = plane()
|
||||
.transform(translation(0., -1., 0.))
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.transparency(0.5)
|
||||
.refractive_index(1.5)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
w.objects.push(floor.clone());
|
||||
let ball = sphere()
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color([1., 0., 0.])
|
||||
.ambient(0.5)
|
||||
.build()?,
|
||||
)
|
||||
.transform(translation(0., -3.5, -0.5))
|
||||
.build()?;
|
||||
w.objects.push(ball);
|
||||
let r = Ray::new(
|
||||
point(0., 0., -3.),
|
||||
vector(0., -(2 as Float).sqrt() / 2., (2 as Float).sqrt() / 2.),
|
||||
);
|
||||
let xs = Intersections::new(vec![Intersection::new((2 as Float).sqrt(), &floor)]);
|
||||
let comps = prepare_computations(&xs[0], &r, &xs);
|
||||
let c = w.shade_hit(&comps, 5);
|
||||
assert_eq!(c, [0.93642, 0.68642, 0.68643].into());
|
||||
Ok(())
|
||||
}
|
||||
#[test]
|
||||
fn reflective_transparent_material() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut w = World::test_world();
|
||||
let floor = plane()
|
||||
.transform(translation(0., -1., 0.))
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.reflective(0.5)
|
||||
.transparency(0.5)
|
||||
.refractive_index(1.5)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
w.objects.push(floor.clone());
|
||||
let ball = sphere()
|
||||
.transform(translation(0., -3.5, -0.5))
|
||||
.material(
|
||||
MaterialBuilder::default()
|
||||
.color([1., 0., 0.])
|
||||
.ambient(0.5)
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
w.objects.push(ball);
|
||||
let r = Ray::new(point(0., 0., -3.), vector(0., -SQRT_2 / 2., SQRT_2 / 2.));
|
||||
let xs = Intersections::new(vec![Intersection::new(SQRT_2, &floor)]);
|
||||
let comps = prepare_computations(&xs[0], &r, &xs);
|
||||
let color = w.shade_hit(&comps, 5);
|
||||
assert_eq!(color, [0.93391, 0.69643, 0.69243].into());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
mod color_at {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn color_when_ray_misses() {
|
||||
// The color when a ray misses.
|
||||
let w = World::test_world();
|
||||
let r = Ray::new(point(0., 0., -5.), vector(0., 1., 0.));
|
||||
let c = w.color_at(&r, 1);
|
||||
assert_eq!(c, BLACK);
|
||||
}
|
||||
#[test]
|
||||
fn color_when_ray_hits() {
|
||||
// The color when a ray hits.
|
||||
let w = World::test_world();
|
||||
let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.));
|
||||
let c = w.color_at(&r, 1);
|
||||
assert_eq!(c, [0.38066, 0.47583, 0.2855].into());
|
||||
}
|
||||
#[test]
|
||||
fn color_with_an_intersection_behind_ray() {
|
||||
// The color with an intersection behind the ray.
|
||||
let w = {
|
||||
let mut w = World::test_world();
|
||||
let mut outer = &mut w.objects[0];
|
||||
outer.material.ambient = 1.;
|
||||
let inner = &mut w.objects[1];
|
||||
inner.material.ambient = 1.;
|
||||
w
|
||||
};
|
||||
let r = Ray::new(point(0., 0., 0.75), vector(0., 0., -1.));
|
||||
let c = w.color_at(&r, 1);
|
||||
// inner.material.color is WHITE
|
||||
assert_eq!(c, WHITE);
|
||||
}
|
||||
#[test]
|
||||
fn ensure_mutually_reflective_surfaces_terminate() -> Result<(), Box<dyn std::error::Error>>
|
||||
{
|
||||
// Ensure mutually reflective surfaces don't infinitely recurse.
|
||||
let lower = ShapeBuilder::plane()
|
||||
.material(MaterialBuilder::default().reflective(1.).build()?)
|
||||
.transform(translation(0., -1., 0.))
|
||||
.build()?;
|
||||
let upper = ShapeBuilder::plane()
|
||||
.material(MaterialBuilder::default().reflective(1.).build()?)
|
||||
.transform(translation(0., 1., 0.))
|
||||
.build()?;
|
||||
let w = WorldBuilder::default()
|
||||
.lights(vec![PointLight::new(point(0., 0., 0.), WHITE)])
|
||||
.objects(vec![lower, upper])
|
||||
.build()?;
|
||||
let r = Ray::new(point(0., 0., 0.), vector(0., 1., 0.));
|
||||
// This should complete without stack overflow.
|
||||
w.color_at(&r, 1);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
mod is_shadowed {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn no_shadow_when_nothing_collinear_with_point_and_light() {
|
||||
// There is no shadow when nothing is collinear with point and light.
|
||||
let w = World::test_world();
|
||||
let light = &w.lights[0];
|
||||
|
||||
let p = point(0., 10., 0.);
|
||||
assert_eq!(w.is_shadowed(p, light), false);
|
||||
}
|
||||
#[test]
|
||||
fn shadow_when_object_between_point_and_light() {
|
||||
// The shadow when an object is between the point and the light.
|
||||
let w = World::test_world();
|
||||
let light = &w.lights[0];
|
||||
|
||||
let p = point(10., -10., 10.);
|
||||
assert_eq!(w.is_shadowed(p, light), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_shadow_when_object_behind_light() {
|
||||
// There is no shadow when an object is behind the light.
|
||||
let w = World::test_world();
|
||||
let light = &w.lights[0];
|
||||
|
||||
let p = point(-20., 20., -20.);
|
||||
assert_eq!(w.is_shadowed(p, light), false);
|
||||
}
|
||||
#[test]
|
||||
fn no_shadow_when_object_behind_point() {
|
||||
// There is no shadow when an object is behind the point.
|
||||
let w = World::test_world();
|
||||
let light = &w.lights[0];
|
||||
|
||||
let p = point(-2., 2., -2.);
|
||||
assert_eq!(w.is_shadowed(p, light), false);
|
||||
}
|
||||
}
|
||||
|
||||
mod reflected_color {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn reflected_color_for_nonreflective_material() {
|
||||
// The reflected color for a nonreflective material.
|
||||
let r = Ray::new(point(0., 0., 0.), vector(0., 0., 1.));
|
||||
let mut w = World::test_world();
|
||||
w.objects[1].material.ambient = 1.;
|
||||
let xs = Intersections::from(Intersection::new(1., &w.objects[1]));
|
||||
let comps = prepare_computations(&xs[0], &r, &xs);
|
||||
let c = w.reflected_color(&comps, 1);
|
||||
assert_eq!(c, BLACK);
|
||||
}
|
||||
#[test]
|
||||
fn reflected_color_for_reflective_material() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// The reflected color for a reflective material.
|
||||
let mut w = World::test_world();
|
||||
let shape = ShapeBuilder::plane()
|
||||
.material(MaterialBuilder::default().reflective(0.5).build()?)
|
||||
.transform(translation(0., -1., 0.))
|
||||
.build()?;
|
||||
w.objects.push(shape.clone());
|
||||
let r = Ray::new(
|
||||
point(0., 0., -3.),
|
||||
vector(0., -(2 as Float).sqrt() / 2., (2 as Float).sqrt() / 2.),
|
||||
);
|
||||
let xs = Intersections::from(Intersection::new((2 as Float).sqrt(), &shape));
|
||||
let comps = prepare_computations(&xs[0], &r, &xs);
|
||||
let c = w.reflected_color(&comps, 1);
|
||||
assert_eq!(c, [0.19033, 0.23791, 0.14274].into());
|
||||
Ok(())
|
||||
}
|
||||
#[test]
|
||||
fn reflected_color_at_maximum_recursion_depth() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// The reflected color at the maximum recursion depth.
|
||||
let mut w = World::test_world();
|
||||
let shape = ShapeBuilder::plane()
|
||||
.material(MaterialBuilder::default().reflective(0.5).build()?)
|
||||
.transform(translation(0., -1., 0.))
|
||||
.build()?;
|
||||
w.objects.push(shape.clone());
|
||||
let r = Ray::new(
|
||||
point(0., 0., -3.),
|
||||
vector(0., -(2 as Float).sqrt() / 2., (2 as Float).sqrt() / 2.),
|
||||
);
|
||||
let xs = Intersections::from(Intersection::new((2 as Float).sqrt(), &shape));
|
||||
let comps = prepare_computations(&xs[0], &r, &xs);
|
||||
let _ = w.reflected_color(&comps, 0);
|
||||
// Just needs to get here without infinite recursion.
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
mod refracted_color {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn refracted_color_with_opaque_surface() {
|
||||
// The refracted color with an opaque surface.
|
||||
let w = World::test_world();
|
||||
let shape = &w.objects[0];
|
||||
let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.));
|
||||
let xs = Intersections::new(vec![
|
||||
Intersection::new(4., &shape),
|
||||
Intersection::new(6., &shape),
|
||||
]);
|
||||
let comps = prepare_computations(&xs[0], &r, &xs);
|
||||
let c = w.refracted_color(&comps, 5);
|
||||
assert_eq!(c, BLACK);
|
||||
}
|
||||
#[test]
|
||||
fn refracted_color_at_maximum_recursion_depth() {
|
||||
// The refracted color at the maximum recursive depth.
|
||||
let mut w = World::test_world();
|
||||
w.objects[0].material.transparency = 1.0;
|
||||
w.objects[0].material.refractive_index = 1.5;
|
||||
let shape = &w.objects[0];
|
||||
let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.));
|
||||
let xs = Intersections::new(vec![
|
||||
Intersection::new(4., &shape),
|
||||
Intersection::new(6., &shape),
|
||||
]);
|
||||
let comps = prepare_computations(&xs[0], &r, &xs);
|
||||
let c = w.refracted_color(&comps, 0);
|
||||
assert_eq!(c, BLACK);
|
||||
}
|
||||
#[test]
|
||||
fn refracted_color_under_total_internal_reflection() {
|
||||
// The refracted color under total internal reflection.
|
||||
let mut w = World::test_world();
|
||||
w.objects[0].material.transparency = 1.0;
|
||||
w.objects[0].material.refractive_index = 1.5;
|
||||
let shape = &w.objects[0];
|
||||
let r = Ray::new(point(0., 0., (2 as Float).sqrt() / 2.), vector(0., 1., 0.));
|
||||
let xs = Intersections::new(vec![
|
||||
Intersection::new(-(2 as Float).sqrt() / 2., &shape),
|
||||
Intersection::new((2 as Float).sqrt() / 2., &shape),
|
||||
]);
|
||||
let comps = prepare_computations(&xs[1], &r, &xs);
|
||||
let c = w.refracted_color(&comps, 5);
|
||||
assert_eq!(c, BLACK);
|
||||
}
|
||||
#[test]
|
||||
fn refracted_color_with_a_refracted_ray() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// The refracted color with a refracted ray.
|
||||
let mut w = World::test_world();
|
||||
w.objects[0].material.ambient = 1.;
|
||||
w.objects[0].material.color = test_pattern().build()?;
|
||||
|
||||
w.objects[1].material.transparency = 1.;
|
||||
w.objects[1].material.refractive_index = 1.5;
|
||||
|
||||
let r = Ray::new(point(0., 0., 0.1), vector(0., 1., 0.));
|
||||
let a = &w.objects[0];
|
||||
let b = &w.objects[1];
|
||||
let xs = Intersections::new(vec![
|
||||
Intersection::new(-0.9899, &a),
|
||||
Intersection::new(-0.4899, &b),
|
||||
Intersection::new(0.4899, &b),
|
||||
Intersection::new(0.9899, &a),
|
||||
]);
|
||||
let comps = prepare_computations(&xs[2], &r, &xs);
|
||||
let c = w.refracted_color(&comps, 5);
|
||||
assert_eq!(c, [0., 0.99887, 0.04721].into());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
4057
rtiow/Cargo.lock
generated
4057
rtiow/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,41 +1,15 @@
|
||||
[package]
|
||||
authors = ["Bill Thiede <rust@xinu.tv>"]
|
||||
edition = "2018"
|
||||
name = "rtiow"
|
||||
version = "0.1.0"
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
|
||||
[[bench]]
|
||||
harness = false
|
||||
name = "spheres"
|
||||
|
||||
[dependencies]
|
||||
actix-web = "0.7.8"
|
||||
askama = "0.7.1"
|
||||
chrono = "*"
|
||||
cpuprofiler = { version = "0.0.3", optional = true }
|
||||
image = "0.19.0"
|
||||
human_format = "1.0.1"
|
||||
lazy_static = "1.1.0"
|
||||
log = "0.4.5"
|
||||
num_cpus = "1.8.0"
|
||||
rand = "0.5.5"
|
||||
serde = "1.0.79"
|
||||
serde_derive = "1.0.79"
|
||||
stderrlog = "0.4.1"
|
||||
structopt = "0.2.10"
|
||||
|
||||
[dependencies.prometheus]
|
||||
features = ["process", "push"]
|
||||
version = "0.7.0"
|
||||
optional = true
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = "0.2"
|
||||
|
||||
[profile]
|
||||
members = [
|
||||
"noise_explorer",
|
||||
"noise_explorer_warp",
|
||||
"renderer",
|
||||
"tracer",
|
||||
"vec3",
|
||||
]
|
||||
[profile.release]
|
||||
debug = true
|
||||
|
||||
[features]
|
||||
profile = ["cpuprofiler"]
|
||||
prom = ["prometheus"]
|
||||
[profile.dev]
|
||||
opt-level = 3
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
#[macro_use]
|
||||
extern crate criterion;
|
||||
|
||||
use criterion::Criterion;
|
||||
use criterion::ParameterizedBenchmark;
|
||||
|
||||
use rtiow::hitable::Hit;
|
||||
use rtiow::material::Lambertian;
|
||||
use rtiow::ray::Ray;
|
||||
use rtiow::sphere::Sphere;
|
||||
use rtiow::texture::ConstantTexture;
|
||||
use rtiow::vec3::Vec3;
|
||||
|
||||
fn criterion_benchmark(c: &mut Criterion) {
|
||||
let sphere = Sphere::new(
|
||||
Vec3::new(0., 0., 0.),
|
||||
1.,
|
||||
Lambertian::new(ConstantTexture::new([1., 0., 0.])),
|
||||
);
|
||||
let rays = vec![
|
||||
// Hit
|
||||
Ray::new([0., 0., -2.], [0., 0., 1.], 0.),
|
||||
// Miss
|
||||
Ray::new([0., 0., -2.], [0., 0., -1.], 0.),
|
||||
];
|
||||
c.bench(
|
||||
"sphere",
|
||||
ParameterizedBenchmark::new(
|
||||
"Sphere",
|
||||
move |b, r| b.iter(|| sphere.hit(*r, 0., 1.)),
|
||||
rays,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
criterion_group!(benches, criterion_benchmark);
|
||||
criterion_main!(benches);
|
||||
4
rtiow/build_all_features.sh
Executable file
4
rtiow/build_all_features.sh
Executable file
@@ -0,0 +1,4 @@
|
||||
set -e
|
||||
export RUSTFLAGS="-D warnings"
|
||||
cargo build --all
|
||||
cargo build --all --features=profile
|
||||
20
rtiow/noise_explorer/Cargo.toml
Normal file
20
rtiow/noise_explorer/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "noise_explorer"
|
||||
version = "0.1.0"
|
||||
authors = ["Bill Thiede <git@xinu.tv>"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
actix-web = "0.7.19"
|
||||
askama = "0.7.2"
|
||||
image = "0.22.5"
|
||||
log = "0.4.17"
|
||||
rand = "0.8.5"
|
||||
rand_xorshift = "0.3.0"
|
||||
renderer = { path = "../renderer" }
|
||||
serde = "1.0.152"
|
||||
serde_derive = "1.0.152"
|
||||
stderrlog = "0.4.3"
|
||||
structopt = "0.2.18"
|
||||
2
rtiow/noise_explorer/rustfmt.toml
Normal file
2
rtiow/noise_explorer/rustfmt.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
imports_granularity = "Crate"
|
||||
format_code_in_doc_comments = true
|
||||
@@ -1,56 +1,26 @@
|
||||
extern crate actix_web;
|
||||
extern crate askama;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate image;
|
||||
extern crate rand;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
extern crate serde;
|
||||
extern crate stderrlog;
|
||||
use std::{fmt, time::SystemTime};
|
||||
|
||||
extern crate structopt;
|
||||
|
||||
extern crate rtiow;
|
||||
|
||||
use std::fmt;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use actix_web::http;
|
||||
use actix_web::middleware;
|
||||
use actix_web::server;
|
||||
use actix_web::App;
|
||||
use actix_web::HttpRequest;
|
||||
use actix_web::HttpResponse;
|
||||
use actix_web::Path;
|
||||
use actix_web::Query;
|
||||
use actix_web::Result;
|
||||
use actix_web::{http, middleware, server, App, HttpRequest, HttpResponse, Path, Query, Result};
|
||||
use askama::Template;
|
||||
use log::info;
|
||||
use rand::SeedableRng;
|
||||
use rand::XorShiftRng;
|
||||
use rand_xorshift::XorShiftRng;
|
||||
use serde_derive::Deserialize;
|
||||
use structopt::StructOpt;
|
||||
|
||||
use rtiow::noise;
|
||||
use rtiow::noise::lode::Lode;
|
||||
use rtiow::noise::perlin::Perlin;
|
||||
use rtiow::noise::NoiseType;
|
||||
use rtiow::texture::NoiseTexture;
|
||||
use rtiow::texture::Texture;
|
||||
use rtiow::vec3::Vec3;
|
||||
use renderer::{
|
||||
noise,
|
||||
noise::{lode::Lode, perlin::Perlin, NoiseType},
|
||||
texture::{NoiseTexture, Texture},
|
||||
vec3::Vec3,
|
||||
};
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
#[derive(StructOpt)]
|
||||
#[structopt(name = "noise_explorer", about = "CLI for exploring Perlin noise")]
|
||||
struct Opt {
|
||||
/// HTTP listen address
|
||||
#[structopt(long = "addr", default_value = "0.0.0.0:8889")]
|
||||
pub addr: String,
|
||||
|
||||
/// Width of noise images
|
||||
#[structopt(short = "w", long = "width", default_value = "128")]
|
||||
pub width: u32,
|
||||
/// Height of noise images
|
||||
#[structopt(short = "h", long = "height", default_value = "128")]
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Deserialize, PartialEq)]
|
||||
@@ -75,11 +45,6 @@ struct OptionalParams {
|
||||
pixel_scale: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct NoiseSourceParam {
|
||||
noise_source: NoiseSource,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct NoiseParams {
|
||||
width: u32,
|
||||
@@ -151,13 +116,15 @@ fn render_noise(noise_params: NoiseParams) -> image::GrayImage {
|
||||
.optional
|
||||
.unwrap_or(OptionalParams { pixel_scale: 1.0 });
|
||||
const SEED: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
|
||||
let rng: &mut XorShiftRng = &mut SeedableRng::from_seed(SEED);
|
||||
let mut rng: XorShiftRng = SeedableRng::from_seed(SEED);
|
||||
let mut img = image::GrayImage::new(noise_params.width, noise_params.height);
|
||||
let tex: NoiseTexture<Box<noise::NoiseSource>> = match noise_params.noise_source {
|
||||
let tex: NoiseTexture<Box<dyn noise::NoiseSource>> = match noise_params.noise_source {
|
||||
NoiseSource::Perlin => {
|
||||
NoiseTexture::new(Box::new(Perlin::new(rng)), noise_params.noise_type)
|
||||
NoiseTexture::new(Box::new(Perlin::new(&mut rng)), noise_params.noise_type)
|
||||
}
|
||||
NoiseSource::Lode => {
|
||||
NoiseTexture::new(Box::new(Lode::new(&mut rng)), noise_params.noise_type)
|
||||
}
|
||||
NoiseSource::Lode => NoiseTexture::new(Box::new(Lode::new(rng)), noise_params.noise_type),
|
||||
};
|
||||
info!("{:?}", noise_params);
|
||||
|
||||
@@ -364,7 +331,7 @@ fn build_specimens(
|
||||
}
|
||||
|
||||
fn style(_req: &HttpRequest) -> Result<HttpResponse> {
|
||||
let bytes = include_bytes!("../../templates/style.css");
|
||||
let bytes = include_bytes!("../templates/style.css");
|
||||
Ok(HttpResponse::Ok().content_type("text/css").body(&bytes[..]))
|
||||
}
|
||||
|
||||
@@ -428,7 +395,6 @@ fn noise_marble(
|
||||
np: Path<NoiseParamsMarble>,
|
||||
optional: Option<Query<OptionalParams>>,
|
||||
) -> Result<HttpResponse> {
|
||||
info!("optional {:?}", optional);
|
||||
noise(NoiseParams {
|
||||
width: np.width,
|
||||
height: np.height,
|
||||
@@ -477,16 +443,9 @@ fn main() -> Result<(), std::io::Error> {
|
||||
mod tests {
|
||||
use std::str;
|
||||
|
||||
use actix_web::http::Method;
|
||||
use actix_web::test::TestServer;
|
||||
use actix_web::HttpMessage;
|
||||
use actix_web::Path;
|
||||
use actix_web::Query;
|
||||
use actix_web::{http::Method, test::TestServer, HttpMessage, Path, Query};
|
||||
|
||||
use super::NoiseParamsMarble;
|
||||
use super::NoiseParamsScale;
|
||||
use super::NoiseParamsTurbulence;
|
||||
use super::OptionalParams;
|
||||
use super::{NoiseParamsMarble, NoiseParamsScale, NoiseParamsTurbulence, OptionalParams};
|
||||
|
||||
#[test]
|
||||
fn noise_param_from_req() {
|
||||
19
rtiow/noise_explorer_warp/Cargo.toml
Normal file
19
rtiow/noise_explorer_warp/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "noise_explorer_warp"
|
||||
version = "0.1.0"
|
||||
authors = ["Bill Thiede <git@xinu.tv>"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
askama = "0.7.2"
|
||||
image = "0.22.5"
|
||||
log = "0.4.17"
|
||||
rand = "0.5.6"
|
||||
renderer = { path = "../renderer" }
|
||||
serde = "1.0.152"
|
||||
serde_derive = "1.0.152"
|
||||
stderrlog = "0.4.3"
|
||||
structopt = "0.2.18"
|
||||
warp = "0.1.23"
|
||||
2
rtiow/noise_explorer_warp/rustfmt.toml
Normal file
2
rtiow/noise_explorer_warp/rustfmt.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
imports_granularity = "Crate"
|
||||
format_code_in_doc_comments = true
|
||||
3
rtiow/noise_explorer_warp/src/main.rs
Normal file
3
rtiow/noise_explorer_warp/src/main.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
42
rtiow/renderer/Cargo.toml
Normal file
42
rtiow/renderer/Cargo.toml
Normal file
@@ -0,0 +1,42 @@
|
||||
[package]
|
||||
name = "renderer"
|
||||
version = "0.1.0"
|
||||
authors = ["Bill Thiede <git@xinu.tv>"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[[bench]]
|
||||
harness = false
|
||||
name = "spheres"
|
||||
[[bench]]
|
||||
harness = false
|
||||
name = "aabb"
|
||||
|
||||
[dependencies]
|
||||
chrono = "*"
|
||||
core_affinity = "0.5"
|
||||
cpuprofiler = { version = "0.0.3", optional = true }
|
||||
image = "0.22.5"
|
||||
lazy_static = "1.4.0"
|
||||
log = "0.4.17"
|
||||
num_cpus = "1.15.0"
|
||||
rand = "0.8.5"
|
||||
serde = "1.0.152"
|
||||
serde_derive = "1.0.152"
|
||||
serde_json = "1.0.93"
|
||||
structopt = "0.2.18"
|
||||
vec3 = {path = "../vec3"}
|
||||
stl = {path = "../../../stl"}
|
||||
strum = { version = "0.24.1", features = ["derive"] }
|
||||
strum_macros = "0.24.3"
|
||||
thiserror = "1.0.38"
|
||||
tev_client = "0.5.2"
|
||||
#stl = {git = "https://git-private.z.xinu.tv/wathiede/stl"}
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = "0.4"
|
||||
pretty_assertions = "1.3.0"
|
||||
proptest = "1.1.0"
|
||||
|
||||
[features]
|
||||
profile = ["cpuprofiler"]
|
||||
47
rtiow/renderer/benches/aabb.rs
Normal file
47
rtiow/renderer/benches/aabb.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use criterion::*;
|
||||
use renderer::{aabb::AABB, ray::Ray};
|
||||
|
||||
fn bench(c: &mut Criterion) {
|
||||
let bb = AABB::new([1., -1., -1.], [3., 1., 1.]);
|
||||
let r_hit = Ray::new([0., 0., 0.], [1., 0., 0.], 0.);
|
||||
let r_miss = Ray::new([0., 0., 0.], [-1., 0., 0.], 0.);
|
||||
let t_min = 0.001;
|
||||
let t_max = f32::MAX;
|
||||
|
||||
let mut group = c.benchmark_group("aabb");
|
||||
group.throughput(Throughput::Elements(1));
|
||||
group.bench_with_input(BenchmarkId::new("hit_naive", "r_hit"), &r_hit, |b, r| {
|
||||
b.iter(|| bb.hit_naive(*r, t_min, t_max))
|
||||
});
|
||||
group.bench_with_input(BenchmarkId::new("hit2", "r_hit"), &r_hit, |b, r| {
|
||||
b.iter(|| bb.hit2(*r, t_min, t_max))
|
||||
});
|
||||
//group.bench_with_input(BenchmarkId::new("hit_precompute", "r_hit"), &r_hit, |b, r| { b.iter(|| bb.hit_precompute(*r, t_min, t_max)) });
|
||||
group.bench_with_input(BenchmarkId::new("hit_fast", "r_hit"), &r_hit, |b, r| {
|
||||
b.iter(|| bb.hit_fast(*r, t_min, t_max))
|
||||
});
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
group.bench_with_input(BenchmarkId::new("hit_simd", "r_hit"), &r_hit, |b, r| {
|
||||
b.iter(|| bb.hit_simd(*r, t_min, t_max))
|
||||
});
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("hit_naive", "r_miss"), &r_miss, |b, r| {
|
||||
b.iter(|| bb.hit_naive(*r, t_min, t_max))
|
||||
});
|
||||
group.bench_with_input(BenchmarkId::new("hit2", "r_miss"), &r_miss, |b, r| {
|
||||
b.iter(|| bb.hit2(*r, t_min, t_max))
|
||||
});
|
||||
//group.bench_with_input(BenchmarkId::new("hit_precompute", "r_miss"), &r_miss, |b, r| { b.iter(|| bb.hit_precompute(*r, t_min, t_max)) });
|
||||
group.bench_with_input(BenchmarkId::new("hit_fast", "r_miss"), &r_miss, |b, r| {
|
||||
b.iter(|| bb.hit_fast(*r, t_min, t_max))
|
||||
});
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
group.bench_with_input(BenchmarkId::new("hit_simd", "r_miss"), &r_miss, |b, r| {
|
||||
b.iter(|| bb.hit_simd(*r, t_min, t_max))
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench);
|
||||
criterion_main!(benches);
|
||||
32
rtiow/renderer/benches/spheres.rs
Normal file
32
rtiow/renderer/benches/spheres.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use criterion::*;
|
||||
|
||||
use renderer::{
|
||||
hitable::Hit, material::Lambertian, ray::Ray, sphere::Sphere, texture::ConstantTexture,
|
||||
vec3::Vec3,
|
||||
};
|
||||
|
||||
fn criterion_benchmark(c: &mut Criterion) {
|
||||
let sphere = Sphere::new(
|
||||
Vec3::new(0., 0., 0.),
|
||||
1.,
|
||||
Lambertian::new(ConstantTexture::new([1., 0., 0.])),
|
||||
);
|
||||
let rays = vec![
|
||||
// Hit
|
||||
Ray::new([0., 0., -2.], [0., 0., 1.], 0.),
|
||||
// Miss
|
||||
Ray::new([0., 0., -2.], [0., 0., -1.], 0.),
|
||||
];
|
||||
let mut group = c.benchmark_group("sphere");
|
||||
group.throughput(Throughput::Elements(1));
|
||||
group.bench_with_input(BenchmarkId::new("Sphere", "hit"), &rays[0], |b, r| {
|
||||
b.iter(|| sphere.hit(*r, 0., 1.))
|
||||
});
|
||||
group.bench_with_input(BenchmarkId::new("Sphere", "miss"), &rays[1], |b, r| {
|
||||
b.iter(|| sphere.hit(*r, 0., 1.))
|
||||
});
|
||||
group.finish()
|
||||
}
|
||||
|
||||
criterion_group!(benches, criterion_benchmark);
|
||||
criterion_main!(benches);
|
||||
|
Before Width: | Height: | Size: 4.4 MiB After Width: | Height: | Size: 4.4 MiB |
|
Before Width: | Height: | Size: 295 KiB After Width: | Height: | Size: 295 KiB |
2
rtiow/renderer/rustfmt.toml
Normal file
2
rtiow/renderer/rustfmt.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
imports_granularity = "Crate"
|
||||
format_code_in_doc_comments = true
|
||||
377
rtiow/renderer/src/aabb.rs
Normal file
377
rtiow/renderer/src/aabb.rs
Normal file
@@ -0,0 +1,377 @@
|
||||
use std::fmt;
|
||||
|
||||
use crate::{ray::Ray, vec3::Vec3};
|
||||
|
||||
#[derive(Default, Copy, Clone, PartialEq)]
|
||||
pub struct AABB {
|
||||
bounds: [Vec3; 2],
|
||||
}
|
||||
|
||||
impl fmt::Display for AABB {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "({})-({})", self.bounds[0], self.bounds[1])
|
||||
}
|
||||
}
|
||||
|
||||
fn min(x: f32, y: f32) -> f32 {
|
||||
if x < y {
|
||||
x
|
||||
} else {
|
||||
y
|
||||
}
|
||||
}
|
||||
|
||||
fn max(x: f32, y: f32) -> f32 {
|
||||
if x > y {
|
||||
x
|
||||
} else {
|
||||
y
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for AABB {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"AABB: <{} - {}> Vol: {} Area: {}",
|
||||
self.bounds[0],
|
||||
self.bounds[1],
|
||||
self.volume(),
|
||||
self.area()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl AABB {
|
||||
// Create AABB with min = f32::MAX and max = -f32::MAX. It is expected the caller will use grow to create
|
||||
// a vaild AABB.
|
||||
pub fn infinite() -> AABB {
|
||||
AABB {
|
||||
bounds: [Vec3::from(f32::MAX), Vec3::from(f32::MIN)],
|
||||
}
|
||||
}
|
||||
pub fn new<V: Into<Vec3>>(min: V, max: V) -> AABB {
|
||||
let min: Vec3 = min.into();
|
||||
let max: Vec3 = max.into();
|
||||
assert!(min.x <= max.x);
|
||||
assert!(min.y <= max.y);
|
||||
assert!(min.z <= max.z);
|
||||
AABB { bounds: [min, max] }
|
||||
}
|
||||
|
||||
pub fn area(&self) -> f32 {
|
||||
if self.max().x == f32::MIN || self.min().x == f32::MAX {
|
||||
return 0.;
|
||||
}
|
||||
let e = self.max() - self.min();
|
||||
let v = e.x * e.y + e.y * e.z + e.z * e.x;
|
||||
if v.is_finite() {
|
||||
v
|
||||
} else {
|
||||
0.
|
||||
}
|
||||
}
|
||||
pub fn volume(&self) -> f32 {
|
||||
if self.max().x == f32::MIN || self.min().x == f32::MAX {
|
||||
return 0.;
|
||||
}
|
||||
let v = (self.min().x - self.max().x).abs()
|
||||
* (self.min().y - self.max().y).abs()
|
||||
* (self.min().z - self.max().z).abs();
|
||||
if v.is_finite() {
|
||||
v
|
||||
} else {
|
||||
0.
|
||||
}
|
||||
}
|
||||
|
||||
pub fn grow(&mut self, v: Vec3) {
|
||||
self.bounds[0] = vec3::min(self.bounds[0], v);
|
||||
self.bounds[1] = vec3::max(self.bounds[1], v);
|
||||
}
|
||||
|
||||
pub fn longest_axis(&self) -> usize {
|
||||
let xd = (self.min().x - self.max().x).abs();
|
||||
let yd = (self.min().y - self.max().y).abs();
|
||||
let zd = (self.min().z - self.max().z).abs();
|
||||
|
||||
if xd >= yd && xd >= zd {
|
||||
return 0;
|
||||
}
|
||||
if yd >= xd && yd >= zd {
|
||||
return 1;
|
||||
}
|
||||
2
|
||||
}
|
||||
|
||||
pub fn mid_point(&self) -> Vec3 {
|
||||
self.bounds[0] + (self.bounds[1] - self.bounds[0]) / 2.
|
||||
}
|
||||
|
||||
pub fn min(&self) -> Vec3 {
|
||||
self.bounds[0]
|
||||
}
|
||||
|
||||
pub fn max(&self) -> Vec3 {
|
||||
self.bounds[1]
|
||||
}
|
||||
|
||||
// TODO(wathiede): implement branchless https://tavianator.com/cgit/dimension.git/tree/libdimension/bvh/bvh.c#n194
|
||||
|
||||
pub fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> bool {
|
||||
self.hit_simd(r, t_min, t_max).is_some()
|
||||
}
|
||||
|
||||
pub fn hit_distance(&self, r: Ray, t_min: f32, t_max: f32) -> Option<f32> {
|
||||
self.hit_simd(r, t_min, t_max)
|
||||
}
|
||||
|
||||
pub fn hit_naive(&self, r: Ray, t_min: f32, t_max: f32) -> Option<f32> {
|
||||
let mut t_min = t_min;
|
||||
let mut t_max = t_max;
|
||||
for axis in 0..3 {
|
||||
let t0 = ((self.min()[axis] - r.origin[axis]) * r.inv_direction[axis])
|
||||
.min((self.max()[axis] - r.origin[axis]) * r.inv_direction[axis]);
|
||||
let t1 = ((self.min()[axis] - r.origin[axis]) * r.inv_direction[axis])
|
||||
.max((self.max()[axis] - r.origin[axis]) * r.inv_direction[axis]);
|
||||
t_min = t0.max(t_min);
|
||||
t_max = t1.min(t_max);
|
||||
if t_max <= t_min {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some(t_min)
|
||||
}
|
||||
|
||||
pub fn hit2(&self, r: Ray, t_min: f32, t_max: f32) -> Option<f32> {
|
||||
let mut t_min = t_min;
|
||||
let mut t_max = t_max;
|
||||
for axis in 0..3 {
|
||||
let d_inv = 1.0 / r.direction[axis];
|
||||
let t0 = min(
|
||||
(self.min()[axis] - r.origin[axis]) * d_inv,
|
||||
(self.max()[axis] - r.origin[axis]) * d_inv,
|
||||
);
|
||||
let t1 = max(
|
||||
(self.min()[axis] - r.origin[axis]) * d_inv,
|
||||
(self.max()[axis] - r.origin[axis]) * d_inv,
|
||||
);
|
||||
t_min = max(t0, t_min);
|
||||
t_max = min(t1, t_max);
|
||||
if t_max <= t_min {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some(t_min)
|
||||
}
|
||||
|
||||
pub fn hit_precompute(&self, r: Ray, t0: f32, t1: f32) -> Option<f32> {
|
||||
// TODO(wathiede): this has bugs.
|
||||
let mut t_min = (self.bounds[r.sign[0]].x - r.origin.x) * r.inv_direction.x;
|
||||
let mut t_max = (self.bounds[1 - r.sign[0]].x - r.origin.x) * r.inv_direction.x;
|
||||
let t_y_min = (self.bounds[r.sign[0]].y - r.origin.y) * r.inv_direction.y;
|
||||
let t_y_max = (self.bounds[1 - r.sign[0]].y - r.origin.y) * r.inv_direction.y;
|
||||
|
||||
if t_min > t_y_max || t_y_min > t_max {
|
||||
return None;
|
||||
}
|
||||
|
||||
if t_y_min > t_min {
|
||||
t_min = t_y_min;
|
||||
}
|
||||
if t_y_max < t_max {
|
||||
t_max = t_y_max;
|
||||
}
|
||||
|
||||
let t_z_min = (self.bounds[r.sign[2]].z - r.origin.z) * r.inv_direction.z;
|
||||
let t_z_max = (self.bounds[1 - r.sign[2]].z - r.origin.z) * r.inv_direction.z;
|
||||
if t_min > t_z_max || t_z_min > t_max {
|
||||
return None;
|
||||
}
|
||||
if t_z_min > t_min {
|
||||
t_min = t_z_min;
|
||||
}
|
||||
if t_z_max < t_max {
|
||||
t_max = t_z_max;
|
||||
}
|
||||
if t_min < t1 && t_max > t0 {
|
||||
Some(t_min)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hit_simd(&self, r: Ray, t_min: f32, t_max: f32) -> Option<f32> {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
unsafe {
|
||||
use std::arch::x86_64::*;
|
||||
let o4 = _mm_set_ps(0., r.origin.z, r.origin.y, r.origin.x);
|
||||
let d4 = _mm_set_ps(0., r.inv_direction.z, r.inv_direction.y, r.inv_direction.x);
|
||||
let bmin4 = _mm_set_ps(0., self.min().z, self.min().y, self.min().x);
|
||||
let bmax4 = _mm_set_ps(0., self.max().z, self.max().y, self.max().x);
|
||||
let mask4 = _mm_cmpeq_ps(_mm_setzero_ps(), _mm_set_ps(1., 0., 0., 0.));
|
||||
let t1 = _mm_mul_ps(_mm_sub_ps(_mm_and_ps(bmin4, mask4), o4), d4);
|
||||
let t2 = _mm_mul_ps(_mm_sub_ps(_mm_and_ps(bmax4, mask4), o4), d4);
|
||||
let vmax4 = _mm_max_ps(t1, t2);
|
||||
let vmin4 = _mm_min_ps(t1, t2);
|
||||
let vmax4: (f32, f32, f32, f32) = std::mem::transmute(vmax4);
|
||||
let vmin4: (f32, f32, f32, f32) = std::mem::transmute(vmin4);
|
||||
let tmax = min(min(vmax4.0, min(vmax4.1, vmax4.2)), t_max);
|
||||
let tmin = max(max(vmin4.0, max(vmin4.1, vmin4.2)), t_min);
|
||||
if tmin <= tmax {
|
||||
Some(tmin)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
// TODO(wathiede): add NEON implementation.
|
||||
self.hit2(r, t_min, t_max)
|
||||
}
|
||||
|
||||
pub fn hit_fast(&self, r: Ray, _t_min: f32, _t_max: f32) -> Option<f32> {
|
||||
let b = self;
|
||||
let mut t1 = (b.min()[0] - r.origin[0]) * 1. / r.direction[0];
|
||||
let mut t2 = (b.max()[0] - r.origin[0]) * 1. / r.direction[0];
|
||||
|
||||
let mut tmin = t1.min(t2);
|
||||
let mut tmax = t1.max(t2);
|
||||
|
||||
for i in 1..3 {
|
||||
t1 = (b.min()[i] - r.origin[i]) * 1. / r.direction[i];
|
||||
t2 = (b.max()[i] - r.origin[i]) * 1. / r.direction[i];
|
||||
|
||||
tmin = tmin.max(t1.min(t2));
|
||||
tmax = tmax.min(t1.max(t2));
|
||||
}
|
||||
|
||||
if tmax > tmin.max(0.0) {
|
||||
Some(tmin)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn surrounding_box(box0: &AABB, box1: &AABB) -> AABB {
|
||||
let min = Vec3::new(
|
||||
box0.min().x.min(box1.min().x),
|
||||
box0.min().y.min(box1.min().y),
|
||||
box0.min().z.min(box1.min().z),
|
||||
);
|
||||
let max = Vec3::new(
|
||||
box0.max().x.max(box1.max().x),
|
||||
box0.max().y.max(box1.max().y),
|
||||
box0.max().z.max(box1.max().z),
|
||||
);
|
||||
AABB::new(min, max)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn infinite() {
|
||||
let bb = AABB::infinite();
|
||||
assert_eq!(bb.area(), 0.);
|
||||
assert_eq!(bb.volume(), 0.);
|
||||
}
|
||||
|
||||
macro_rules! hit_test {
|
||||
($($name:ident,)*) => {
|
||||
$(
|
||||
mod $name {
|
||||
use super::*;
|
||||
const T_MIN: f32 = 0.001;
|
||||
const T_MAX: f32 = f32::MAX;
|
||||
|
||||
fn test_bb() -> AABB {
|
||||
AABB::new([-1., -1., -1.], [1., 1., 1.])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hit_front() {
|
||||
let bb = test_bb();
|
||||
let r = Ray::new([-2., 0., 0.], [1., 0., 0.], 0.5);
|
||||
assert!(bb.$name(r, T_MIN, T_MAX).is_some());
|
||||
}
|
||||
#[test]
|
||||
fn hit_back() {
|
||||
let bb = test_bb();
|
||||
let r = Ray::new([2., 0., 0.], [-1., 0., 0.], 0.5);
|
||||
assert!(bb.$name(r, T_MIN, T_MAX).is_some());
|
||||
}
|
||||
#[test]
|
||||
fn hit_top() {
|
||||
let bb = test_bb();
|
||||
let r = Ray::new([0., 2., 0.], [0., -1., 0.], 0.5);
|
||||
assert!(bb.$name(r, T_MIN, T_MAX).is_some());
|
||||
}
|
||||
#[test]
|
||||
fn hit_bottom() {
|
||||
let bb = test_bb();
|
||||
let r = Ray::new([0., -2., 0.], [0., 1., 0.], 0.5);
|
||||
assert!(bb.$name(r, T_MIN, T_MAX).is_some());
|
||||
}
|
||||
#[test]
|
||||
fn hit_left() {
|
||||
let bb = test_bb();
|
||||
let r = Ray::new([0., 0., -2.], [0., 0., 1.], 0.5);
|
||||
assert!(bb.$name(r, T_MIN, T_MAX).is_some());
|
||||
}
|
||||
#[test]
|
||||
fn hit_right() {
|
||||
let bb = test_bb();
|
||||
let r = Ray::new([0., 0., 2.], [0., 0., -1.], 0.5);
|
||||
assert!(bb.$name(r, T_MIN, T_MAX).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn miss_front() {
|
||||
let bb = test_bb();
|
||||
let r = Ray::new([0., 1.1, -1.1], [0., -1., 0.], 0.5);
|
||||
assert_eq!(bb.$name(r, T_MIN, T_MAX), None);
|
||||
}
|
||||
#[test]
|
||||
fn miss_back() {
|
||||
let bb = test_bb();
|
||||
let r = Ray::new([0., 1.1, 1.1], [0., -1., 0.], 0.5);
|
||||
assert_eq!(bb.$name(r, T_MIN, T_MAX), None);
|
||||
}
|
||||
#[test]
|
||||
fn miss_top() {
|
||||
let bb = test_bb();
|
||||
let r = Ray::new([0., 1.1, -1.1], [0., 0., 1.], 0.5);
|
||||
assert_eq!(bb.$name(r, T_MIN, T_MAX), None);
|
||||
}
|
||||
#[test]
|
||||
fn miss_bottom() {
|
||||
let bb = test_bb();
|
||||
let r = Ray::new([0., -1.1, -1.1], [0., 0., 1.], 0.5);
|
||||
assert_eq!(bb.$name(r, T_MIN, T_MAX), None);
|
||||
}
|
||||
#[test]
|
||||
fn miss_left() {
|
||||
let bb = test_bb();
|
||||
let r = Ray::new([-1.1, 0., -1.1], [0., 0., 1.], 0.5);
|
||||
assert_eq!(bb.$name(r, T_MIN, T_MAX), None);
|
||||
}
|
||||
#[test]
|
||||
fn miss_right() {
|
||||
let bb = test_bb();
|
||||
let r = Ray::new([1.1, 0., -1.1], [0., 0., 1.], 0.5);
|
||||
assert_eq!(bb.$name(r, T_MIN, T_MAX), None);
|
||||
}
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
hit_test! {
|
||||
hit_naive,
|
||||
hit_simd,
|
||||
hit_fast,
|
||||
hit2,
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
use std;
|
||||
use std::fmt;
|
||||
use std::time::Instant;
|
||||
use std::{self, fmt, time::Instant};
|
||||
|
||||
use rand;
|
||||
use rand::Rng;
|
||||
use log::info;
|
||||
use rand::{self, Rng};
|
||||
|
||||
use crate::aabb::surrounding_box;
|
||||
use crate::aabb::AABB;
|
||||
use crate::hitable::Hit;
|
||||
use crate::hitable::HitRecord;
|
||||
use crate::ray::Ray;
|
||||
use crate::{
|
||||
aabb::{surrounding_box, AABB},
|
||||
hitable::{Hit, HitRecord},
|
||||
ray::Ray,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
enum BVHNode {
|
||||
Leaf(Box<dyn Hit>),
|
||||
Branch {
|
||||
@@ -77,10 +76,10 @@ fn box_z_compare(ah: &Box<dyn Hit>, bh: &Box<dyn Hit>) -> std::cmp::Ordering {
|
||||
// returns a reference.)
|
||||
fn vec_first_into<T>(v: Vec<T>) -> T {
|
||||
if v.len() != 1 {
|
||||
panic!(format!(
|
||||
panic!(
|
||||
"vec_first_into called for vector length != 1, length {}",
|
||||
v.len()
|
||||
));
|
||||
);
|
||||
}
|
||||
v.into_iter().next().unwrap()
|
||||
}
|
||||
@@ -105,7 +104,7 @@ impl BVHNode {
|
||||
} else {
|
||||
let mut l: Vec<Box<dyn Hit>> = l.into_iter().collect();
|
||||
let mut rng = rand::thread_rng();
|
||||
match rng.gen_range::<u16>(0, 3) {
|
||||
match rng.gen_range(0..3) {
|
||||
0 => l.sort_by(box_x_compare),
|
||||
1 => l.sort_by(box_y_compare),
|
||||
2 => l.sort_by(box_z_compare),
|
||||
@@ -180,6 +179,7 @@ impl Hit for BVHNode {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BVH {
|
||||
root: BVHNode,
|
||||
}
|
||||
@@ -231,12 +231,13 @@ impl Hit for BVH {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::aabb::AABB;
|
||||
use crate::material::Lambertian;
|
||||
use crate::material::Metal;
|
||||
use crate::sphere::Sphere;
|
||||
use crate::texture::ConstantTexture;
|
||||
use crate::vec3::Vec3;
|
||||
use crate::{
|
||||
aabb::AABB,
|
||||
material::{Lambertian, Metal},
|
||||
sphere::Sphere,
|
||||
texture::ConstantTexture,
|
||||
vec3::Vec3,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
711
rtiow/renderer/src/bvh_triangles.rs
Normal file
711
rtiow/renderer/src/bvh_triangles.rs
Normal file
@@ -0,0 +1,711 @@
|
||||
/// Implementation based on blog post @
|
||||
/// https://jacco.ompf2.com/2022/04/13/how-to-build-a-bvh-part-1-basics/
|
||||
use std::f32::EPSILON;
|
||||
use std::fmt;
|
||||
|
||||
use log::info;
|
||||
use stl::STL;
|
||||
|
||||
use crate::{
|
||||
aabb::AABB,
|
||||
hitable::{Hit, HitRecord},
|
||||
material::Material,
|
||||
ray::Ray,
|
||||
vec3::{cross, dot, Vec3},
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct BVHNode {
|
||||
aabb: AABB,
|
||||
// When tri_count==0, left_first holds the left child's index in bvh_nodes. When >0 left_first
|
||||
// holds the index for the first triangle in triangles.
|
||||
left_first: u32,
|
||||
tri_count: u32,
|
||||
}
|
||||
|
||||
impl BVHNode {
|
||||
fn is_leaf(&self) -> bool {
|
||||
self.tri_count > 0
|
||||
}
|
||||
|
||||
fn cost(&self) -> f32 {
|
||||
let area = self.aabb.area();
|
||||
self.tri_count as f32 * area
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct Triangle {
|
||||
centroid: Vec3,
|
||||
verts: [Vec3; 3],
|
||||
}
|
||||
impl fmt::Debug for Triangle {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Tri: <{}, {}, {}> @ {}",
|
||||
self.verts[0], self.verts[1], self.verts[2], self.centroid
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BVHTriangles<M>
|
||||
where
|
||||
M: Material,
|
||||
{
|
||||
pub triangles: Vec<Triangle>,
|
||||
triangle_index: Vec<usize>,
|
||||
material: M,
|
||||
bvh_nodes: Vec<BVHNode>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct Bin {
|
||||
bounds: AABB,
|
||||
tri_count: usize,
|
||||
}
|
||||
|
||||
impl<M> fmt::Debug for BVHTriangles<M>
|
||||
where
|
||||
M: Material,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{} triangles", self.triangles.len())?;
|
||||
if f.alternate() {
|
||||
writeln!(f)?;
|
||||
}
|
||||
for (i, t) in self.triangles.iter().enumerate() {
|
||||
if f.alternate() {
|
||||
write!(f, "\t")?;
|
||||
}
|
||||
write!(f, "{i:>3} {:?} ", t)?;
|
||||
if f.alternate() {
|
||||
writeln!(f)?;
|
||||
}
|
||||
}
|
||||
if f.alternate() {
|
||||
writeln!(f)?;
|
||||
}
|
||||
for (i, n) in self.bvh_nodes.iter().enumerate() {
|
||||
write!(f, "N[{i}] {n:?}")?;
|
||||
if f.alternate() {
|
||||
writeln!(f)?;
|
||||
}
|
||||
let n = &self.bvh_nodes[i];
|
||||
if n.is_leaf() {
|
||||
for t_idx in n.left_first..(n.left_first + n.tri_count) {
|
||||
if f.alternate() {
|
||||
write!(f, "\t")?;
|
||||
}
|
||||
write!(f, "{:?} ", self.triangles[t_idx as usize])?;
|
||||
if f.alternate() {
|
||||
writeln!(f)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SplitCost {
|
||||
pos: f32,
|
||||
axis: usize,
|
||||
cost: f32,
|
||||
}
|
||||
|
||||
const ROOT_NODE_IDX: usize = 0;
|
||||
impl<M> BVHTriangles<M>
|
||||
where
|
||||
M: Material,
|
||||
{
|
||||
pub fn new(stl: &STL, material: M, scale_factor: f32) -> BVHTriangles<M> {
|
||||
let now = std::time::Instant::now();
|
||||
|
||||
assert_eq!(std::mem::size_of::<BVHNode>(), 32);
|
||||
let div3 = 1. / 3.;
|
||||
let triangles: Vec<_> = stl
|
||||
.triangles
|
||||
.iter()
|
||||
.map(|t| {
|
||||
let v0 = t.verts[0] * scale_factor;
|
||||
let v1 = t.verts[1] * scale_factor;
|
||||
let v2 = t.verts[2] * scale_factor;
|
||||
let centroid = (v0 + v1 + v2) * div3;
|
||||
Triangle {
|
||||
centroid,
|
||||
verts: [v0, v1, v2],
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let triangle_index = (0..triangles.len()).collect();
|
||||
|
||||
let n = 2 * triangles.len() - 2;
|
||||
let bvh_nodes = Vec::with_capacity(n);
|
||||
let mut bvh = BVHTriangles {
|
||||
triangles,
|
||||
triangle_index,
|
||||
bvh_nodes,
|
||||
material,
|
||||
};
|
||||
bvh.build_bvh();
|
||||
info!(
|
||||
"BVHTriangles build time {:0.3}s",
|
||||
now.elapsed().as_secs_f32()
|
||||
);
|
||||
struct Stats {
|
||||
nodes: usize,
|
||||
leafs: usize,
|
||||
min_tris: u32,
|
||||
max_tris: u32,
|
||||
}
|
||||
impl Default for Stats {
|
||||
fn default() -> Self {
|
||||
Stats {
|
||||
nodes: 0,
|
||||
leafs: 0,
|
||||
min_tris: u32::MAX,
|
||||
max_tris: u32::MIN,
|
||||
}
|
||||
}
|
||||
}
|
||||
let stats = bvh.bvh_nodes.iter().fold(Stats::default(), |mut stats, n| {
|
||||
stats.nodes += 1;
|
||||
if n.is_leaf() {
|
||||
stats.leafs += 1;
|
||||
stats.min_tris = n.tri_count.min(stats.min_tris);
|
||||
stats.max_tris = n.tri_count.max(stats.max_tris);
|
||||
}
|
||||
stats
|
||||
});
|
||||
info!("BVHTriangles build stats:");
|
||||
info!(" Nodes: {}", stats.nodes);
|
||||
info!(" Leaves: {}", stats.leafs);
|
||||
info!(" Tris: {}", bvh.triangles.len());
|
||||
info!(" Min Tri: {}", stats.min_tris);
|
||||
info!(" Max Tri: {}", stats.max_tris);
|
||||
info!(" Avg Tri: {}", bvh.triangles.len() / stats.leafs);
|
||||
info!(" Predict: {}", bvh.bvh_nodes.capacity());
|
||||
info!(" Actual: {}", bvh.bvh_nodes.len());
|
||||
info!(
|
||||
" Savings: {} bytes",
|
||||
(bvh.bvh_nodes.capacity() - bvh.bvh_nodes.len()) * std::mem::size_of::<BVHNode>()
|
||||
);
|
||||
bvh.bvh_nodes.shrink_to_fit();
|
||||
//dbg!(&bvh);
|
||||
bvh
|
||||
}
|
||||
|
||||
fn build_bvh(&mut self) {
|
||||
// assign all triangles to root node
|
||||
let root = BVHNode {
|
||||
aabb: AABB::default(),
|
||||
left_first: 0,
|
||||
tri_count: self.triangles.len() as u32,
|
||||
};
|
||||
self.bvh_nodes.push(root);
|
||||
self.update_node_bounds(ROOT_NODE_IDX);
|
||||
// subdivide recursively
|
||||
self.subdivide(ROOT_NODE_IDX);
|
||||
}
|
||||
fn update_node_bounds(&mut self, node_idx: usize) {
|
||||
let node = &mut self.bvh_nodes[node_idx];
|
||||
let mut aabb_min: Vec3 = f32::MAX.into();
|
||||
let mut aabb_max: Vec3 = f32::MIN.into();
|
||||
for i in node.left_first..(node.left_first + node.tri_count) {
|
||||
let leaf_tri_idx = self.triangle_index[i as usize];
|
||||
let leaf_tri = &self.triangles[leaf_tri_idx];
|
||||
aabb_min = vec3::min(aabb_min, leaf_tri.verts[0]);
|
||||
aabb_min = vec3::min(aabb_min, leaf_tri.verts[1]);
|
||||
aabb_min = vec3::min(aabb_min, leaf_tri.verts[2]);
|
||||
aabb_max = vec3::max(aabb_max, leaf_tri.verts[0]);
|
||||
aabb_max = vec3::max(aabb_max, leaf_tri.verts[1]);
|
||||
aabb_max = vec3::max(aabb_max, leaf_tri.verts[2]);
|
||||
}
|
||||
node.aabb = AABB::new(aabb_min, aabb_max);
|
||||
}
|
||||
|
||||
fn find_best_split_plane(&self, node: &BVHNode) -> SplitCost {
|
||||
let mut best = SplitCost {
|
||||
pos: 0.,
|
||||
cost: f32::MAX,
|
||||
axis: usize::MAX,
|
||||
};
|
||||
|
||||
for axis in 0..3 {
|
||||
let mut bounds_min = f32::MAX;
|
||||
let mut bounds_max = f32::MIN;
|
||||
|
||||
for i in 0..node.tri_count {
|
||||
let triangle = &self.triangles[self.triangle_index[(node.left_first + i) as usize]];
|
||||
bounds_min = bounds_min.min(triangle.centroid[axis]);
|
||||
bounds_max = bounds_max.max(triangle.centroid[axis]);
|
||||
}
|
||||
|
||||
if bounds_min == bounds_max {
|
||||
continue;
|
||||
}
|
||||
|
||||
const NUM_BINS: usize = 8;
|
||||
let mut bins: Vec<_> = (0..NUM_BINS)
|
||||
.map(|_| Bin {
|
||||
bounds: AABB::infinite(),
|
||||
tri_count: 0,
|
||||
})
|
||||
.collect();
|
||||
let scale = bins.len() as f32 / (bounds_max - bounds_min);
|
||||
// populate the bins
|
||||
for i in 0..node.tri_count {
|
||||
let triangle = &self.triangles[self.triangle_index[(node.left_first + i) as usize]];
|
||||
let bin_idx = (triangle.centroid[axis] - bounds_min) * scale;
|
||||
let bin_idx = (bins.len() - 1).min(bin_idx as usize);
|
||||
|
||||
bins[bin_idx].tri_count += 1;
|
||||
bins[bin_idx].bounds.grow(triangle.verts[0]);
|
||||
bins[bin_idx].bounds.grow(triangle.verts[1]);
|
||||
bins[bin_idx].bounds.grow(triangle.verts[2]);
|
||||
}
|
||||
|
||||
// gather data for the 7 planes between the 8 bins
|
||||
let mut left_area: Vec<_> = (0..bins.len() - 1).map(|_| 0.).collect();
|
||||
let mut right_area: Vec<_> = (0..bins.len() - 1).map(|_| 0.).collect();
|
||||
let mut left_count: Vec<_> = (0..bins.len() - 1).map(|_| 0).collect();
|
||||
let mut right_count: Vec<_> = (0..bins.len() - 1).map(|_| 0).collect();
|
||||
let mut left_box = AABB::infinite();
|
||||
let mut right_box = AABB::infinite();
|
||||
let mut left_sum = 0;
|
||||
let mut right_sum = 0;
|
||||
for i in 0..(bins.len() - 1) {
|
||||
left_sum += bins[i].tri_count;
|
||||
left_count[i] = left_sum;
|
||||
left_box.grow(bins[i].bounds.min());
|
||||
left_box.grow(bins[i].bounds.max());
|
||||
left_area[i] = left_box.area();
|
||||
|
||||
right_sum += bins[bins.len() - 1 - i].tri_count;
|
||||
right_count[bins.len() - 2 - i] = right_sum;
|
||||
right_box.grow(bins[bins.len() - 1 - i].bounds.min());
|
||||
right_box.grow(bins[bins.len() - 1 - i].bounds.max());
|
||||
right_area[bins.len() - 2 - i] = right_box.area();
|
||||
}
|
||||
|
||||
let scale = (bounds_max - bounds_min) / bins.len() as f32;
|
||||
// calculate SAH cost for the 7 planes
|
||||
for i in 0..(bins.len() - 1) {
|
||||
let plane_cost =
|
||||
left_count[i] as f32 * left_area[i] + right_count[i] as f32 * right_area[i];
|
||||
if plane_cost < best.cost {
|
||||
best.axis = axis;
|
||||
best.pos = bounds_min + scale * (i as f32 + 1.);
|
||||
best.cost = plane_cost;
|
||||
}
|
||||
}
|
||||
}
|
||||
best
|
||||
}
|
||||
|
||||
fn subdivide(&mut self, idx: usize) {
|
||||
let (left_first, tri_count, left_count, i) = {
|
||||
let node = &self.bvh_nodes[idx];
|
||||
let split = self.find_best_split_plane(&node);
|
||||
let no_split_cost = node.cost();
|
||||
// Stop subdividing if it isn't getting any better.
|
||||
if split.cost >= no_split_cost {
|
||||
return;
|
||||
}
|
||||
|
||||
// Split the group in two halves.
|
||||
let mut i = node.left_first as isize;
|
||||
let mut j = i + node.tri_count as isize - 1;
|
||||
while i <= j {
|
||||
if self.triangles[self.triangle_index[i as usize]].centroid[split.axis] < split.pos
|
||||
{
|
||||
i += 1;
|
||||
} else {
|
||||
self.triangles.swap(
|
||||
self.triangle_index[i as usize],
|
||||
self.triangle_index[j as usize],
|
||||
);
|
||||
j -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Create child nodes for each half.
|
||||
let left_count = i as u32 - node.left_first;
|
||||
if left_count == 0 || left_count == node.tri_count {
|
||||
return;
|
||||
}
|
||||
(node.left_first, node.tri_count, left_count, i)
|
||||
};
|
||||
|
||||
// create child nodes
|
||||
let left_child_idx = self.bvh_nodes.len() as u32;
|
||||
let right_child_idx = left_child_idx + 1 as u32;
|
||||
let left = BVHNode {
|
||||
aabb: AABB::default(),
|
||||
left_first,
|
||||
tri_count: left_count as u32,
|
||||
};
|
||||
let right = BVHNode {
|
||||
aabb: AABB::default(),
|
||||
left_first: i as u32,
|
||||
tri_count: (tri_count - left_count) as u32,
|
||||
};
|
||||
self.bvh_nodes.push(left);
|
||||
self.bvh_nodes.push(right);
|
||||
let node = &mut self.bvh_nodes[idx];
|
||||
node.left_first = left_child_idx;
|
||||
node.tri_count = 0;
|
||||
|
||||
// Recurse
|
||||
self.update_node_bounds(left_child_idx as usize);
|
||||
self.subdivide(left_child_idx as usize);
|
||||
self.update_node_bounds(right_child_idx as usize);
|
||||
self.subdivide(right_child_idx as usize);
|
||||
}
|
||||
|
||||
fn intersect_bvh(&self, r: Ray, t_min: f32, t_max: f32) -> Option<HitRecord> {
|
||||
let mut node = &self.bvh_nodes[ROOT_NODE_IDX];
|
||||
let mut stack = Vec::with_capacity(2);
|
||||
let mut nearest = None;
|
||||
loop {
|
||||
if node.is_leaf() {
|
||||
let canditate = (node.left_first..(node.left_first + node.tri_count))
|
||||
// Map from idx to Triangle
|
||||
.map(|idx| &self.triangles[self.triangle_index[idx as usize]])
|
||||
// Try to hit all triangles for this node, filtering out misses.
|
||||
.filter_map(|tri| intersect_tri(r, &tri))
|
||||
// Find the nearest hit (if any).
|
||||
.min_by(|a, b| a.t.partial_cmp(&b.t).unwrap());
|
||||
|
||||
// merge candidate with nearest.
|
||||
nearest = match (&canditate, &nearest) {
|
||||
(Some(_), None) => canditate,
|
||||
(None, Some(_)) => nearest,
|
||||
(Some(c), Some(n)) => {
|
||||
//info!("merging {c:#?} and {n:#?}");
|
||||
if c.t < n.t {
|
||||
canditate
|
||||
} else {
|
||||
nearest
|
||||
}
|
||||
}
|
||||
(None, None) => None,
|
||||
};
|
||||
if stack.is_empty() {
|
||||
break;
|
||||
}
|
||||
node = stack.pop().unwrap();
|
||||
continue;
|
||||
}
|
||||
|
||||
let child1 = &self.bvh_nodes[node.left_first as usize];
|
||||
let child2 = &self.bvh_nodes[node.left_first as usize + 1];
|
||||
let dist1 = child1.aabb.hit_distance(r, t_min, t_max);
|
||||
let dist2 = child2.aabb.hit_distance(r, t_min, t_max);
|
||||
// Swap c1/c2 & d1/d2 based on d1/d2.
|
||||
let (child1, child2, dist1, dist2) = match (dist1, dist2) {
|
||||
(Some(d1), Some(d2)) if d1 > d2 => (child2, child1, dist2, dist1),
|
||||
(None, Some(_)) => (child2, child1, dist2, dist1),
|
||||
_ => (child1, child2, dist1, dist2),
|
||||
};
|
||||
|
||||
// dist1/child1 should now be the nearest hit.
|
||||
|
||||
// If we missed dist1/child1, then we implicitly missed dist2/child2, so pop a child
|
||||
// from the stack or exit the function.
|
||||
if dist1.is_none() {
|
||||
if stack.is_empty() {
|
||||
break;
|
||||
}
|
||||
node = stack.pop().unwrap();
|
||||
} else {
|
||||
// We hit child1, so process it next.
|
||||
node = child1;
|
||||
// If we also hit child2 save it on the stack so we can process it later.
|
||||
if dist2.is_some() {
|
||||
stack.push(child2);
|
||||
}
|
||||
}
|
||||
}
|
||||
nearest
|
||||
.and_then(|rtr: RayTriangleResult| Some(rtr.hit_record_with_material(&self.material)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Based on https://www.scratchapixel.com/lessons/3d-basic-rendering/ray-tracing-rendering-a-triangle/moller-trumbore-ray-triangle-intersection.html
|
||||
fn intersect_tri(r: Ray, tri: &Triangle) -> Option<RayTriangleResult> {
|
||||
// #ifdef MOLLER_TRUMBORE
|
||||
// Vec3f v0v1 = v1 - v0;
|
||||
// Vec3f v0v2 = v2 - v0;
|
||||
// Vec3f pvec = dir.crossProduct(v0v2);
|
||||
// float det = v0v1.dotProduct(pvec);
|
||||
// #ifdef CULLING
|
||||
// // if the determinant is negative, the triangle is 'back facing'
|
||||
// // if the determinant is close to 0, the ray misses the triangle
|
||||
// if (det < kEpsilon) return false;
|
||||
// #else
|
||||
// // ray and triangle are parallel if det is close to 0
|
||||
// if (fabs(det) < kEpsilon) return false;
|
||||
// #endif
|
||||
// float invDet = 1 / det;
|
||||
//
|
||||
// Vec3f tvec = orig - v0;
|
||||
// u = tvec.dotProduct(pvec) * invDet;
|
||||
// if (u < 0 || u > 1) return false;
|
||||
//
|
||||
// Vec3f qvec = tvec.crossProduct(v0v1);
|
||||
// v = dir.dotProduct(qvec) * invDet;
|
||||
// if (v < 0 || u + v > 1) return false;
|
||||
//
|
||||
// t = v0v2.dotProduct(qvec) * invDet;
|
||||
//
|
||||
let v0 = tri.verts[0];
|
||||
let v1 = tri.verts[1];
|
||||
let v2 = tri.verts[2];
|
||||
|
||||
let v0v1 = v1 - v0;
|
||||
let v0v2 = v2 - v0;
|
||||
let p = cross(r.direction, v0v2);
|
||||
let det = dot(v0v1, p);
|
||||
if det.abs() < EPSILON {
|
||||
return None;
|
||||
}
|
||||
|
||||
let inv_det = 1. / det;
|
||||
|
||||
let t = r.origin - v0;
|
||||
let u = dot(t, p) * inv_det;
|
||||
if u < 0. || u > 1. {
|
||||
return None;
|
||||
}
|
||||
|
||||
let q = cross(t, v0v1);
|
||||
let v = dot(r.direction, q) * inv_det;
|
||||
if v < 0. || u + v > 1. {
|
||||
return None;
|
||||
}
|
||||
let t = dot(v0v2, q) * inv_det;
|
||||
|
||||
if t > EPSILON {
|
||||
return Some(RayTriangleResult {
|
||||
t,
|
||||
p: r.point_at_parameter(t),
|
||||
tri: tri.clone(),
|
||||
});
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
impl<M> Hit for BVHTriangles<M>
|
||||
where
|
||||
M: Material,
|
||||
{
|
||||
fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option<HitRecord> {
|
||||
self.intersect_bvh(r, t_min, t_max)
|
||||
}
|
||||
|
||||
fn bounding_box(&self, _t_min: f32, _t_max: f32) -> Option<AABB> {
|
||||
Some(self.bvh_nodes[ROOT_NODE_IDX].aabb)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct RayTriangleResult {
|
||||
t: f32,
|
||||
p: Vec3,
|
||||
tri: Triangle,
|
||||
}
|
||||
|
||||
impl RayTriangleResult {
|
||||
fn hit_record_with_material<'m>(self, material: &'m dyn Material) -> HitRecord<'m> {
|
||||
// We don't support UV (yet?).
|
||||
let uv = (0.5, 0.5);
|
||||
let v0 = self.tri.verts[0];
|
||||
let v1 = self.tri.verts[1];
|
||||
let v2 = self.tri.verts[2];
|
||||
|
||||
let v0v1 = v1 - v0;
|
||||
let v0v2 = v2 - v0;
|
||||
let normal = cross(v0v1, v0v2).unit_vector();
|
||||
//println!("hit triangle {tri:?}");
|
||||
HitRecord {
|
||||
t: self.t,
|
||||
uv,
|
||||
p: self.p,
|
||||
normal,
|
||||
material,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
bvh_triangles::BVHTriangles,
|
||||
cuboid::Cuboid,
|
||||
hitable::Hit,
|
||||
material::{Dielectric, Lambertian},
|
||||
ray::Ray,
|
||||
texture::ConstantTexture,
|
||||
};
|
||||
use pretty_assertions::assert_eq;
|
||||
use proptest::prelude::*;
|
||||
use std::{
|
||||
io::{BufReader, Cursor},
|
||||
sync::Arc,
|
||||
};
|
||||
use stl::STL;
|
||||
|
||||
#[test]
|
||||
fn compare_cuboid() {
|
||||
let c = Cuboid::new(
|
||||
[0., 0., 0.].into(),
|
||||
[20., 20., 20.].into(),
|
||||
Arc::new(Dielectric::new(1.5)),
|
||||
);
|
||||
let stl_cube = STL::parse(
|
||||
BufReader::new(Cursor::new(include_bytes!("../stls/cube.stl"))),
|
||||
false,
|
||||
)
|
||||
.expect("failed to parse cube");
|
||||
|
||||
let s = BVHTriangles::new(
|
||||
&stl_cube,
|
||||
Dielectric::new(1.5),
|
||||
//Metal::new(Vec3::new(0.8, 0.8, 0.8), 0.2),
|
||||
//Lambertian::new(ConstantTexture::new(Vec3::new(1.0, 0.2, 0.2))),
|
||||
);
|
||||
//dbg!(&s);
|
||||
let mut rays: Vec<_> = (1..20)
|
||||
.flat_map(|y| {
|
||||
(1..20).flat_map(move |x| {
|
||||
let x = x as f32;
|
||||
let y = y as f32;
|
||||
vec![
|
||||
// Outward in angle
|
||||
Ray::new([-1., x, y], [1., 0., 0.], 0.),
|
||||
Ray::new([21., x, y], [-1., 0., 0.], 0.),
|
||||
Ray::new([x, -1., y], [0., 1., 0.], 0.),
|
||||
Ray::new([x, 21., y], [0., -1., 0.], 0.),
|
||||
Ray::new([x, y, -1.], [0., 0., 1.], 0.),
|
||||
Ray::new([x, y, 21.], [0., 0., -1.], 0.),
|
||||
// Inward out (
|
||||
Ray::new([x, y, 10.], [1., 0., 0.], 0.),
|
||||
Ray::new([x, y, 10.], [-1., 0., 0.], 0.),
|
||||
Ray::new([x, 10., y], [1., 0., 0.], 0.),
|
||||
Ray::new([x, 10., y], [-1., 0., 0.], 0.),
|
||||
Ray::new([10., x, y], [1., 0., 0.], 0.),
|
||||
Ray::new([10., x, y], [-1., 0., 0.], 0.),
|
||||
Ray::new([x, y, 10.], [0., 1., 0.], 0.),
|
||||
Ray::new([x, y, 10.], [0., -1., 0.], 0.),
|
||||
Ray::new([x, 10., y], [0., 1., 0.], 0.),
|
||||
Ray::new([x, 10., y], [0., -1., 0.], 0.),
|
||||
Ray::new([10., x, y], [0., 1., 0.], 0.),
|
||||
Ray::new([10., x, y], [0., -1., 0.], 0.),
|
||||
Ray::new([x, y, 10.], [0., 0., 1.], 0.),
|
||||
Ray::new([x, y, 10.], [0., 0., -1.], 0.),
|
||||
Ray::new([x, 10., y], [0., 0., 1.], 0.),
|
||||
Ray::new([x, 10., y], [0., 0., -1.], 0.),
|
||||
Ray::new([10., x, y], [0., 0., 1.], 0.),
|
||||
Ray::new([10., x, y], [0., 0., -1.], 0.),
|
||||
]
|
||||
.into_iter()
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
if false {
|
||||
// Outward in at an angle.
|
||||
let sqrt2 = 2f32.sqrt();
|
||||
rays.extend(vec![
|
||||
Ray::new([-1., 10., 10.], [sqrt2, sqrt2, 0.], 0.),
|
||||
Ray::new([-1., 10., 10.], [sqrt2, -sqrt2, 0.], 0.),
|
||||
Ray::new([-1., 10., 10.], [sqrt2, 0., sqrt2], 0.),
|
||||
Ray::new([-1., 10., 10.], [sqrt2, 0., -sqrt2], 0.),
|
||||
]);
|
||||
}
|
||||
|
||||
for r in rays.into_iter() {
|
||||
let c_hit = c
|
||||
.hit(r, 0., f32::MAX)
|
||||
.expect(&format!("c_hit missed {r:#?}"));
|
||||
let s_hit = s
|
||||
.hit(r, 0., f32::MAX)
|
||||
.expect(&format!("s_hit missed {r:#?}"));
|
||||
assert!(
|
||||
(c_hit.t - s_hit.t).abs() < EPSILON,
|
||||
"{r:?} [t] c_hit: {c_hit:#?}, s_hit: {s_hit:#?}"
|
||||
);
|
||||
// uv isn't valid for BVHTriangles.
|
||||
// assert_eq!( c_hit.uv, s_hit.uv, "{i}: [uv] c_hit: {c_hit:?}, s_hit: {s_hit:?}");
|
||||
assert_eq!(
|
||||
c_hit.p, s_hit.p,
|
||||
"{r:?}: [p] c_hit: {c_hit:#?}, s_hit: {s_hit:#?}"
|
||||
);
|
||||
assert_eq!(
|
||||
c_hit.normal, s_hit.normal,
|
||||
"{r:?}: [normal] c_hit: {c_hit:?}, s_hit: {s_hit:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
proptest! {
|
||||
// TODO(wathiede): make this work.
|
||||
//#[test]
|
||||
fn compare_cuboid_proptest(
|
||||
ox in -20.0f32..40.0,
|
||||
oy in -20.0f32..40.0,
|
||||
oz in -20.0f32..40.0,
|
||||
dx in -1.0f32..1.0,
|
||||
dy in -1.0f32..1.0,
|
||||
dz in -1.0f32..1.0,
|
||||
) {
|
||||
let r = Ray::new([ox,oy,oz].into(), Vec3::new(dx, dy, dz).unit_vector(), 0.5);
|
||||
let c = Cuboid::new(
|
||||
[0., 0., 0.].into(),
|
||||
[20., 20., 20.].into(),
|
||||
Arc::new(Dielectric::new(1.5)),
|
||||
);
|
||||
let stl_cube = STL::parse(
|
||||
BufReader::new(Cursor::new(include_bytes!("../stls/cube.stl"))),
|
||||
false,
|
||||
)
|
||||
.expect("failed to parse cube");
|
||||
|
||||
let s = BVHTriangles::new(
|
||||
&stl_cube,
|
||||
Dielectric::new(1.5),
|
||||
//Metal::new(Vec3::new(0.8, 0.8, 0.8), 0.2),
|
||||
//Lambertian::new(ConstantTexture::new(Vec3::new(1.0, 0.2, 0.2))),
|
||||
);
|
||||
|
||||
let c_hit = c .hit(r, 0., f32::MAX);
|
||||
let s_hit = s .hit(r, 0., f32::MAX);
|
||||
|
||||
match (c_hit, s_hit) {
|
||||
(Some(_), None)=>assert!(false, "hit cuboid but not STL"),
|
||||
(None, Some(_))=>assert!(false, "hit STL but not cuboid"),
|
||||
(Some(c_hit), Some(s_hit))=> {
|
||||
assert!(
|
||||
(c_hit.t - s_hit.t).abs() < 0.00001,
|
||||
"{r:?} [t] c_hit: {c_hit:#?}, s_hit: {s_hit:#?}"
|
||||
);
|
||||
// uv isn't valid for BVHTriangles.
|
||||
// assert_eq!( c_hit.uv, s_hit.uv, "{i}: [uv] c_hit: {c_hit:?}, s_hit: {s_hit:?}");
|
||||
assert_eq!(
|
||||
c_hit.p, s_hit.p,
|
||||
"{r:?}: [p] c_hit: {c_hit:#?}, s_hit: {s_hit:#?}"
|
||||
);
|
||||
assert_eq!(
|
||||
c_hit.normal, s_hit.normal,
|
||||
"{r:?}: [normal] c_hit: {c_hit:?}, s_hit: {s_hit:?}"
|
||||
);
|
||||
},
|
||||
// It's okay if they both miss.
|
||||
(None,None)=>(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,24 @@
|
||||
extern crate rand;
|
||||
use std::f32::consts::PI;
|
||||
|
||||
use self::rand::Rng;
|
||||
use rand::{self, Rng};
|
||||
|
||||
use crate::ray::Ray;
|
||||
use crate::vec3::cross;
|
||||
use crate::vec3::Vec3;
|
||||
use crate::{
|
||||
ray::Ray,
|
||||
vec3::{cross, Vec3},
|
||||
};
|
||||
|
||||
fn random_in_unit_disk() -> Vec3 {
|
||||
let mut rng = rand::thread_rng();
|
||||
let v = Vec3::new(1., 1., 0.);
|
||||
loop {
|
||||
let p =
|
||||
2. * Vec3::new(
|
||||
rng.gen_range::<f32>(0., 1.),
|
||||
rng.gen_range::<f32>(0., 1.),
|
||||
0.,
|
||||
) - v;
|
||||
let p = 2. * Vec3::new(rng.gen(), rng.gen(), 0.) - v;
|
||||
if p.squared_length() < 1. {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Camera {
|
||||
origin: Vec3,
|
||||
lower_left_corner: Vec3,
|
||||
@@ -77,7 +73,7 @@ impl Camera {
|
||||
let mut rng = rand::thread_rng();
|
||||
let rd = self.lens_radius * random_in_unit_disk();
|
||||
let offset = self.u * rd.x + self.v * rd.y;
|
||||
let time = self.time_min + rng.gen_range::<f32>(0., 1.) * (self.time_max - self.time_min);
|
||||
let time = self.time_min + rng.gen::<f32>() * (self.time_max - self.time_min);
|
||||
Ray::new(
|
||||
self.origin + offset,
|
||||
self.lower_left_corner + self.horizontal * u + self.vertical * v - self.origin - offset,
|
||||
46
rtiow/renderer/src/colors.rs
Normal file
46
rtiow/renderer/src/colors.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use rand::{self, Rng};
|
||||
|
||||
use crate::vec3::Vec3;
|
||||
|
||||
// HSV values in [0..1]
|
||||
// returns [r, g, b] values from 0 to 255
|
||||
//From https://martin.ankerl.com/2009/12/09/how-to-create-random-colors-programmatically/
|
||||
pub fn hsv_to_rgb(h: f32, s: f32, v: f32) -> Vec3 {
|
||||
let h_i = (h * 6.) as i32;
|
||||
let f = h * 6. - h_i as f32;
|
||||
let p = v * (1. - s);
|
||||
let q = v * (1. - f * s);
|
||||
let t = v * (1. - (1. - f) * s);
|
||||
match h_i {
|
||||
0 => Vec3::new(v, t, p),
|
||||
1 => Vec3::new(q, v, p),
|
||||
2 => Vec3::new(p, v, t),
|
||||
3 => Vec3::new(p, q, v),
|
||||
4 => Vec3::new(t, p, v),
|
||||
5 => Vec3::new(v, p, q),
|
||||
_ => panic!("Unknown H value {}", h_i),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_rainbow(num: usize) -> Vec<Vec3> {
|
||||
(0..num)
|
||||
.map(|n| {
|
||||
let h = n as f32 / num as f32;
|
||||
hsv_to_rgb(h, 0.99, 0.99)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
pub fn generate_palette(num: usize) -> Vec<Vec3> {
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut random = || rng.gen();
|
||||
// use golden ratio
|
||||
let golden_ratio_conjugate = 0.618_034;
|
||||
let mut h = random();
|
||||
(0..num)
|
||||
.map(|_| {
|
||||
h += golden_ratio_conjugate;
|
||||
h %= 1.0;
|
||||
hsv_to_rgb(h, 0.99, 0.99)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
use std;
|
||||
|
||||
use rand;
|
||||
use rand::Rng;
|
||||
use rand::{self, Rng};
|
||||
|
||||
use crate::aabb::AABB;
|
||||
use crate::hitable::Hit;
|
||||
use crate::hitable::HitRecord;
|
||||
use crate::material::Isotropic;
|
||||
use crate::ray::Ray;
|
||||
use crate::texture::Texture;
|
||||
use crate::vec3::Vec3;
|
||||
use crate::{
|
||||
aabb::AABB,
|
||||
hitable::{Hit, HitRecord},
|
||||
material::Isotropic,
|
||||
ray::Ray,
|
||||
texture::Texture,
|
||||
vec3::Vec3,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ConstantMedium<H, T>
|
||||
where
|
||||
H: Hit,
|
||||
@@ -60,14 +61,10 @@ where
|
||||
rec1.t = 0.;
|
||||
}
|
||||
let distance_inside_boundary = (rec2.t - rec1.t) * r.direction.length();
|
||||
let hit_distance = -(1. / self.density) * rng.gen_range::<f32>(0., 1.).ln();
|
||||
let hit_distance: f32 = -(1. / self.density) * rng.gen::<f32>().ln();
|
||||
if hit_distance < distance_inside_boundary {
|
||||
let t = rec1.t + hit_distance / r.direction.length();
|
||||
let normal = Vec3::new(
|
||||
rng.gen_range::<f32>(0., 1.),
|
||||
rng.gen_range::<f32>(0., 1.),
|
||||
rng.gen_range::<f32>(0., 1.),
|
||||
).unit_vector();
|
||||
let normal = Vec3::new(rng.gen(), rng.gen(), rng.gen()).unit_vector();
|
||||
return Some(HitRecord {
|
||||
t,
|
||||
p: r.point_at_parameter(t),
|
||||
@@ -1,17 +1,17 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::aabb::AABB;
|
||||
use crate::flip_normals::FlipNormals;
|
||||
use crate::hitable::Hit;
|
||||
use crate::hitable::HitRecord;
|
||||
use crate::hitable_list::HitableList;
|
||||
use crate::material::Material;
|
||||
use crate::ray::Ray;
|
||||
use crate::rect::XYRect;
|
||||
use crate::rect::XZRect;
|
||||
use crate::rect::YZRect;
|
||||
use crate::vec3::Vec3;
|
||||
use crate::{
|
||||
aabb::AABB,
|
||||
flip_normals::FlipNormals,
|
||||
hitable::{Hit, HitRecord},
|
||||
hitable_list::HitableList,
|
||||
material::Material,
|
||||
ray::Ray,
|
||||
rect::{XYRect, XZRect, YZRect},
|
||||
vec3::Vec3,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Cuboid {
|
||||
p_min: Vec3,
|
||||
p_max: Vec3,
|
||||
@@ -22,6 +22,9 @@ impl Cuboid {
|
||||
// This clippy doesn't work right with Arc.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn new(p_min: Vec3, p_max: Vec3, material: Arc<dyn Material>) -> Cuboid {
|
||||
assert!(p_min.x <= p_max.x);
|
||||
assert!(p_min.y <= p_max.y);
|
||||
assert!(p_min.z <= p_max.z);
|
||||
Cuboid {
|
||||
p_min,
|
||||
p_max,
|
||||
44
rtiow/renderer/src/debug_hit.rs
Normal file
44
rtiow/renderer/src/debug_hit.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use crate::{
|
||||
aabb::AABB,
|
||||
hitable::{Hit, HitRecord},
|
||||
material::Lambertian,
|
||||
ray::Ray,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DebugHit<H>
|
||||
where
|
||||
H: Hit,
|
||||
{
|
||||
hitable: H,
|
||||
material: Lambertian<[f32; 3]>,
|
||||
}
|
||||
|
||||
impl<H> DebugHit<H>
|
||||
where
|
||||
H: Hit,
|
||||
{
|
||||
pub fn new(hitable: H) -> DebugHit<H>
|
||||
where {
|
||||
DebugHit {
|
||||
hitable,
|
||||
material: Lambertian::new([0.2, 0.2, 0.2]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<H> Hit for DebugHit<H>
|
||||
where
|
||||
H: Hit,
|
||||
{
|
||||
fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option<HitRecord> {
|
||||
if let Some(hit) = self.hitable.hit(r, t_min, t_max) {
|
||||
return Some(HitRecord { t: hit.t, ..hit });
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn bounding_box(&self, t_min: f32, t_max: f32) -> Option<AABB> {
|
||||
self.hitable.bounding_box(t_min, t_max)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
use crate::aabb::AABB;
|
||||
use crate::hitable::Hit;
|
||||
use crate::hitable::HitRecord;
|
||||
use crate::ray::Ray;
|
||||
use crate::{
|
||||
aabb::AABB,
|
||||
hitable::{Hit, HitRecord},
|
||||
ray::Ray,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FlipNormals<H>
|
||||
where
|
||||
H: Hit,
|
||||
139
rtiow/renderer/src/glowybox.rs
Normal file
139
rtiow/renderer/src/glowybox.rs
Normal file
@@ -0,0 +1,139 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
aabb::AABB,
|
||||
cuboid::Cuboid,
|
||||
hitable::{Hit, HitRecord},
|
||||
material::Material,
|
||||
ray::Ray,
|
||||
vec3::Vec3,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Glowybox {
|
||||
p_min: Vec3,
|
||||
p_max: Vec3,
|
||||
main: Cuboid,
|
||||
edges: [Cuboid; 12],
|
||||
}
|
||||
|
||||
impl Glowybox {
|
||||
// This clippy doesn't work right with Arc.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn new(
|
||||
p_min: Vec3,
|
||||
p_max: Vec3,
|
||||
edge_thickness: f32,
|
||||
main_material: Arc<dyn Material>,
|
||||
edge_material: Arc<dyn Material>,
|
||||
) -> Glowybox {
|
||||
assert!(p_min.x < p_max.x);
|
||||
assert!(p_min.y < p_max.y);
|
||||
assert!(p_min.z < p_max.z);
|
||||
let main = Cuboid::new(p_min, p_max, main_material);
|
||||
// Top edges
|
||||
let ht = edge_thickness / 2.;
|
||||
let edges = [
|
||||
// Top edges
|
||||
Cuboid::new(
|
||||
[p_min.x - ht, p_max.y - ht, p_min.z - ht].into(),
|
||||
[p_min.x + ht, p_max.y + ht, p_max.z + ht].into(),
|
||||
Arc::clone(&edge_material),
|
||||
),
|
||||
Cuboid::new(
|
||||
[p_min.x - ht, p_max.y - ht, p_min.z - ht].into(),
|
||||
[p_max.x + ht, p_max.y + ht, p_min.z + ht].into(),
|
||||
Arc::clone(&edge_material),
|
||||
),
|
||||
Cuboid::new(
|
||||
[p_max.x - ht, p_max.y - ht, p_min.z - ht].into(),
|
||||
[p_max.x + ht, p_max.y + ht, p_max.z + ht].into(),
|
||||
Arc::clone(&edge_material),
|
||||
),
|
||||
Cuboid::new(
|
||||
[p_min.x - ht, p_max.y - ht, p_max.z - ht].into(),
|
||||
[p_max.x + ht, p_max.y + ht, p_max.z + ht].into(),
|
||||
Arc::clone(&edge_material),
|
||||
),
|
||||
// Bottom edges
|
||||
Cuboid::new(
|
||||
[p_min.x - ht, p_min.y - ht, p_min.z - ht].into(),
|
||||
[p_min.x + ht, p_min.y + ht, p_max.z + ht].into(),
|
||||
Arc::clone(&edge_material),
|
||||
),
|
||||
Cuboid::new(
|
||||
[p_min.x - ht, p_min.y - ht, p_min.z - ht].into(),
|
||||
[p_max.x + ht, p_min.y + ht, p_min.z + ht].into(),
|
||||
Arc::clone(&edge_material),
|
||||
),
|
||||
Cuboid::new(
|
||||
[p_max.x - ht, p_min.y - ht, p_min.z - ht].into(),
|
||||
[p_max.x + ht, p_min.y + ht, p_max.z + ht].into(),
|
||||
Arc::clone(&edge_material),
|
||||
),
|
||||
Cuboid::new(
|
||||
[p_min.x - ht, p_min.y - ht, p_max.z - ht].into(),
|
||||
[p_max.x + ht, p_min.y + ht, p_max.z + ht].into(),
|
||||
Arc::clone(&edge_material),
|
||||
),
|
||||
// Middle edges
|
||||
Cuboid::new(
|
||||
[p_min.x - ht, p_min.y - ht, p_min.z - ht].into(),
|
||||
[p_min.x + ht, p_max.y + ht, p_min.z + ht].into(),
|
||||
Arc::clone(&edge_material),
|
||||
),
|
||||
Cuboid::new(
|
||||
[p_min.x - ht, p_min.y - ht, p_max.z - ht].into(),
|
||||
[p_min.x + ht, p_max.y + ht, p_max.z + ht].into(),
|
||||
Arc::clone(&edge_material),
|
||||
),
|
||||
Cuboid::new(
|
||||
[p_max.x - ht, p_min.y - ht, p_min.z - ht].into(),
|
||||
[p_max.x + ht, p_max.y + ht, p_min.z + ht].into(),
|
||||
Arc::clone(&edge_material),
|
||||
),
|
||||
Cuboid::new(
|
||||
[p_max.x - ht, p_min.y - ht, p_max.z - ht].into(),
|
||||
[p_max.x + ht, p_max.y + ht, p_max.z + ht].into(),
|
||||
Arc::clone(&edge_material),
|
||||
),
|
||||
];
|
||||
let p_min = [p_min.x - ht, p_min.y - ht, p_min.z - ht].into();
|
||||
let p_max = [p_max.x + ht, p_max.y + ht, p_max.z + ht].into();
|
||||
Glowybox {
|
||||
p_min,
|
||||
p_max,
|
||||
main,
|
||||
edges,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Hit for Glowybox {
|
||||
fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option<HitRecord> {
|
||||
let mut edge_hit = None;
|
||||
for edge in &self.edges {
|
||||
if let Some(hit) = edge.hit(r, t_min, t_max) {
|
||||
edge_hit = Some(hit);
|
||||
break;
|
||||
}
|
||||
}
|
||||
let main_hit = self.main.hit(r, t_min, t_max);
|
||||
match (edge_hit, main_hit) {
|
||||
(Some(ehit), Some(mhit)) => {
|
||||
if mhit.t < ehit.t {
|
||||
Some(mhit)
|
||||
} else {
|
||||
Some(ehit)
|
||||
}
|
||||
}
|
||||
(Some(ehit), None) => Some(ehit),
|
||||
(None, Some(mhit)) => Some(mhit),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn bounding_box(&self, _t_min: f32, _t_max: f32) -> Option<AABB> {
|
||||
Some(AABB::new(self.p_min, self.p_max))
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
use std::sync::Arc;
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
use crate::aabb::AABB;
|
||||
use crate::material::Material;
|
||||
use crate::ray::Ray;
|
||||
use crate::vec3::Vec3;
|
||||
use crate::{aabb::AABB, material::Material, ray::Ray, vec3::Vec3};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HitRecord<'m> {
|
||||
pub t: f32,
|
||||
pub uv: (f32, f32),
|
||||
@@ -13,7 +11,7 @@ pub struct HitRecord<'m> {
|
||||
pub material: &'m dyn Material,
|
||||
}
|
||||
|
||||
pub trait Hit: Send + Sync {
|
||||
pub trait Hit: Send + Sync + Debug {
|
||||
fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option<HitRecord>;
|
||||
fn bounding_box(&self, t_min: f32, t_max: f32) -> Option<AABB>;
|
||||
}
|
||||
@@ -26,3 +24,12 @@ impl Hit for Arc<dyn Hit> {
|
||||
(**self).bounding_box(t_min, t_max)
|
||||
}
|
||||
}
|
||||
|
||||
impl Hit for Box<dyn Hit> {
|
||||
fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option<HitRecord> {
|
||||
(**self).hit(r, t_min, t_max)
|
||||
}
|
||||
fn bounding_box(&self, t_min: f32, t_max: f32) -> Option<AABB> {
|
||||
(**self).bounding_box(t_min, t_max)
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
use std;
|
||||
|
||||
use crate::aabb::surrounding_box;
|
||||
use crate::aabb::AABB;
|
||||
use crate::hitable::Hit;
|
||||
use crate::hitable::HitRecord;
|
||||
use crate::ray::Ray;
|
||||
use crate::vec3::Vec3;
|
||||
use crate::{
|
||||
aabb::{surrounding_box, AABB},
|
||||
hitable::{Hit, HitRecord},
|
||||
ray::Ray,
|
||||
vec3::Vec3,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct HitableList {
|
||||
list: Vec<Box<dyn Hit>>,
|
||||
}
|
||||
@@ -1,47 +1,29 @@
|
||||
#![doc(html_root_url = "https://docs.rs/human_format")]
|
||||
|
||||
#![allow(dead_code)]
|
||||
//! From https://raw.githubusercontent.com/BobGneu/human-format-rs/master/src/lib.rs
|
||||
//! `human_format` provides facilitates creating a formatted string, converting between numbers that are beyond typical
|
||||
//! `human` provides facilitates creating a formatted string, converting between numbers that are beyond typical
|
||||
//! needs for humans into a simpler string that conveys the gist of the meaning of the number.
|
||||
//!
|
||||
//! ## Setup
|
||||
//!
|
||||
//! Add the library to your dependencies listing
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! human_format = "0.2"
|
||||
//! ```
|
||||
//!
|
||||
//! Add the crate reference at your crate root
|
||||
//!
|
||||
//! ```rust
|
||||
//! extern crate human_format;
|
||||
//! ```
|
||||
//!
|
||||
//! Print some human readable strings
|
||||
//!
|
||||
//! ```rust
|
||||
//! use renderer::human;
|
||||
//!
|
||||
//! // "1.00 k"
|
||||
//! let tmpStr = human_format::Formatter::new()
|
||||
//! .format(1000.0);
|
||||
//! let tmpStr = human::Formatter::new().format(1000.0);
|
||||
//! # assert_eq!(tmpStr, "1.00 k");
|
||||
//!
|
||||
//! // "1.00 M"
|
||||
//! let tmpStr2 = human_format::Formatter::new()
|
||||
//! .format(1000000.0);
|
||||
//! let tmpStr2 = human::Formatter::new().format(1000000.0);
|
||||
//! # assert_eq!(tmpStr2, "1.00 M");
|
||||
//!
|
||||
//! // "1.00 B"
|
||||
//! let tmpStr3 = human_format::Formatter::new()
|
||||
//! .format(1000000000.0);
|
||||
//! let tmpStr3 = human::Formatter::new().format(1000000000.0);
|
||||
//! # assert_eq!(tmpStr3, "1.00 B");
|
||||
//! ```
|
||||
//!
|
||||
//! If you are so inspired you can even try playing with units and customizing your `Scales`
|
||||
//!
|
||||
//! For more examples you should review the examples on github: [tests/demo.rs](https://github.com/BobGneu/human-format-rs/blob/master/tests/demo.rs)
|
||||
//!
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ScaledValue {
|
||||
@@ -72,7 +54,7 @@ impl Formatter {
|
||||
Formatter {
|
||||
decimals: 2,
|
||||
separator: " ".to_owned(),
|
||||
scales: Scales::SI(),
|
||||
scales: Scales::si(),
|
||||
forced_units: "".to_owned(),
|
||||
forced_suffix: "".to_owned(),
|
||||
}
|
||||
@@ -144,18 +126,24 @@ impl Formatter {
|
||||
|
||||
let magnitude_multiplier = self.scales.get_magnitude_multipler(&suffix);
|
||||
|
||||
(result * magnitude_multiplier)
|
||||
result * magnitude_multiplier
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Formatter {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Scales {
|
||||
/// Instantiates a new `Scales` with SI keys
|
||||
pub fn new() -> Self {
|
||||
Scales::SI()
|
||||
Scales::si()
|
||||
}
|
||||
|
||||
/// Instantiates a new `Scales` with SI keys
|
||||
pub fn SI() -> Self {
|
||||
pub fn si() -> Self {
|
||||
Scales {
|
||||
base: 1000,
|
||||
suffixes: vec![
|
||||
@@ -173,7 +161,7 @@ impl Scales {
|
||||
}
|
||||
|
||||
/// Instantiates a new `Scales` with Binary keys
|
||||
pub fn Binary() -> Self {
|
||||
pub fn binary() -> Self {
|
||||
Scales {
|
||||
base: 1000,
|
||||
suffixes: vec![
|
||||
@@ -211,15 +199,13 @@ impl Scales {
|
||||
}
|
||||
|
||||
fn get_magnitude_multipler(&self, value: &str) -> f64 {
|
||||
let ndx = 0;
|
||||
|
||||
for ndx in 0..self.suffixes.len() {
|
||||
if value == self.suffixes[ndx] {
|
||||
return self.base.pow(ndx as u32) as f64;
|
||||
}
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
0.0
|
||||
}
|
||||
|
||||
fn to_scaled_value(&self, value: f64) -> ScaledValue {
|
||||
@@ -241,3 +227,9 @@ impl Scales {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Scales {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
use std::fmt;
|
||||
|
||||
use crate::aabb::surrounding_box;
|
||||
use crate::aabb::AABB;
|
||||
use crate::hitable::Hit;
|
||||
use crate::hitable::HitRecord;
|
||||
use crate::ray::Ray;
|
||||
use log::info;
|
||||
|
||||
use crate::{
|
||||
aabb::{surrounding_box, AABB},
|
||||
hitable::{Hit, HitRecord},
|
||||
hitable_list::HitableList,
|
||||
ray::Ray,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum KDTree {
|
||||
Leaf(Box<dyn Hit>),
|
||||
Branch {
|
||||
@@ -20,10 +24,10 @@ pub enum KDTree {
|
||||
// returns a reference.)
|
||||
fn vec_first_into<T>(v: Vec<T>) -> T {
|
||||
if v.len() != 1 {
|
||||
panic!(format!(
|
||||
panic!(
|
||||
"vec_first_into called for vector length != 1, length {}",
|
||||
v.len()
|
||||
));
|
||||
);
|
||||
}
|
||||
v.into_iter().next().unwrap()
|
||||
}
|
||||
@@ -100,6 +104,14 @@ impl KDTree {
|
||||
}),
|
||||
_ => panic!("Unreachable"),
|
||||
};
|
||||
//info!("left_half {:?}", left_half);
|
||||
//info!("right_half {:?}", right_half);
|
||||
if left_half.is_empty() {
|
||||
return KDTree::Leaf(Box::new(HitableList::new(right_half)));
|
||||
};
|
||||
if right_half.is_empty() {
|
||||
return KDTree::Leaf(Box::new(HitableList::new(left_half)));
|
||||
};
|
||||
KDTree::Branch {
|
||||
left: Box::new(KDTree::new(left_half, t_min, t_max)),
|
||||
right: Box::new(KDTree::new(right_half, t_min, t_max)),
|
||||
@@ -144,7 +156,7 @@ fn print_tree(f: &mut fmt::Formatter, depth: usize, kdt: &KDTree) -> fmt::Result
|
||||
impl fmt::Display for KDTree {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
writeln!(f, "kd-tree")?;
|
||||
print_tree(f, 1, &self)
|
||||
print_tree(f, 1, self)
|
||||
}
|
||||
}
|
||||
impl Hit for KDTree {
|
||||
@@ -1,37 +1,30 @@
|
||||
pub mod aabb;
|
||||
pub mod bvh;
|
||||
pub mod bvh_triangles;
|
||||
pub mod camera;
|
||||
pub mod colors;
|
||||
pub mod constant_medium;
|
||||
pub mod cuboid;
|
||||
pub mod debug_hit;
|
||||
pub mod flip_normals;
|
||||
pub mod glowybox;
|
||||
pub mod hitable;
|
||||
pub mod hitable_list;
|
||||
mod human;
|
||||
pub mod human;
|
||||
pub mod kdtree;
|
||||
pub mod material;
|
||||
pub mod moving_sphere;
|
||||
pub mod noise;
|
||||
pub mod output;
|
||||
pub mod parser;
|
||||
pub mod ray;
|
||||
pub mod rect;
|
||||
pub mod renderer;
|
||||
pub mod rotate;
|
||||
pub mod scale;
|
||||
pub mod scenes;
|
||||
pub mod sphere;
|
||||
pub mod texture;
|
||||
pub mod translate;
|
||||
pub mod triangles;
|
||||
pub mod vec3;
|
||||
|
||||
extern crate image;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate num_cpus;
|
||||
extern crate rand;
|
||||
#[macro_use]
|
||||
extern crate structopt;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
#[macro_use]
|
||||
#[cfg(feature = "prom")]
|
||||
extern crate prometheus;
|
||||
@@ -1,38 +1,33 @@
|
||||
use std::sync::Arc;
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
extern crate rand;
|
||||
use self::rand::Rng;
|
||||
use rand::{self, Rng};
|
||||
|
||||
use crate::hitable::HitRecord;
|
||||
use crate::ray::Ray;
|
||||
use crate::texture::Texture;
|
||||
use crate::vec3::dot;
|
||||
use crate::vec3::Vec3;
|
||||
use crate::{
|
||||
hitable::HitRecord,
|
||||
ray::Ray,
|
||||
texture::Texture,
|
||||
vec3::{dot, Vec3},
|
||||
};
|
||||
|
||||
fn random_in_unit_sphere() -> Vec3 {
|
||||
let mut rng = rand::thread_rng();
|
||||
let v = Vec3::new(1., 1., 1.);
|
||||
loop {
|
||||
let p =
|
||||
2. * Vec3::new(
|
||||
rng.gen_range::<f32>(0., 1.),
|
||||
rng.gen_range::<f32>(0., 1.),
|
||||
rng.gen_range::<f32>(0., 1.),
|
||||
) - v;
|
||||
let p = 2. * Vec3::new(rng.gen(), rng.gen(), rng.gen()) - v;
|
||||
if p.squared_length() < 1. {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Debug)]
|
||||
pub struct ScatterResponse {
|
||||
pub scattered: Ray,
|
||||
pub attenutation: Vec3,
|
||||
pub reflected: bool,
|
||||
}
|
||||
|
||||
pub trait Material: Send + Sync {
|
||||
pub trait Material: Send + Sync + Debug {
|
||||
fn scatter(&self, r_in: &Ray, rec: &HitRecord) -> ScatterResponse;
|
||||
fn emitted(&self, _u: f32, _v: f32, _p: Vec3) -> Vec3 {
|
||||
Vec3::new(0., 0., 0.)
|
||||
@@ -57,6 +52,7 @@ impl Material for Box<dyn Material> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Isotropic<T>
|
||||
where
|
||||
T: Texture,
|
||||
@@ -87,6 +83,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Lambertian<T>
|
||||
where
|
||||
T: Texture,
|
||||
@@ -118,6 +115,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Metal {
|
||||
albedo: Vec3,
|
||||
fuzzy: f32,
|
||||
@@ -172,6 +170,7 @@ fn schlick(cosine: f32, ref_idx: f32) -> f32 {
|
||||
r0 + (1. - r0) * (1. - cosine).powf(5.)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Dielectric {
|
||||
ref_idx: f32,
|
||||
}
|
||||
@@ -201,7 +200,7 @@ impl Material for Dielectric {
|
||||
let scattered = if let Some(refracted) = refract(r_in.direction, outward_normal, ni_over_nt)
|
||||
{
|
||||
let mut rng = rand::thread_rng();
|
||||
if rng.gen_range::<f32>(0., 1.) < schlick(cosine, self.ref_idx) {
|
||||
if rng.gen::<f32>() < schlick(cosine, self.ref_idx) {
|
||||
Ray::new(rec.p, reflected, r_in.time)
|
||||
} else {
|
||||
Ray::new(rec.p, refracted, r_in.time)
|
||||
@@ -218,6 +217,7 @@ impl Material for Dielectric {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DiffuseLight<T>
|
||||
where
|
||||
T: Texture,
|
||||
@@ -251,6 +251,20 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DebugMaterial {}
|
||||
|
||||
impl Material for DebugMaterial {
|
||||
fn scatter(&self, _r_in: &Ray, rec: &HitRecord) -> ScatterResponse {
|
||||
let dir = Vec3::new(0., -1., -1.).unit_vector();
|
||||
ScatterResponse {
|
||||
scattered: Ray::new(rec.p, dir, 0.),
|
||||
attenutation: [1., 1., 1.].into(),
|
||||
reflected: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -1,13 +1,13 @@
|
||||
use crate::aabb::surrounding_box;
|
||||
use crate::aabb::AABB;
|
||||
use crate::hitable::Hit;
|
||||
use crate::hitable::HitRecord;
|
||||
use crate::material::Material;
|
||||
use crate::ray::Ray;
|
||||
use crate::sphere::get_sphere_uv;
|
||||
use crate::vec3::dot;
|
||||
use crate::vec3::Vec3;
|
||||
use crate::{
|
||||
aabb::{surrounding_box, AABB},
|
||||
hitable::{Hit, HitRecord},
|
||||
material::Material,
|
||||
ray::Ray,
|
||||
sphere::get_sphere_uv,
|
||||
vec3::{dot, Vec3},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MovingSphere<M>
|
||||
where
|
||||
M: Material,
|
||||
@@ -1,10 +1,11 @@
|
||||
use log::trace;
|
||||
/// Implements the concepts from https://lodev.org/cgtutor/randomnoise.html
|
||||
use rand;
|
||||
|
||||
use crate::noise::NoiseSource;
|
||||
use crate::vec3::Vec3;
|
||||
use crate::{noise::NoiseSource, vec3::Vec3};
|
||||
|
||||
const NOISE_SIZE: usize = 128;
|
||||
#[derive(Debug)]
|
||||
pub struct Lode {
|
||||
// Using fixed array causes stack overflow.
|
||||
noise: Vec<Vec<Vec<f32>>>, //[[[f32; NOISE_SIZE]; NOISE_SIZE]; NOISE_SIZE],
|
||||
@@ -21,7 +22,7 @@ impl Lode {
|
||||
for x in 0..NOISE_SIZE {
|
||||
for y in 0..NOISE_SIZE {
|
||||
for z in 0..NOISE_SIZE {
|
||||
noise[x][y][z] = rng.gen_range::<f32>(0., 1.);
|
||||
noise[x][y][z] = rng.gen();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
pub mod lode;
|
||||
pub mod perlin;
|
||||
|
||||
use std::f32::consts::PI;
|
||||
use std::{f32::consts::PI, fmt::Debug};
|
||||
|
||||
use serde_derive::Deserialize;
|
||||
|
||||
use crate::vec3::Vec3;
|
||||
|
||||
pub trait NoiseSource: Send + Sync {
|
||||
pub trait NoiseSource: Send + Sync + Debug {
|
||||
/// value returns noise on the interval [0., 1.).
|
||||
fn value(&self, p: Vec3) -> f32;
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
// There are many math functions in this file, so we allow single letter variable names.
|
||||
#![allow(clippy::many_single_char_names)]
|
||||
use rand::Rng;
|
||||
use log::trace;
|
||||
use rand::{seq::SliceRandom, Rng};
|
||||
|
||||
use crate::noise::NoiseSource;
|
||||
use crate::vec3::dot;
|
||||
use crate::vec3::Vec3;
|
||||
use crate::{
|
||||
noise::NoiseSource,
|
||||
vec3::{dot, Vec3},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Perlin {
|
||||
ran_vec: Vec<Vec3>,
|
||||
perm_x: Vec<usize>,
|
||||
@@ -20,9 +23,9 @@ where
|
||||
(0..256)
|
||||
.map(|_| {
|
||||
Vec3::new(
|
||||
rng.gen_range::<f32>(-1., 1.),
|
||||
rng.gen_range::<f32>(-1., 1.),
|
||||
rng.gen_range::<f32>(-1., 1.),
|
||||
rng.gen_range(-1. ..1.),
|
||||
rng.gen_range(-1. ..1.),
|
||||
rng.gen_range(-1. ..1.),
|
||||
)
|
||||
.unit_vector()
|
||||
})
|
||||
@@ -33,8 +36,8 @@ fn perlin_generate_perm<R>(rng: &mut R) -> Vec<usize>
|
||||
where
|
||||
R: Rng,
|
||||
{
|
||||
let mut p: Vec<usize> = (0..256).map(|i| i).collect();
|
||||
rng.shuffle(&mut p);
|
||||
let mut p: Vec<usize> = (0..256).collect();
|
||||
p.shuffle(rng);
|
||||
p
|
||||
}
|
||||
|
||||
250
rtiow/renderer/src/output.rs
Normal file
250
rtiow/renderer/src/output.rs
Normal file
@@ -0,0 +1,250 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::File,
|
||||
io::BufWriter,
|
||||
net::TcpStream,
|
||||
path::Path,
|
||||
sync::{Arc, Mutex},
|
||||
time,
|
||||
};
|
||||
|
||||
use chrono::Local;
|
||||
use image;
|
||||
use log::info;
|
||||
use serde_derive::Serialize;
|
||||
use tev_client::{PacketCreateImage, PacketUpdateImage, TevClient};
|
||||
|
||||
use crate::{renderer::Scene, vec3::Vec3};
|
||||
|
||||
// Main RGB image output from rendering the scene.
|
||||
pub const MAIN_IMAGE: &str = "@final";
|
||||
// Debug image for adaptive pixels subsampling.
|
||||
// Red indicates recursion hit maximum depth splitting the pixel.
|
||||
// Green indicates no subdivision necessary
|
||||
pub const ADAPTIVE_DEPTH: &str = "adaptive_depth";
|
||||
// Grey scale showing rays cast per pixel.
|
||||
pub const RAYS_PER_PIXEL: &str = "rays_per_pixel";
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ImageMetadata {
|
||||
name: String,
|
||||
image: String,
|
||||
binary: String,
|
||||
ratio: f32,
|
||||
size: (usize, usize),
|
||||
format: ImageType,
|
||||
}
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Data<'s> {
|
||||
timestamp: i64,
|
||||
render_time_seconds: f32,
|
||||
scene: &'s Scene,
|
||||
image_metadata: Vec<ImageMetadata>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Serialize)]
|
||||
pub enum ImageType {
|
||||
RGB01,
|
||||
Grey01,
|
||||
GreyNormalized,
|
||||
}
|
||||
|
||||
struct Image {
|
||||
w: usize,
|
||||
h: usize,
|
||||
pix: Vec<Vec3>,
|
||||
}
|
||||
|
||||
impl Image {
|
||||
fn new(w: usize, h: usize) -> Image {
|
||||
Image {
|
||||
w,
|
||||
h,
|
||||
pix: (0..w * h).map(|_| [0., 0., 0.].into()).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn put_pixel(&mut self, x: usize, y: usize, p: Vec3) {
|
||||
let offset = x + y * self.w;
|
||||
self.pix[offset] = p;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OutputManager {
|
||||
images: Arc<Mutex<HashMap<String, (ImageType, Image)>>>,
|
||||
tev_client: Option<Arc<Mutex<TevClient>>>,
|
||||
}
|
||||
|
||||
impl OutputManager {
|
||||
pub fn new(tev_addr: &Option<String>) -> std::io::Result<OutputManager> {
|
||||
let tev_client = if let Some(addr) = tev_addr {
|
||||
Some(Arc::new(Mutex::new(TevClient::wrap(TcpStream::connect(
|
||||
addr,
|
||||
)?))))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(OutputManager {
|
||||
images: Arc::new(Mutex::new(HashMap::new())),
|
||||
tev_client,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn register_image(&self, name: String, dimensions: (usize, usize), it: ImageType) {
|
||||
let mut images = self.images.lock().unwrap();
|
||||
images.insert(name.clone(), (it, Image::new(dimensions.0, dimensions.1)));
|
||||
self.tev_client.clone().map(|c| {
|
||||
c.lock().unwrap().send(PacketCreateImage {
|
||||
image_name: &name,
|
||||
grab_focus: false,
|
||||
width: dimensions.0 as u32,
|
||||
height: dimensions.1 as u32,
|
||||
channel_names: &["R", "G", "B"],
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
pub fn set_pixel(&self, name: &str, x: usize, y: usize, pixel: Vec3) {
|
||||
let mut images = self.images.lock().unwrap();
|
||||
let (_it, img) = images
|
||||
.get_mut(name)
|
||||
.unwrap_or_else(|| panic!("couldn't find image named '{}'", name));
|
||||
let y_inv = img.h - y - 1;
|
||||
img.put_pixel(x, y_inv, pixel);
|
||||
self.tev_client.clone().map(|c| {
|
||||
c.lock().unwrap().send(PacketUpdateImage {
|
||||
image_name: &name,
|
||||
grab_focus: false,
|
||||
channel_names: &["R", "G", "B"],
|
||||
channel_offsets: &[0, 1, 2],
|
||||
channel_strides: &[0, 0, 0],
|
||||
x: x as u32,
|
||||
y: y_inv as u32,
|
||||
width: 1,
|
||||
height: 1,
|
||||
data: &[pixel.x, pixel.y, pixel.z],
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
pub fn set_pixel_grey(&self, name: &str, x: usize, y: usize, grey: f32) {
|
||||
let mut images = self.images.lock().unwrap();
|
||||
let (_it, img) = images
|
||||
.get_mut(name)
|
||||
.unwrap_or_else(|| panic!("couldn't find image named '{}'", name));
|
||||
let y_inv = img.h - y - 1;
|
||||
img.put_pixel(x, y_inv, [grey, grey, grey].into());
|
||||
}
|
||||
pub fn write_images<P: AsRef<Path>>(
|
||||
&self,
|
||||
scene: &Scene,
|
||||
render_time: time::Duration,
|
||||
output_dir: P,
|
||||
) -> std::io::Result<()> {
|
||||
let output_dir: &Path = output_dir.as_ref();
|
||||
let now = Local::now();
|
||||
let images = self.images.lock().unwrap();
|
||||
// Write out images in consistent order.
|
||||
let mut names = images.keys().collect::<Vec<_>>();
|
||||
names.sort();
|
||||
let mut image_metadata = Vec::new();
|
||||
for name in &names {
|
||||
let (it, img) = images.get(*name).unwrap();
|
||||
let image = format!("{}.png", name);
|
||||
let binary = format!("{}.json", name);
|
||||
let ratio = img.w as f32 / img.h as f32;
|
||||
let size = (img.w, img.h);
|
||||
let image_path = output_dir.join(&image);
|
||||
let binary_path = output_dir.join(&binary);
|
||||
image_metadata.push(ImageMetadata {
|
||||
name: name.to_string(),
|
||||
image,
|
||||
binary,
|
||||
ratio,
|
||||
size,
|
||||
format: *it,
|
||||
});
|
||||
info!("Saving {}", image_path.to_string_lossy());
|
||||
match it {
|
||||
ImageType::RGB01 => {
|
||||
let mut out_img = image::RgbImage::new(img.w as u32, img.h as u32);
|
||||
out_img
|
||||
.enumerate_pixels_mut()
|
||||
.enumerate()
|
||||
.for_each(|(i, (_x, _y, p))| {
|
||||
let pixel = img.pix[i];
|
||||
*p = image::Rgb([
|
||||
(pixel[0] * 255.).min(255.) as u8,
|
||||
(pixel[1] * 255.).min(255.) as u8,
|
||||
(pixel[2] * 255.).min(255.) as u8,
|
||||
])
|
||||
});
|
||||
out_img.save(image_path)?;
|
||||
}
|
||||
ImageType::Grey01 => {
|
||||
let mut out_img = image::GrayImage::new(img.w as u32, img.h as u32);
|
||||
out_img
|
||||
.enumerate_pixels_mut()
|
||||
.enumerate()
|
||||
.for_each(|(i, (_x, _y, p))| {
|
||||
let pixel = img.pix[i];
|
||||
*p = image::Luma([(pixel[0] * 255.).min(255.) as u8])
|
||||
});
|
||||
out_img.save(image_path)?;
|
||||
}
|
||||
ImageType::GreyNormalized => {
|
||||
let mut out_img = image::GrayImage::new(img.w as u32, img.h as u32);
|
||||
|
||||
let max_val = img.pix.iter().map(|v| v.x).fold(0., f32::max);
|
||||
out_img
|
||||
.enumerate_pixels_mut()
|
||||
.enumerate()
|
||||
.for_each(|(i, (_x, _y, p))| {
|
||||
let pixel = img.pix[i];
|
||||
*p = image::Luma([(pixel[0] / max_val * 255.).min(255.) as u8])
|
||||
});
|
||||
out_img.save(image_path)?;
|
||||
}
|
||||
};
|
||||
info!("Saving {}", binary_path.to_string_lossy());
|
||||
let f = File::create(output_dir.join(binary_path))?;
|
||||
let f = BufWriter::new(f);
|
||||
match it {
|
||||
ImageType::RGB01 => {
|
||||
serde_json::ser::to_writer(
|
||||
f,
|
||||
&img.pix
|
||||
.iter()
|
||||
.map(|v| [v.x, v.y, v.z])
|
||||
.collect::<Vec<[f32; 3]>>(),
|
||||
)?;
|
||||
}
|
||||
ImageType::Grey01 | ImageType::GreyNormalized => {
|
||||
serde_json::ser::to_writer(
|
||||
f,
|
||||
&img.pix.iter().map(|v| v.x).collect::<Vec<f32>>(),
|
||||
)?;
|
||||
}
|
||||
};
|
||||
}
|
||||
let f = File::create(output_dir.join("data.json"))?;
|
||||
let f = BufWriter::new(f);
|
||||
serde_json::ser::to_writer(
|
||||
f,
|
||||
&Data {
|
||||
timestamp: now.timestamp(),
|
||||
render_time_seconds: render_time.as_secs_f32(),
|
||||
scene,
|
||||
image_metadata,
|
||||
},
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
trait ImageSaver {
|
||||
fn save<Q>(&self, path: Q) -> std::io::Result<()>
|
||||
where
|
||||
Q: AsRef<Path> + Sized;
|
||||
}
|
||||
206
rtiow/renderer/src/parser.rs
Normal file
206
rtiow/renderer/src/parser.rs
Normal file
@@ -0,0 +1,206 @@
|
||||
use crate::{
|
||||
bvh_triangles::BVHTriangles,
|
||||
camera::Camera,
|
||||
cuboid::Cuboid,
|
||||
hitable::Hit,
|
||||
hitable_list::HitableList,
|
||||
material::{Dielectric, DiffuseLight, Isotropic, Lambertian, Material, Metal},
|
||||
renderer::Scene,
|
||||
sphere::Sphere,
|
||||
texture::{EnvMap, Texture},
|
||||
};
|
||||
use chrono::IsoWeek;
|
||||
use serde::Deserialize;
|
||||
use std::{collections::HashMap, fs::File, io::BufReader, path::PathBuf, sync::Arc};
|
||||
use stl::STL;
|
||||
use thiserror::Error;
|
||||
use vec3::Vec3;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Config {
|
||||
scene: SceneConfig,
|
||||
camera: CameraConfig,
|
||||
materials: Vec<MaterialConfig>,
|
||||
hitables: Vec<HitableConfig>,
|
||||
envmap: Option<EnvMapConfig>,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ConfigError {
|
||||
#[error("failed to load image")]
|
||||
ImageError(#[from] image::ImageError),
|
||||
#[error("failed to parser STL")]
|
||||
STLError(#[from] stl::ParseError),
|
||||
#[error("I/O error")]
|
||||
IOError(#[from] std::io::Error),
|
||||
#[error("duplication material named '{0}'")]
|
||||
DuplicateMaterial(String),
|
||||
#[error("unkown material named '{0}'")]
|
||||
UnknownMaterial(String),
|
||||
}
|
||||
|
||||
impl TryFrom<Config> for Scene {
|
||||
type Error = ConfigError;
|
||||
|
||||
fn try_from(c: Config) -> Result<Scene, Self::Error> {
|
||||
let mut materials = HashMap::new();
|
||||
for mc in c.materials {
|
||||
let v: Arc<dyn Material> = match mc.material {
|
||||
Materials::Metal { albedo, fuzzy } => Arc::new(Metal::new(albedo, fuzzy)),
|
||||
Materials::Dielectric { ref_idx } => Arc::new(Dielectric::new(ref_idx)),
|
||||
Materials::DiffuseLight { texture } => Arc::new(DiffuseLight::new(texture)),
|
||||
Materials::Isotropic { texture } => Arc::new(Isotropic::new(texture)),
|
||||
Materials::Lambertian { texture } => Arc::new(Lambertian::new(texture)),
|
||||
};
|
||||
if materials.insert(mc.name.clone(), v).is_some() {
|
||||
return Err(ConfigError::DuplicateMaterial(mc.name));
|
||||
}
|
||||
}
|
||||
|
||||
let hitables: Result<Vec<Box<dyn Hit>>, Self::Error> = c
|
||||
.hitables
|
||||
.into_iter()
|
||||
.map(|hc| -> Result<Box<dyn Hit>, Self::Error> {
|
||||
match hc.hitable {
|
||||
Hitables::Sphere { center, radius } => Ok(Box::new(Sphere::new(
|
||||
center,
|
||||
radius,
|
||||
Arc::clone(
|
||||
materials
|
||||
.get(&hc.material_name)
|
||||
.ok_or(ConfigError::UnknownMaterial(hc.material_name))?,
|
||||
),
|
||||
))),
|
||||
Hitables::Cuboid { min, max } => Ok(Box::new(Cuboid::new(
|
||||
min.into(),
|
||||
max.into(),
|
||||
Arc::clone(
|
||||
materials
|
||||
.get(&hc.material_name)
|
||||
.ok_or(ConfigError::UnknownMaterial(hc.material_name))?,
|
||||
),
|
||||
))),
|
||||
Hitables::STL { path, scale } => {
|
||||
let r = BufReader::new(File::open(path)?);
|
||||
let stl = STL::parse(r, false)?;
|
||||
Ok(Box::new(BVHTriangles::new(
|
||||
&stl,
|
||||
Arc::clone(
|
||||
materials
|
||||
.get(&hc.material_name)
|
||||
.ok_or(ConfigError::UnknownMaterial(hc.material_name))?,
|
||||
),
|
||||
scale.unwrap_or(1.),
|
||||
)))
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let hitables = hitables?;
|
||||
|
||||
let world: Box<dyn Hit> = Box::new(HitableList::new(hitables));
|
||||
let mut env_map: Option<EnvMap> = None;
|
||||
|
||||
if let Some(em) = c.envmap {
|
||||
let im = image::open(em.path)?.into_rgb();
|
||||
env_map = Some(EnvMap::new(im));
|
||||
};
|
||||
|
||||
let camera = make_camera(&c.camera, c.scene.width, c.scene.height);
|
||||
|
||||
let scene = Scene {
|
||||
world,
|
||||
camera,
|
||||
env_map,
|
||||
subsamples: c.scene.subsamples.unwrap_or(8),
|
||||
adaptive_subsampling: c.scene.adaptive_subsampling,
|
||||
num_threads: c.scene.num_threads,
|
||||
width: c.scene.width,
|
||||
height: c.scene.height,
|
||||
global_illumination: c.scene.global_illumination.unwrap_or(true),
|
||||
};
|
||||
Ok(scene)
|
||||
}
|
||||
}
|
||||
|
||||
fn make_camera(cfg: &CameraConfig, width: usize, height: usize) -> Camera {
|
||||
Camera::new(
|
||||
cfg.lookfrom.into(),
|
||||
cfg.lookat.into(),
|
||||
Vec3::new(0., 1., 0.),
|
||||
cfg.fov,
|
||||
width as f32 / height as f32,
|
||||
cfg.aperture,
|
||||
cfg.focus_dist,
|
||||
cfg.time_min,
|
||||
cfg.time_max,
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct SceneConfig {
|
||||
subsamples: Option<usize>,
|
||||
adaptive_subsampling: Option<f32>,
|
||||
num_threads: Option<usize>,
|
||||
width: usize,
|
||||
height: usize,
|
||||
global_illumination: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
struct HitableConfig {
|
||||
material_name: String,
|
||||
#[serde(flatten)]
|
||||
hitable: Hitables,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
enum Hitables {
|
||||
#[serde(rename = "sphere")]
|
||||
Sphere { center: [f32; 3], radius: f32 },
|
||||
#[serde(rename = "cuboid")]
|
||||
Cuboid { min: [f32; 3], max: [f32; 3] },
|
||||
#[serde(rename = "stl")]
|
||||
STL { path: PathBuf, scale: Option<f32> },
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct MaterialConfig {
|
||||
name: String,
|
||||
#[serde(flatten)]
|
||||
material: Materials,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
enum Materials {
|
||||
#[serde(rename = "metal")]
|
||||
Metal { albedo: [f32; 3], fuzzy: f32 },
|
||||
#[serde(rename = "dielectric")]
|
||||
Dielectric { ref_idx: f32 },
|
||||
// TODO(wathiede): these all take Textures, for now, only support RGB
|
||||
#[serde(rename = "diffuse_light")]
|
||||
DiffuseLight { texture: [f32; 3] },
|
||||
#[serde(rename = "isotropic")]
|
||||
Isotropic { texture: [f32; 3] },
|
||||
#[serde(rename = "lambertian")]
|
||||
Lambertian { texture: [f32; 3] },
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CameraConfig {
|
||||
lookfrom: [f32; 3],
|
||||
lookat: [f32; 3],
|
||||
fov: f32,
|
||||
aperture: f32,
|
||||
focus_dist: f32,
|
||||
time_min: f32,
|
||||
time_max: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct EnvMapConfig {
|
||||
path: PathBuf,
|
||||
}
|
||||
@@ -5,6 +5,9 @@ pub struct Ray {
|
||||
pub origin: Vec3,
|
||||
pub direction: Vec3,
|
||||
pub time: f32,
|
||||
|
||||
// Precache 1/direction, a single ray intersects multiple AABB's, and divides are more
|
||||
// expensive than multiplies.
|
||||
pub inv_direction: Vec3,
|
||||
pub sign: [usize; 3],
|
||||
}
|
||||
@@ -14,7 +17,7 @@ impl Ray {
|
||||
where
|
||||
V: Into<Vec3>,
|
||||
{
|
||||
let direction = direction.into();
|
||||
let direction: Vec3 = direction.into();
|
||||
let origin = origin.into();
|
||||
let inv = 1. / direction;
|
||||
Ray {
|
||||
@@ -1,12 +1,14 @@
|
||||
// There are many math functions in this file, so we allow single letter variable names.
|
||||
#![allow(clippy::many_single_char_names)]
|
||||
use crate::aabb::AABB;
|
||||
use crate::hitable::Hit;
|
||||
use crate::hitable::HitRecord;
|
||||
use crate::material::Material;
|
||||
use crate::ray::Ray;
|
||||
use crate::vec3::Vec3;
|
||||
use crate::{
|
||||
aabb::AABB,
|
||||
hitable::{Hit, HitRecord},
|
||||
material::Material,
|
||||
ray::Ray,
|
||||
vec3::Vec3,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct XYRect<M>
|
||||
where
|
||||
M: Material,
|
||||
@@ -68,6 +70,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct XZRect<M>
|
||||
where
|
||||
M: Material,
|
||||
@@ -129,6 +132,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct YZRect<M>
|
||||
where
|
||||
M: Material,
|
||||
668
rtiow/renderer/src/renderer.rs
Normal file
668
rtiow/renderer/src/renderer.rs
Normal file
@@ -0,0 +1,668 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt,
|
||||
ops::{AddAssign, Range},
|
||||
path::{Path, PathBuf},
|
||||
str, sync,
|
||||
sync::{
|
||||
mpsc::{sync_channel, Receiver, SyncSender},
|
||||
Arc, Mutex,
|
||||
},
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use core_affinity;
|
||||
use log::{info, trace};
|
||||
use num_cpus;
|
||||
use rand::{self, Rng};
|
||||
use serde_derive::Serialize;
|
||||
use structopt::StructOpt;
|
||||
|
||||
use crate::{
|
||||
camera::Camera,
|
||||
hitable::Hit,
|
||||
human,
|
||||
material::{Lambertian, Material},
|
||||
output,
|
||||
output::OutputManager,
|
||||
ray::Ray,
|
||||
scenes,
|
||||
sphere::Sphere,
|
||||
texture::{ConstantTexture, EnvMap},
|
||||
vec3::Vec3,
|
||||
};
|
||||
use strum::{EnumString, EnumVariantNames};
|
||||
|
||||
#[derive(Debug, EnumString, EnumVariantNames, strum::Display)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum Model {
|
||||
BVH,
|
||||
Bench,
|
||||
Book,
|
||||
CornellBox,
|
||||
CornellSmoke,
|
||||
Final,
|
||||
Mandelbrot,
|
||||
PerlinDebug,
|
||||
Spheramid,
|
||||
Stltest,
|
||||
Test,
|
||||
Tron,
|
||||
Tutorial,
|
||||
Dragon,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn scene(&self, opt: &Opt) -> Scene {
|
||||
match self {
|
||||
Model::BVH => scenes::bvh::new(opt),
|
||||
Model::Bench => scenes::bench::new(opt),
|
||||
Model::Book => scenes::book::new(opt),
|
||||
Model::CornellBox => scenes::cornell_box::new(opt),
|
||||
Model::CornellSmoke => scenes::cornell_smoke::new(opt),
|
||||
Model::Dragon => scenes::dragon::new(opt),
|
||||
Model::Final => scenes::final_scene::new(opt),
|
||||
Model::Mandelbrot => scenes::mandelbrot::new(opt),
|
||||
Model::PerlinDebug => scenes::perlin_debug::new(opt),
|
||||
Model::Spheramid => scenes::spheramid::new(opt),
|
||||
Model::Stltest => scenes::stltest::new(opt),
|
||||
Model::Test => scenes::test::new(opt),
|
||||
Model::Tron => scenes::tron::new(opt),
|
||||
Model::Tutorial => scenes::tutorial::new(opt),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ModelParseError(String);
|
||||
|
||||
impl fmt::Display for ModelParseError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "unknown model enum type '{}'", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
#[structopt(name = "tracer", about = "An experimental ray tracer.")]
|
||||
pub struct Opt {
|
||||
/// Image width
|
||||
#[structopt(short = "w", long = "width", default_value = "512")]
|
||||
pub width: usize,
|
||||
/// Image height
|
||||
#[structopt(short = "h", long = "height", default_value = "512")]
|
||||
pub height: usize,
|
||||
/// Number of threads
|
||||
#[structopt(short = "t", long = "num_threads")]
|
||||
pub num_threads: Option<usize>,
|
||||
/// Sub-samples per pixel
|
||||
#[structopt(short = "s", long = "subsample", default_value = "8")]
|
||||
pub subsamples: usize,
|
||||
/// Select scene to render.
|
||||
#[structopt(long = "model")]
|
||||
pub model: Option<Model>,
|
||||
/// Toml config describing scene.
|
||||
#[structopt(long = "config")]
|
||||
pub config: Option<PathBuf>,
|
||||
/// Path to store pprof profile data, i.e. /tmp/cpuprofile.pprof
|
||||
#[structopt(long = "pprof", parse(from_os_str))]
|
||||
pub pprof: Option<PathBuf>,
|
||||
/// Use acceleration data structure, may be BVH or kd-tree depending on scene.
|
||||
#[structopt(long = "use_accel")]
|
||||
pub use_accel: bool,
|
||||
/// Host:port of running tev instance.
|
||||
#[structopt(long = "tev_addr")]
|
||||
pub tev_addr: Option<String>,
|
||||
|
||||
/// Output directory
|
||||
#[structopt(
|
||||
short = "o",
|
||||
long = "output",
|
||||
parse(from_os_str),
|
||||
default_value = "/tmp/tracer"
|
||||
)]
|
||||
pub output: PathBuf,
|
||||
}
|
||||
|
||||
pub fn opt_hash(opt: &Opt) -> String {
|
||||
// TODO(wathiede): add threads.
|
||||
format!(
|
||||
"w:{}-h:{}-s:{}-pprof:{}-model:{}-use_accel:{}-{}",
|
||||
opt.width,
|
||||
opt.height,
|
||||
opt.subsamples,
|
||||
opt.pprof.is_some(),
|
||||
opt.model.as_ref().unwrap().to_string(),
|
||||
opt.use_accel,
|
||||
opt.output.display().to_string().replace('/', "_")
|
||||
)
|
||||
}
|
||||
|
||||
// TODO(wathiede): implement the skips and then the renderer could use json as an input file type.
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Scene {
|
||||
#[serde(skip)]
|
||||
pub world: Box<dyn Hit>,
|
||||
//#[serde(skip)]
|
||||
//pub materials: HashMap<String, Box<dyn Material>>,
|
||||
#[serde(skip)]
|
||||
pub camera: Camera,
|
||||
pub subsamples: usize,
|
||||
/// overrides subsamples setting.
|
||||
pub adaptive_subsampling: Option<f32>,
|
||||
pub num_threads: Option<usize>,
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub global_illumination: bool,
|
||||
#[serde(skip)]
|
||||
pub env_map: Option<EnvMap>,
|
||||
}
|
||||
|
||||
impl Default for Scene {
|
||||
fn default() -> Scene {
|
||||
let lookfrom = Vec3::new(20., 20., 20.);
|
||||
let lookat = Vec3::new(0., 0., 0.);
|
||||
let dist_to_focus = (lookfrom - lookat).length();
|
||||
let aperture = 0.1;
|
||||
let time_min = 0.;
|
||||
let time_max = 1.;
|
||||
let camera = Camera::new(
|
||||
lookfrom,
|
||||
lookat,
|
||||
Vec3::new(0., 1., 0.),
|
||||
70.,
|
||||
1.,
|
||||
aperture,
|
||||
dist_to_focus,
|
||||
time_min,
|
||||
time_max,
|
||||
);
|
||||
Scene {
|
||||
world: Box::new(Sphere::new(
|
||||
Vec3::new(0., 0., 0.),
|
||||
1.0,
|
||||
Lambertian::new(ConstantTexture::new([0., 1., 0.])),
|
||||
)),
|
||||
camera,
|
||||
subsamples: 0,
|
||||
adaptive_subsampling: None,
|
||||
num_threads: None,
|
||||
width: 0,
|
||||
height: 0,
|
||||
global_illumination: false,
|
||||
env_map: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// color will trace ray up to 50 bounces deep accumulating color as it goes. If
|
||||
// global_illumination is true, a default light background color is assumed and will light the
|
||||
// world. If false, it is expected the scene has emissive light sources.
|
||||
fn color(
|
||||
r: Ray,
|
||||
world: &dyn Hit,
|
||||
depth: usize,
|
||||
global_illumination: bool,
|
||||
env_map: &Option<EnvMap>,
|
||||
) -> (Vec3, usize) {
|
||||
if let Some(rec) = world.hit(r, 0.001, std::f32::MAX) {
|
||||
let (u, v) = rec.uv;
|
||||
let emitted = rec.material.emitted(u, v, rec.p);
|
||||
let scatter_response = rec.material.scatter(&r, &rec);
|
||||
if depth < 50 && scatter_response.reflected {
|
||||
let (c, rays) = color(
|
||||
scatter_response.scattered,
|
||||
world,
|
||||
depth + 1,
|
||||
global_illumination,
|
||||
env_map,
|
||||
);
|
||||
return (emitted + scatter_response.attenutation * c, rays + 1);
|
||||
} else {
|
||||
return (emitted, 1);
|
||||
}
|
||||
}
|
||||
if global_illumination {
|
||||
return match env_map {
|
||||
Some(env_map) => (env_map.color(r.direction.unit_vector()), 1),
|
||||
None => {
|
||||
let unit_direction = r.direction.unit_vector();
|
||||
// No hit, choose color from background.
|
||||
let t = 0.5 * (unit_direction.y + 1.);
|
||||
(
|
||||
Vec3::new(1., 1., 1.) * (1. - t) + Vec3::new(0.5, 0.7, 1.) * t,
|
||||
1,
|
||||
)
|
||||
}
|
||||
};
|
||||
}
|
||||
// No global illumination, so background is black.
|
||||
(Vec3::new(0., 0., 0.), 1)
|
||||
}
|
||||
|
||||
const MAX_ADAPTIVE_DEPTH: usize = 10;
|
||||
fn trace_pixel_adaptive(
|
||||
depth: usize,
|
||||
threshold: f32,
|
||||
x: usize,
|
||||
y: usize,
|
||||
x_range: Range<f32>,
|
||||
y_range: Range<f32>,
|
||||
scene: &Scene,
|
||||
output: &OutputManager,
|
||||
) -> (Vec3, usize) {
|
||||
let w = scene.width as f32;
|
||||
let h = scene.height as f32;
|
||||
let x_mid = x_range.start + ((x_range.end - x_range.start) / 2.);
|
||||
let y_mid = y_range.start + ((y_range.end - y_range.start) / 2.);
|
||||
let mc = ((x_mid + x as f32) / w, (y_mid + y as f32) / h);
|
||||
let (center, rays) = color(
|
||||
scene.camera.get_ray(mc.0, mc.1),
|
||||
scene.world.as_ref(),
|
||||
0,
|
||||
scene.global_illumination,
|
||||
&scene.env_map,
|
||||
);
|
||||
if depth == 0 {
|
||||
output.set_pixel(output::ADAPTIVE_DEPTH, x, y, [1., 0., 0.].into());
|
||||
return (center, rays);
|
||||
}
|
||||
// t = top
|
||||
// m = middle
|
||||
// b = bottom
|
||||
// l = left
|
||||
// c = center
|
||||
// r = right
|
||||
let tl = (
|
||||
(x_range.start + x as f32) / w,
|
||||
(y_range.start + y as f32) / h,
|
||||
);
|
||||
let tr = ((x_range.end + x as f32) / w, (y_range.start + y as f32) / h);
|
||||
let bl = ((x_range.start + x as f32) / w, (y_range.end + y as f32) / h);
|
||||
let br = ((x_range.end + x as f32) / w, (y_range.end + y as f32) / h);
|
||||
|
||||
let (corners, rays) = [tl, tr, mc, bl, br]
|
||||
.iter()
|
||||
.map(|(u, v)| {
|
||||
color(
|
||||
scene.camera.get_ray(*u, *v),
|
||||
scene.world.as_ref(),
|
||||
0,
|
||||
scene.global_illumination,
|
||||
&scene.env_map,
|
||||
)
|
||||
})
|
||||
.fold(
|
||||
([0., 0., 0.].into(), 0),
|
||||
|(p1, r1): (Vec3, usize), (p2, r2): (Vec3, usize)| ((p1 + p2), (r1 + r2)),
|
||||
);
|
||||
let corners = corners / 5.;
|
||||
if (corners - center).length() > threshold {
|
||||
let tl = trace_pixel_adaptive(
|
||||
depth - 1,
|
||||
threshold,
|
||||
x,
|
||||
y,
|
||||
x_range.start..x_mid,
|
||||
y_range.start..y_mid,
|
||||
scene,
|
||||
output,
|
||||
);
|
||||
let tr = trace_pixel_adaptive(
|
||||
depth - 1,
|
||||
threshold,
|
||||
x,
|
||||
y,
|
||||
x_mid..x_range.end,
|
||||
y_range.start..y_mid,
|
||||
scene,
|
||||
output,
|
||||
);
|
||||
let bl = trace_pixel_adaptive(
|
||||
depth - 1,
|
||||
threshold,
|
||||
x,
|
||||
y,
|
||||
x_range.start..x_mid,
|
||||
y_mid..y_range.end,
|
||||
scene,
|
||||
output,
|
||||
);
|
||||
let br = trace_pixel_adaptive(
|
||||
depth - 1,
|
||||
threshold,
|
||||
x,
|
||||
y,
|
||||
x_mid..x_range.end,
|
||||
y_mid..y_range.end,
|
||||
scene,
|
||||
output,
|
||||
);
|
||||
let pixel = (tl.0 + tr.0 + bl.0 + br.0) / 4.;
|
||||
let rays = tl.1 + tr.1 + bl.1 + br.1;
|
||||
(pixel, rays)
|
||||
} else {
|
||||
if depth == MAX_ADAPTIVE_DEPTH {
|
||||
output.set_pixel(output::ADAPTIVE_DEPTH, x, y, [0., 1., 0.].into());
|
||||
}
|
||||
(corners, rays)
|
||||
}
|
||||
}
|
||||
|
||||
fn trace_pixel_random(x: usize, y: usize, scene: &Scene) -> (Vec3, usize) {
|
||||
let mut rng = rand::thread_rng();
|
||||
let u = (rng.gen::<f32>() + x as f32) / scene.width as f32;
|
||||
let v = (rng.gen::<f32>() + y as f32) / scene.height as f32;
|
||||
let ray = scene.camera.get_ray(u, v);
|
||||
color(
|
||||
ray,
|
||||
scene.world.as_ref(),
|
||||
0,
|
||||
scene.global_illumination,
|
||||
&scene.env_map,
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
struct RenderStats {
|
||||
rays: usize,
|
||||
pixels: usize,
|
||||
}
|
||||
|
||||
impl AddAssign for RenderStats {
|
||||
fn add_assign(&mut self, other: Self) {
|
||||
*self = Self {
|
||||
rays: self.rays + other.rays,
|
||||
pixels: self.pixels + other.pixels,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn progress(
|
||||
start_time: Instant,
|
||||
last_stat: &RenderStats,
|
||||
current_stat: &RenderStats,
|
||||
time_diff: Duration,
|
||||
pixel_total: usize,
|
||||
) -> String {
|
||||
let human = human::Formatter::new();
|
||||
let pixel_diff = current_stat.pixels - last_stat.pixels;
|
||||
let ray_diff = current_stat.rays - last_stat.rays;
|
||||
let now = Instant::now();
|
||||
let start_diff = now - start_time;
|
||||
let ratio = current_stat.pixels as f32 / pixel_total as f32;
|
||||
let percent = ratio * 100.;
|
||||
let elapsed = start_diff.as_secs_f32();
|
||||
let total = elapsed * (1. / ratio);
|
||||
let eta = total - elapsed;
|
||||
format!(
|
||||
"{:7} / {:7}pixels ({:2.0}%) {:7}pixels/s {:7}rays/s eta {:.0}s",
|
||||
human.format(current_stat.pixels as f64),
|
||||
human.format(pixel_total as f64),
|
||||
percent,
|
||||
human.format(pixel_diff as f64 / time_diff.as_secs_f64()),
|
||||
human.format(ray_diff as f64 / time_diff.as_secs_f64()),
|
||||
eta
|
||||
)
|
||||
}
|
||||
|
||||
enum Request {
|
||||
Pixel { x: usize, y: usize },
|
||||
Line { width: usize, y: usize },
|
||||
// TODO(wathiede): add Cohort that does 4x4 or 8x8 pixel chunks.
|
||||
}
|
||||
|
||||
enum Response {
|
||||
Pixel {
|
||||
x: usize,
|
||||
y: usize,
|
||||
pixel: Vec3,
|
||||
rs: RenderStats,
|
||||
},
|
||||
Line {
|
||||
y: usize,
|
||||
pixels: Vec<Vec3>,
|
||||
rs: RenderStats,
|
||||
},
|
||||
}
|
||||
|
||||
fn render_pixel(scene: &Scene, x: usize, y: usize, output: &OutputManager) -> (Vec3, usize) {
|
||||
let (pixel, rays) = if let Some(threshold) = scene.adaptive_subsampling {
|
||||
trace_pixel_adaptive(
|
||||
MAX_ADAPTIVE_DEPTH,
|
||||
threshold,
|
||||
x,
|
||||
y,
|
||||
0.0..1.0,
|
||||
0.0..1.0,
|
||||
scene,
|
||||
output,
|
||||
)
|
||||
} else {
|
||||
let (pixel, rays) = (0..scene.subsamples)
|
||||
.map(|_| trace_pixel_random(x, y, scene))
|
||||
.fold(
|
||||
([0., 0., 0.].into(), 0),
|
||||
|(p1, r1): (Vec3, usize), (p2, r2): (Vec3, usize)| ((p1 + p2), (r1 + r2)),
|
||||
);
|
||||
output.set_pixel_grey(output::RAYS_PER_PIXEL, x, y, rays as f32);
|
||||
(pixel / scene.subsamples as f32, rays)
|
||||
};
|
||||
// Gamma correct, use gamma 2 correction, which is 1/gamma where gamma=2 which is 1/2 or
|
||||
// sqrt.
|
||||
(
|
||||
Vec3::new(pixel[0].sqrt(), pixel[1].sqrt(), pixel[2].sqrt()),
|
||||
rays,
|
||||
)
|
||||
}
|
||||
|
||||
fn render_worker(
|
||||
tid: usize,
|
||||
scene: &Scene,
|
||||
input_chan: Arc<Mutex<Receiver<Request>>>,
|
||||
output_chan: &SyncSender<Response>,
|
||||
output: &OutputManager,
|
||||
) {
|
||||
loop {
|
||||
let job = { input_chan.lock().unwrap().recv() };
|
||||
match job {
|
||||
Err(err) => {
|
||||
trace!("Shutting down render_worker {}: {}", tid, err);
|
||||
return;
|
||||
}
|
||||
Ok(req) => match req {
|
||||
Request::Line { width, y } => {
|
||||
trace!("tid {} width {} y {}", tid, width, y);
|
||||
let batch = false;
|
||||
if batch {
|
||||
let (pixels, rays): (Vec<Vec3>, Vec<usize>) = (0..width)
|
||||
.map(|x| render_pixel(scene, x, y, output))
|
||||
.collect::<Vec<(_, _)>>()
|
||||
.into_iter()
|
||||
.unzip();
|
||||
let rays = rays.iter().sum();
|
||||
output_chan
|
||||
.send(Response::Line {
|
||||
y,
|
||||
pixels,
|
||||
rs: RenderStats {
|
||||
rays,
|
||||
pixels: width,
|
||||
},
|
||||
})
|
||||
.expect("failed to send pixel response");
|
||||
} else {
|
||||
(0..width).for_each(|x| {
|
||||
let (pixel, rays) = render_pixel(scene, x, y, output);
|
||||
output_chan
|
||||
.send(Response::Pixel {
|
||||
x,
|
||||
y,
|
||||
pixel,
|
||||
rs: RenderStats { rays, pixels: 1 },
|
||||
})
|
||||
.expect("failed to send pixel response");
|
||||
});
|
||||
}
|
||||
}
|
||||
Request::Pixel { x, y } => {
|
||||
trace!("tid {} x {} y {}", tid, x, y);
|
||||
let (pixel, rays) = render_pixel(scene, x, y, output);
|
||||
output_chan
|
||||
.send(Response::Pixel {
|
||||
x,
|
||||
y,
|
||||
pixel,
|
||||
rs: RenderStats { rays, pixels: 1 },
|
||||
})
|
||||
.expect("failed to send line response");
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
lazy_static! {
|
||||
static ref DEBUGGER: Arc<Mutex<OutputManager>> = Arc::new(Mutex::new(OutputManager::new()));
|
||||
}
|
||||
*/
|
||||
|
||||
pub fn render(
|
||||
scene: Scene,
|
||||
output_dir: &Path,
|
||||
tev_addr: &Option<String>,
|
||||
) -> std::result::Result<(), std::io::Error> {
|
||||
// Default to half the cores to disable hyperthreading.
|
||||
let num_threads = scene.num_threads.unwrap_or_else(|| num_cpus::get() / 2);
|
||||
let (pixel_req_tx, pixel_req_rx) = sync_channel(2 * num_threads);
|
||||
let (pixel_resp_tx, pixel_resp_rx) = sync_channel(2 * num_threads);
|
||||
|
||||
let scene = Arc::new(scene);
|
||||
let pixel_req_rx = Arc::new(Mutex::new(pixel_req_rx));
|
||||
info!("Adaptive subsampling: {:?}", scene.adaptive_subsampling);
|
||||
// Retrieve the IDs of all active CPU cores.
|
||||
let core_ids = core_affinity::get_core_ids().unwrap();
|
||||
let core_ids = if core_ids.len() > num_threads {
|
||||
core_ids[..num_threads].to_vec()
|
||||
} else {
|
||||
core_ids
|
||||
};
|
||||
let output = output::OutputManager::new(tev_addr)?;
|
||||
let output = Arc::new(output);
|
||||
|
||||
info!("Creating {} render threads", core_ids.len());
|
||||
output.register_image(
|
||||
output::MAIN_IMAGE.to_string(),
|
||||
(scene.width, scene.height),
|
||||
output::ImageType::RGB01,
|
||||
);
|
||||
if scene.adaptive_subsampling.is_some() {
|
||||
output.register_image(
|
||||
output::ADAPTIVE_DEPTH.to_string(),
|
||||
(scene.width, scene.height),
|
||||
output::ImageType::RGB01,
|
||||
);
|
||||
}
|
||||
output.register_image(
|
||||
output::RAYS_PER_PIXEL.to_string(),
|
||||
(scene.width, scene.height),
|
||||
output::ImageType::GreyNormalized,
|
||||
);
|
||||
// Create a thread for each active CPU core.
|
||||
let mut handles = core_ids
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter(|(i, _id)| *i < num_threads)
|
||||
.map(|(i, id)| {
|
||||
let s = sync::Arc::clone(&scene);
|
||||
let pixel_req_rx = pixel_req_rx.clone();
|
||||
let pixel_resp_tx = pixel_resp_tx.clone();
|
||||
let output = sync::Arc::clone(&output);
|
||||
thread::spawn(move || {
|
||||
core_affinity::set_for_current(id);
|
||||
render_worker(i, &s, pixel_req_rx, &pixel_resp_tx, &output);
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
drop(pixel_req_rx);
|
||||
drop(pixel_resp_tx);
|
||||
|
||||
let (w, h) = (scene.width, scene.height);
|
||||
handles.push(thread::spawn(move || {
|
||||
let batch_line_requests = false;
|
||||
if batch_line_requests {
|
||||
for y in 0..h {
|
||||
pixel_req_tx
|
||||
.send(Request::Line { width: w, y })
|
||||
.expect("failed to send line request");
|
||||
}
|
||||
} else {
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
pixel_req_tx
|
||||
.send(Request::Pixel { x, y })
|
||||
.expect("failed to send pixel request");
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(pixel_req_tx);
|
||||
}));
|
||||
|
||||
info!("Rendering with {} subsamples", scene.subsamples);
|
||||
|
||||
let pixel_total = scene.width * scene.height;
|
||||
let mut last_time = Instant::now();
|
||||
let mut last_stat: RenderStats = Default::default();
|
||||
let mut current_stat: RenderStats = Default::default();
|
||||
let render_start_time = Instant::now();
|
||||
for resp in pixel_resp_rx {
|
||||
match resp {
|
||||
Response::Pixel { x, y, pixel, rs } => {
|
||||
current_stat += rs;
|
||||
output.set_pixel(output::MAIN_IMAGE, x, y, pixel);
|
||||
}
|
||||
Response::Line { y, pixels, rs } => {
|
||||
current_stat += rs;
|
||||
for (x, pixel) in pixels.iter().enumerate() {
|
||||
output.set_pixel(output::MAIN_IMAGE, x, y, *pixel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let now = Instant::now();
|
||||
let time_diff = now - last_time;
|
||||
if time_diff > Duration::from_secs(5) {
|
||||
println!(
|
||||
"{}",
|
||||
progress(
|
||||
render_start_time,
|
||||
&last_stat,
|
||||
¤t_stat,
|
||||
time_diff,
|
||||
pixel_total
|
||||
)
|
||||
);
|
||||
last_stat = current_stat;
|
||||
last_time = now;
|
||||
}
|
||||
}
|
||||
for thr in handles {
|
||||
thr.join().expect("thread join");
|
||||
}
|
||||
let time_diff = Instant::now() - render_start_time;
|
||||
println!(
|
||||
"Render {} seconds {}",
|
||||
time_diff.as_secs_f32(),
|
||||
progress(
|
||||
render_start_time,
|
||||
&Default::default(),
|
||||
¤t_stat,
|
||||
time_diff,
|
||||
pixel_total
|
||||
)
|
||||
);
|
||||
|
||||
output.write_images(&scene, time_diff, output_dir)
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
use std::f32::consts::PI;
|
||||
use std::f32::MAX;
|
||||
use std::f32::MIN;
|
||||
use std::f32::{consts::PI, MAX, MIN};
|
||||
|
||||
use crate::aabb::AABB;
|
||||
use crate::hitable::Hit;
|
||||
use crate::hitable::HitRecord;
|
||||
use crate::ray::Ray;
|
||||
use crate::vec3::Vec3;
|
||||
use crate::{
|
||||
aabb::AABB,
|
||||
hitable::{Hit, HitRecord},
|
||||
ray::Ray,
|
||||
vec3::Vec3,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RotateY<H>
|
||||
where
|
||||
H: Hit,
|
||||
47
rtiow/renderer/src/scale.rs
Normal file
47
rtiow/renderer/src/scale.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use crate::{
|
||||
aabb::AABB,
|
||||
hitable::{Hit, HitRecord},
|
||||
ray::Ray,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Scale<H>
|
||||
where
|
||||
H: Hit,
|
||||
{
|
||||
hitable: H,
|
||||
scale: f32,
|
||||
}
|
||||
|
||||
impl<H> Scale<H>
|
||||
where
|
||||
H: Hit,
|
||||
{
|
||||
pub fn new(hitable: H, scale: f32) -> Scale<H> {
|
||||
Scale { hitable, scale }
|
||||
}
|
||||
}
|
||||
|
||||
impl<H> Hit for Scale<H>
|
||||
where
|
||||
H: Hit,
|
||||
{
|
||||
fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option<HitRecord> {
|
||||
let moved_r = Ray::new(r.origin / self.scale, r.direction, r.time);
|
||||
if let Some(rec) = self.hitable.hit(moved_r, t_min, t_max) {
|
||||
return Some(HitRecord {
|
||||
p: rec.p * self.scale,
|
||||
t: rec.t * self.scale,
|
||||
..rec
|
||||
});
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn bounding_box(&self, t_min: f32, t_max: f32) -> Option<AABB> {
|
||||
if let Some(bbox) = self.hitable.bounding_box(t_min, t_max) {
|
||||
return Some(AABB::new(bbox.min() * self.scale, bbox.max() * self.scale));
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,18 @@
|
||||
use rand;
|
||||
use rand::Rng;
|
||||
use log::trace;
|
||||
use rand::{self, Rng};
|
||||
|
||||
use crate::bvh::BVH;
|
||||
use crate::camera::Camera;
|
||||
use crate::hitable::Hit;
|
||||
use crate::hitable_list::HitableList;
|
||||
use crate::kdtree::KDTree;
|
||||
use crate::material::Lambertian;
|
||||
use crate::renderer::Opt;
|
||||
use crate::renderer::Scene;
|
||||
use crate::sphere::Sphere;
|
||||
use crate::texture::ConstantTexture;
|
||||
use crate::vec3::Vec3;
|
||||
use crate::{
|
||||
bvh::BVH,
|
||||
camera::Camera,
|
||||
hitable::Hit,
|
||||
hitable_list::HitableList,
|
||||
kdtree::KDTree,
|
||||
material::Lambertian,
|
||||
renderer::{Opt, Scene},
|
||||
sphere::Sphere,
|
||||
texture::ConstantTexture,
|
||||
vec3::Vec3,
|
||||
};
|
||||
|
||||
pub fn new(opt: &Opt) -> Scene {
|
||||
let lookfrom = Vec3::new(20., 20., 20.);
|
||||
@@ -36,12 +37,12 @@ pub fn new(opt: &Opt) -> Scene {
|
||||
let len = 1000;
|
||||
for x in 0..len {
|
||||
for z in 0..len {
|
||||
let r = rng.gen_range::<f32>(0., 1.);
|
||||
let g = rng.gen_range::<f32>(0., 1.);
|
||||
let b = rng.gen_range::<f32>(0., 1.);
|
||||
let r = rng.gen();
|
||||
let g = rng.gen();
|
||||
let b = rng.gen();
|
||||
|
||||
let x_pos = (x - len / 2) as f32 + rng.gen_range(-0.1, 0.1);
|
||||
let z_pos = (z - len / 2) as f32 + rng.gen_range(-0.1, 0.1);
|
||||
let x_pos = (x - len / 2) as f32 + rng.gen_range(-0.1..0.1);
|
||||
let z_pos = (z - len / 2) as f32 + rng.gen_range(-0.1..0.1);
|
||||
|
||||
grid.push(Box::new(Sphere::new(
|
||||
Vec3::new(x_pos, 0., z_pos),
|
||||
@@ -72,7 +73,6 @@ pub fn new(opt: &Opt) -> Scene {
|
||||
num_threads: opt.num_threads,
|
||||
width: opt.width,
|
||||
height: opt.height,
|
||||
global_illumination: true,
|
||||
env_map: None,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,16 @@
|
||||
use rand;
|
||||
use rand::Rng;
|
||||
use rand::{self, Rng};
|
||||
|
||||
use crate::camera::Camera;
|
||||
use crate::hitable::Hit;
|
||||
use crate::hitable_list::HitableList;
|
||||
use crate::kdtree::KDTree;
|
||||
use crate::material::Dielectric;
|
||||
use crate::material::Lambertian;
|
||||
use crate::material::Material;
|
||||
use crate::material::Metal;
|
||||
use crate::renderer::Opt;
|
||||
use crate::renderer::Scene;
|
||||
use crate::sphere::Sphere;
|
||||
use crate::texture::CheckerTexture;
|
||||
use crate::texture::ConstantTexture;
|
||||
use crate::texture::EnvMap;
|
||||
use crate::vec3::Vec3;
|
||||
use crate::{
|
||||
camera::Camera,
|
||||
hitable::Hit,
|
||||
hitable_list::HitableList,
|
||||
kdtree::KDTree,
|
||||
material::{Dielectric, Lambertian, Material, Metal},
|
||||
renderer::{Opt, Scene},
|
||||
sphere::Sphere,
|
||||
texture::{CheckerTexture, ConstantTexture, EnvMap},
|
||||
vec3::Vec3,
|
||||
};
|
||||
|
||||
pub fn new(opt: &Opt) -> Scene {
|
||||
let lookfrom = Vec3::new(13., 2., 3.);
|
||||
@@ -51,11 +46,13 @@ pub fn new(opt: &Opt) -> Scene {
|
||||
camera,
|
||||
world,
|
||||
subsamples: opt.subsamples,
|
||||
adaptive_subsampling: Some(0.5),
|
||||
num_threads: opt.num_threads,
|
||||
width: opt.width,
|
||||
height: opt.height,
|
||||
global_illumination: true,
|
||||
env_map: Some(EnvMap::new(skybox)),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +76,7 @@ where
|
||||
1000.,
|
||||
ground_material,
|
||||
))];
|
||||
let mut random = || rng.gen_range::<f32>(0., 1.);
|
||||
let mut random = || rng.gen::<f32>();
|
||||
|
||||
for a in -11..11 {
|
||||
for b in -11..11 {
|
||||
@@ -1,14 +1,16 @@
|
||||
use crate::bvh::BVH;
|
||||
use crate::camera::Camera;
|
||||
use crate::hitable::Hit;
|
||||
use crate::material::Lambertian;
|
||||
use crate::material::Metal;
|
||||
use crate::moving_sphere::MovingSphere;
|
||||
use crate::renderer::Opt;
|
||||
use crate::renderer::Scene;
|
||||
use crate::sphere::Sphere;
|
||||
use crate::texture::ConstantTexture;
|
||||
use crate::vec3::Vec3;
|
||||
use log::trace;
|
||||
|
||||
use crate::{
|
||||
bvh::BVH,
|
||||
camera::Camera,
|
||||
hitable::Hit,
|
||||
material::{Lambertian, Metal},
|
||||
moving_sphere::MovingSphere,
|
||||
renderer::{Opt, Scene},
|
||||
sphere::Sphere,
|
||||
texture::ConstantTexture,
|
||||
vec3::Vec3,
|
||||
};
|
||||
|
||||
pub fn new(opt: &Opt) -> Scene {
|
||||
let lookfrom = Vec3::new(3., 3., 2.);
|
||||
@@ -66,7 +68,6 @@ pub fn new(opt: &Opt) -> Scene {
|
||||
num_threads: opt.num_threads,
|
||||
width: opt.width,
|
||||
height: opt.height,
|
||||
global_illumination: true,
|
||||
env_map: None,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,20 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::camera::Camera;
|
||||
use crate::cuboid::Cuboid;
|
||||
use crate::flip_normals::FlipNormals;
|
||||
use crate::hitable::Hit;
|
||||
use crate::hitable_list::HitableList;
|
||||
use crate::kdtree::KDTree;
|
||||
use crate::material::DiffuseLight;
|
||||
use crate::material::Lambertian;
|
||||
use crate::rect::XYRect;
|
||||
use crate::rect::XZRect;
|
||||
use crate::rect::YZRect;
|
||||
use crate::renderer::Opt;
|
||||
use crate::renderer::Scene;
|
||||
use crate::rotate::RotateY;
|
||||
use crate::texture::ConstantTexture;
|
||||
use crate::translate::Translate;
|
||||
use crate::vec3::Vec3;
|
||||
use crate::{
|
||||
camera::Camera,
|
||||
cuboid::Cuboid,
|
||||
flip_normals::FlipNormals,
|
||||
hitable::Hit,
|
||||
hitable_list::HitableList,
|
||||
kdtree::KDTree,
|
||||
material::{DiffuseLight, Lambertian},
|
||||
rect::{XYRect, XZRect, YZRect},
|
||||
renderer::{Opt, Scene},
|
||||
rotate::RotateY,
|
||||
texture::ConstantTexture,
|
||||
translate::Translate,
|
||||
vec3::Vec3,
|
||||
};
|
||||
|
||||
pub fn new(opt: &Opt) -> Scene {
|
||||
let lookfrom = Vec3::new(278., 278., -800.);
|
||||
@@ -134,6 +132,6 @@ pub fn new(opt: &Opt) -> Scene {
|
||||
width: opt.width,
|
||||
height: opt.height,
|
||||
global_illumination: false,
|
||||
env_map: None,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,21 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::camera::Camera;
|
||||
use crate::constant_medium::ConstantMedium;
|
||||
use crate::cuboid::Cuboid;
|
||||
use crate::flip_normals::FlipNormals;
|
||||
use crate::hitable::Hit;
|
||||
use crate::hitable_list::HitableList;
|
||||
use crate::kdtree::KDTree;
|
||||
use crate::material::DiffuseLight;
|
||||
use crate::material::Lambertian;
|
||||
use crate::material::Material;
|
||||
use crate::rect::XYRect;
|
||||
use crate::rect::XZRect;
|
||||
use crate::rect::YZRect;
|
||||
use crate::renderer::Opt;
|
||||
use crate::renderer::Scene;
|
||||
use crate::rotate::RotateY;
|
||||
use crate::texture::ConstantTexture;
|
||||
use crate::translate::Translate;
|
||||
use crate::vec3::Vec3;
|
||||
use crate::{
|
||||
camera::Camera,
|
||||
constant_medium::ConstantMedium,
|
||||
cuboid::Cuboid,
|
||||
flip_normals::FlipNormals,
|
||||
hitable::Hit,
|
||||
hitable_list::HitableList,
|
||||
kdtree::KDTree,
|
||||
material::{DiffuseLight, Lambertian, Material},
|
||||
rect::{XYRect, XZRect, YZRect},
|
||||
renderer::{Opt, Scene},
|
||||
rotate::RotateY,
|
||||
texture::ConstantTexture,
|
||||
translate::Translate,
|
||||
vec3::Vec3,
|
||||
};
|
||||
|
||||
pub fn new(opt: &Opt) -> Scene {
|
||||
let lookfrom = Vec3::new(278., 278., -800.);
|
||||
@@ -119,7 +116,6 @@ pub fn new(opt: &Opt) -> Scene {
|
||||
num_threads: opt.num_threads,
|
||||
width: opt.width,
|
||||
height: opt.height,
|
||||
global_illumination: false,
|
||||
env_map: None,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
116
rtiow/renderer/src/scenes/dragon.rs
Normal file
116
rtiow/renderer/src/scenes/dragon.rs
Normal file
@@ -0,0 +1,116 @@
|
||||
use std::{
|
||||
f32::consts::PI,
|
||||
io::{BufReader, Cursor},
|
||||
};
|
||||
|
||||
use stl::STL;
|
||||
|
||||
use crate::{
|
||||
bvh_triangles::BVHTriangles,
|
||||
camera::Camera,
|
||||
colors::generate_rainbow,
|
||||
hitable::Hit,
|
||||
hitable_list::HitableList,
|
||||
kdtree::KDTree,
|
||||
material::{Lambertian, Metal},
|
||||
renderer::{Opt, Scene},
|
||||
rotate::RotateY,
|
||||
scale::Scale,
|
||||
sphere::Sphere,
|
||||
texture::{ConstantTexture, EnvMap},
|
||||
translate::Translate,
|
||||
vec3::Vec3,
|
||||
};
|
||||
|
||||
pub fn new(opt: &Opt) -> Scene {
|
||||
let lookfrom = Vec3::new(0., 80., 80.);
|
||||
let lookat = Vec3::new(0., 0., 0.);
|
||||
let dist_to_focus = 10.0;
|
||||
let aperture = 0.0;
|
||||
let time_min = 0.;
|
||||
let time_max = 1.;
|
||||
let camera = Camera::new(
|
||||
lookfrom,
|
||||
lookat,
|
||||
Vec3::new(0., 1., 0.),
|
||||
45.,
|
||||
opt.width as f32 / opt.height as f32,
|
||||
aperture,
|
||||
dist_to_focus,
|
||||
time_min,
|
||||
time_max,
|
||||
);
|
||||
//let dragon_material = Dielectric::new(1.5);
|
||||
let dragon_material = Metal::new(Vec3::new(0.6, 0.6, 0.6), 0.0);
|
||||
//let dragon_material = Lambertian::new(ConstantTexture::new(Vec3::new(1.0, 1.0, 0.2)));
|
||||
|
||||
let ground_color = if opt.use_accel {
|
||||
ConstantTexture::new(Vec3::new(1.0, 0.4, 0.4))
|
||||
} else {
|
||||
ConstantTexture::new(Vec3::new(0.4, 0.4, 0.4))
|
||||
};
|
||||
|
||||
let stl_cube = STL::parse(
|
||||
BufReader::new(Cursor::new(include_bytes!("../../stls/dragon.stl"))),
|
||||
false,
|
||||
)
|
||||
.expect("failed to parse cube");
|
||||
let _light_size = 50.;
|
||||
let _light_height = 200.;
|
||||
let sphere_radius = 5.;
|
||||
let circle_radius = 40.;
|
||||
let num_spheres = 16;
|
||||
let palette = generate_rainbow(num_spheres);
|
||||
let spheres: Vec<Box<dyn Hit>> = (0..num_spheres)
|
||||
.map(|i| (i, i as f32, num_spheres as f32))
|
||||
.map(|(idx, idx_f, n)| (idx, idx_f * 2. * PI / n))
|
||||
.map(|(idx, rad)| -> Box<dyn Hit> {
|
||||
let x = circle_radius * rad.cos();
|
||||
let y = 4. * sphere_radius;
|
||||
let z = circle_radius * rad.sin();
|
||||
let c = palette[idx];
|
||||
Box::new(Sphere::new(
|
||||
[x, y, z],
|
||||
sphere_radius,
|
||||
Lambertian::new(ConstantTexture::new(c)),
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
let mut objects: Vec<Box<dyn Hit>> = vec![
|
||||
Box::new(Sphere::new(
|
||||
Vec3::new(0., 0.1, -0.5),
|
||||
0.1,
|
||||
Lambertian::new([0., 1., 1.]),
|
||||
)),
|
||||
// Earth sized sphere
|
||||
//Box::new(Sphere::new( Vec3::new(0., -10000., 0.), 10000., Lambertian::new(ground_color),)),
|
||||
// STL Mesh
|
||||
Box::new(crate::debug_hit::DebugHit::new(RotateY::new(
|
||||
Translate::new(
|
||||
BVHTriangles::new(&stl_cube, dragon_material, 250.),
|
||||
[0., -10., 0.],
|
||||
),
|
||||
180.,
|
||||
))),
|
||||
];
|
||||
objects.extend(spheres);
|
||||
|
||||
let world: Box<dyn Hit> = if opt.use_accel {
|
||||
Box::new(KDTree::new(objects, time_min, time_max))
|
||||
} else {
|
||||
Box::new(HitableList::new(objects))
|
||||
};
|
||||
let skybox_bytes = include_bytes!("../../images/envmap.jpg");
|
||||
let skybox = image::load_from_memory(skybox_bytes).unwrap().to_rgb();
|
||||
Scene {
|
||||
camera,
|
||||
world,
|
||||
subsamples: opt.subsamples,
|
||||
num_threads: opt.num_threads,
|
||||
width: opt.width,
|
||||
height: opt.height,
|
||||
global_illumination: true,
|
||||
env_map: Some(EnvMap::new(skybox)),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,26 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use image;
|
||||
use rand;
|
||||
use rand::Rng;
|
||||
use rand::{self, Rng};
|
||||
|
||||
use crate::camera::Camera;
|
||||
use crate::constant_medium::ConstantMedium;
|
||||
use crate::cuboid::Cuboid;
|
||||
use crate::hitable::Hit;
|
||||
use crate::hitable_list::HitableList;
|
||||
use crate::kdtree::KDTree;
|
||||
use crate::material::Dielectric;
|
||||
use crate::material::DiffuseLight;
|
||||
use crate::material::Lambertian;
|
||||
use crate::material::Material;
|
||||
use crate::material::Metal;
|
||||
use crate::moving_sphere::MovingSphere;
|
||||
use crate::noise::perlin::Perlin;
|
||||
use crate::noise::NoiseType;
|
||||
use crate::rect::XZRect;
|
||||
use crate::renderer::Opt;
|
||||
use crate::renderer::Scene;
|
||||
use crate::rotate::RotateY;
|
||||
use crate::sphere::Sphere;
|
||||
use crate::texture::ConstantTexture;
|
||||
use crate::texture::ImageTexture;
|
||||
use crate::texture::NoiseTexture;
|
||||
use crate::translate::Translate;
|
||||
use crate::vec3::Vec3;
|
||||
use crate::{
|
||||
camera::Camera,
|
||||
constant_medium::ConstantMedium,
|
||||
cuboid::Cuboid,
|
||||
hitable::Hit,
|
||||
hitable_list::HitableList,
|
||||
kdtree::KDTree,
|
||||
material::{Dielectric, DiffuseLight, Lambertian, Material, Metal},
|
||||
moving_sphere::MovingSphere,
|
||||
noise::{perlin::Perlin, NoiseType},
|
||||
rect::XZRect,
|
||||
renderer::{Opt, Scene},
|
||||
rotate::RotateY,
|
||||
sphere::Sphere,
|
||||
texture::{ConstantTexture, ImageTexture, NoiseTexture},
|
||||
translate::Translate,
|
||||
vec3::Vec3,
|
||||
};
|
||||
|
||||
pub fn new(opt: &Opt) -> Scene {
|
||||
let lookfrom = Vec3::new(478., 278., -600.);
|
||||
@@ -64,7 +57,7 @@ pub fn new(opt: &Opt) -> Scene {
|
||||
let z0 = -1000. + j as f32 * w;
|
||||
let y0 = 0.;
|
||||
let x1 = x0 + w;
|
||||
let y1 = 100. * (rng.gen_range::<f32>(0., 1.) + 0.01);
|
||||
let y1 = 100. * (rng.gen::<f32>() + 0.01);
|
||||
let z1 = z0 + w;
|
||||
boxlist.push(Box::new(Cuboid::new(
|
||||
Vec3::new(x0, y0, z0),
|
||||
@@ -152,9 +145,9 @@ pub fn new(opt: &Opt) -> Scene {
|
||||
for _ in 0..ns {
|
||||
boxlist.push(Box::new(Sphere::new(
|
||||
[
|
||||
165. * rng.gen_range::<f32>(0., 1.),
|
||||
165. * rng.gen_range::<f32>(0., 1.),
|
||||
165. * rng.gen_range::<f32>(0., 1.),
|
||||
165. * rng.gen::<f32>(),
|
||||
165. * rng.gen::<f32>(),
|
||||
165. * rng.gen::<f32>(),
|
||||
],
|
||||
10.,
|
||||
Arc::clone(&white),
|
||||
@@ -172,7 +165,6 @@ pub fn new(opt: &Opt) -> Scene {
|
||||
num_threads: opt.num_threads,
|
||||
width: opt.width,
|
||||
height: opt.height,
|
||||
global_illumination: false,
|
||||
env_map: None,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,18 @@
|
||||
use rand;
|
||||
|
||||
use crate::camera::Camera;
|
||||
use crate::hitable::Hit;
|
||||
use crate::hitable_list::HitableList;
|
||||
use crate::kdtree::KDTree;
|
||||
use crate::material::DiffuseLight;
|
||||
use crate::material::Lambertian;
|
||||
use crate::noise::perlin::Perlin;
|
||||
use crate::noise::NoiseType;
|
||||
use crate::rect::XYRect;
|
||||
use crate::rect::XZRect;
|
||||
use crate::rect::YZRect;
|
||||
use crate::renderer::Opt;
|
||||
use crate::renderer::Scene;
|
||||
use crate::sphere::Sphere;
|
||||
use crate::texture::ConstantTexture;
|
||||
use crate::texture::ImageTexture;
|
||||
use crate::texture::Mandelbrot;
|
||||
use crate::texture::NoiseTexture;
|
||||
use crate::vec3::Vec3;
|
||||
use crate::{
|
||||
camera::Camera,
|
||||
hitable::Hit,
|
||||
hitable_list::HitableList,
|
||||
kdtree::KDTree,
|
||||
material::{DiffuseLight, Lambertian},
|
||||
noise::{perlin::Perlin, NoiseType},
|
||||
rect::{XYRect, XZRect, YZRect},
|
||||
renderer::{Opt, Scene},
|
||||
sphere::Sphere,
|
||||
texture::{ConstantTexture, ImageTexture, Mandelbrot, NoiseTexture},
|
||||
vec3::Vec3,
|
||||
};
|
||||
|
||||
pub fn new(opt: &Opt) -> Scene {
|
||||
let lookfrom = Vec3::new(20., 20., 20.);
|
||||
@@ -152,7 +146,6 @@ pub fn new(opt: &Opt) -> Scene {
|
||||
num_threads: opt.num_threads,
|
||||
width: opt.width,
|
||||
height: opt.height,
|
||||
global_illumination: false,
|
||||
env_map: None,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,12 @@ pub mod book;
|
||||
pub mod bvh;
|
||||
pub mod cornell_box;
|
||||
pub mod cornell_smoke;
|
||||
pub mod dragon;
|
||||
pub mod final_scene;
|
||||
pub mod mandelbrot;
|
||||
pub mod perlin_debug;
|
||||
pub mod spheramid;
|
||||
pub mod stltest;
|
||||
pub mod test;
|
||||
pub mod tron;
|
||||
pub mod tutorial;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user