Compare commits

...

361 Commits

Author SHA1 Message Date
f93d215fc2 rtiow: add support to send rendering output to https://github.com/Tom94/tev 2023-10-29 10:44:59 -07:00
593632a9e3 rtiow: move image writer to an instance instead of wrappers around global. 2023-10-29 09:17:32 -07:00
ed2ee749cd rtiow: set default binary to run tracer. 2023-10-29 09:17:10 -07:00
42f3daefaa Random WIP 2023-09-21 10:25:23 -07:00
9430a1e7da cargo update and address workspace lint. 2023-09-21 10:24:50 -07:00
d3153032b1 rtiow: add basic Metal material support to parser. 2023-02-15 21:32:08 -08:00
35071b06ac rtiow: make hitables an enum of various types. 2023-02-15 19:44:17 -08:00
5f0e7a26dd rtiow: implement EnvMap in parser. 2023-02-15 15:46:23 -08:00
6fbdb49ce1 rtiow: lint. 2023-02-15 15:46:08 -08:00
23bc5b0bf0 rtiow: latest cargo files. 2023-02-15 15:40:25 -08:00
37137ac9ca rtiow: add bones of a scene file format based on toml. 2023-02-15 14:40:25 -08:00
9353ff675e rtiow: add toml and set debugging to optimized builds. 2023-02-15 14:39:04 -08:00
4b8bd84a84 rtiow: cargo update; cargo upgrade 2023-02-13 21:00:45 -08:00
e19ec20c7b rtiow: Many changes
Add debugging hit DebugHit.
Move some color helpers from mandelbrot to colors mod.
Implement Texture trait for [f32;3]
2023-02-13 20:57:10 -08:00
deb46acb5a rtiow: fix bug in calculation of t in Scale hitable. 2023-02-13 20:55:13 -08:00
1076e6dcaf rtiow: BVHTriangles use binning to speed up BVH building. 2023-02-12 16:52:07 -08:00
7d9750b9d0 rtiow: AABB handle infinite bounds better. 2023-02-12 16:51:25 -08:00
88b8c547e0 rtiow: cleanup elapsed time logging. 2023-02-12 14:09:11 -08:00
ac555beafc rtiow: BVHTriangles use a fixed 100 divisions for split planes. 2023-02-12 13:47:29 -08:00
0158f9ea15 rtiow: BVHTriangles use BVHNode::cost for readability. 2023-02-12 13:26:45 -08:00
450342c3d4 rtiow: BVHTriangles refactor part of subdivide into find_best_split_plane. 2023-02-12 13:14:02 -08:00
41f9fa2742 rtiow: add proptest. 2023-02-12 13:05:01 -08:00
7ec30d8557 rtiow: BVHTriangles faster BVH traversal. 2023-02-12 13:04:08 -08:00
f51d3396f4 rtiow: tweak scenes and cleanup lint 2023-02-12 13:03:01 -08:00
a2f9166b5a rtiow: extend progress interval from 1s -> 5s. 2023-02-12 12:46:53 -08:00
f73a471cb6 rtiow: AABB add hit_distance to return Option<f32> instead of bool. 2023-02-12 12:46:10 -08:00
cd149755cb rtiow: pretty print CLI options. 2023-02-11 11:16:37 -08:00
9188ce17fa rtiow: print BVH stats. 2023-02-11 11:16:24 -08:00
63975bad96 rtiow: BVHTriangles use SAH for division and leave original triangles untouched. 2023-02-10 17:04:23 -08:00
df928e1779 rtiow: aabb add area/grow methods and infinite constructor. 2023-02-10 17:04:01 -08:00
3c28466d68 rtiow: shrink BVHNode to 32 bytes. 2023-02-05 14:15:34 -08:00
a0b79ee2fa rtiow: add commented out failing test. 2023-02-02 20:33:58 -08:00
4e62975d56 rtiow: fixed width formatting when printing Vec3. 2023-02-02 19:46:32 -08:00
eea5c7c61e rtiow: better debugging, testing and fix some BVHTriangles bugs. 2023-02-02 19:46:00 -08:00
188b550fb7 rtiow: add simple debugging material. 2023-02-02 16:57:33 -08:00
f7c5f29e67 rtiow: AABB more compact Debug representation. Loosen assertions. 2023-02-02 16:56:43 -08:00
6ab3021403 rtiow: add more rays in bvh_triangles test and better failure logging. 2023-02-01 14:24:24 -08:00
d213e04c11 rtiow: default thread count to half the cores.
This seems to be faster than using both HT buddies.
2023-02-01 14:18:21 -08:00
739b38b4ed rtiow: make compile on aarch64. 2023-01-31 20:48:33 -08:00
5ba5aa5f5d rtiow: cargo fix. 2023-01-30 19:59:40 -08:00
4e6e9bf78a rtiow: cargo update. 2023-01-30 19:57:57 -08:00
beeb5e479b rtiow: add dragon scene, tune stltest to compare with cuboid.
Refactor enum Model to use strum.
2023-01-30 19:55:28 -08:00
fc1bfa419e rtiow: BVHTriangles add tests comparing results with Cuboid impl. 2023-01-29 19:56:51 -08:00
95827a4a52 rtiow: descend both children in BVHTriangles::hit. 2023-01-29 09:05:16 -08:00
d3dd002883 rtiow: use hand written SIMD hit test. 2023-01-28 13:04:18 -08:00
ef737c6df9 rtiow: squelch kdtree log spam. 2023-01-28 13:04:01 -08:00
2c490b7e83 vec3: inline many methods for major performance improvement. 2023-01-28 11:29:19 -08:00
63f8fba6a4 rtiow: fix ETA calculation. 2023-01-28 11:28:55 -08:00
5c2786a54d Fix AABB::hit_simd. Add comprehensive AABB hit testing. 2023-01-28 10:38:09 -08:00
2d696932e3 rtiow: add aabb tests and benchmark along with terrible SIMD impl. 2023-01-22 12:03:17 -08:00
27d6c1280b cargo upgrade -p criterion 2023-01-21 16:10:13 -08:00
4506418706 rtiow: remove need for right_child in BVHNode. 2023-01-21 15:59:33 -08:00
1d8aff7905 rtiow: using println and compute ETA in progress. 2023-01-19 21:19:05 -08:00
585ad4805c rtiow: implement triangle renderer that uses BVH internally. 2023-01-19 20:18:51 -08:00
b7f163c5a9 rtiow: minor cleanup. 2023-01-19 20:11:35 -08:00
4ab9425a97 rtiow/vec3: add min/max functions for building new Vec3 from 2 others. 2023-01-19 19:48:59 -08:00
468cba97b3 rtiow: remove unused ray/triangle intersection implementations. 2023-01-18 20:17:05 -08:00
b9ebc186fa rtiow: add new Scale tranformer. 2023-01-18 20:15:06 -08:00
3e9d900f1e Implement Vec3/Vec3 2023-01-18 20:14:49 -08:00
9e81acfda9 Working basic triangle intersection. 2023-01-17 21:32:28 -08:00
f8ec874d13 rtiow: change scene to aid in debugging. 2023-01-15 16:25:31 -08:00
a8756debb8 rtiow: precache some things in Triangles. 2023-01-15 15:29:52 -08:00
a0fb4637b5 rtiow: add ability to render single material triangle mesh. 2023-01-15 15:15:23 -08:00
6069bf9a65 rtiow: don't batch by line, improves parallelism in the long tail. 2023-01-15 12:48:25 -08:00
eeb7813243 rtiow: bump editions to 2021 2023-01-15 11:59:33 -08:00
c644299726 rtiow: update crate use statement in benches. 2023-01-15 11:57:56 -08:00
e6db61543b zigrtiow: commit example test w/ threads. 2023-01-15 11:55:59 -08:00
39eeb79409 rtiow: stub triangles shape created from STLs. 2023-01-15 11:55:11 -08:00
54e72cd81d vec3: helper to create a Vec3 from a single f32. 2023-01-15 11:54:48 -08:00
2d91f781f3 rtiow: remove rustfmt.toml, use systemwide settings. 2023-01-15 11:37:50 -08:00
24e8b4f9cf rtiow: move vec3 to separate crate so it can be used elsewhere. 2023-01-15 11:35:55 -08:00
a12938db95 rtiow: run cargo update and fix build_all_features.sh errors. 2022-09-17 16:51:21 -07:00
4066bf4b85 rtiow: add blox with gloxy edges.
Fixed bug in kdtree that this uncovered.
Marked Hit and it's dependencies as needing to implement the Debug
trait.
2022-09-17 16:45:29 -07:00
b432e9a6dd zigrtiow: create scene from book cover. 2022-08-14 12:18:55 -07:00
62317d57ae zigrtiow: implement depth of field. 2022-08-14 11:36:50 -07:00
8d92cc861e zigrtiow: placeable camera and helpers for creating materials. 2022-08-13 20:59:21 -07:00
f2ade1eee2 zigrtiow: partially configurable camera. 2022-08-13 20:35:15 -07:00
a4baedefec zigrtiow: hollow glass sphere. 2022-08-13 17:25:51 -07:00
8adf1bcadb zigrtiow: some refraction with dielectric. 2022-08-13 17:17:35 -07:00
aea437785a zigrtiow: dielectric w/o internal reflection. 2022-08-13 17:03:08 -07:00
91fd65259c zigrtiow: alloc image on heap to enable larger images. 2022-08-09 21:27:28 -07:00
e5ffe87192 zigrtiow: multithreaded renderer. 2022-08-09 21:18:02 -07:00
ac73d13fb0 zigrtiow: add fuzzy metal reflections. 2022-08-06 08:17:26 -07:00
6b4be0ed1e zigrtiow: add metal material. 2022-08-06 08:14:15 -07:00
85b87a6854 zigrtiow: helper script for development. 2022-08-06 08:07:19 -07:00
d15a9e6c3e zigrtiow: add material property to hittable. 2022-08-06 08:06:53 -07:00
a2012e6742 zigrtiow: use hemisphere random rays. 2022-08-05 18:37:41 -07:00
6d7998ad9f zigrtiow: fix acne 2022-08-05 18:31:09 -07:00
58646e4142 zigrtiow: gamma correct. 2022-08-04 21:46:08 -07:00
94b0f8355e zigrtiow: shoot child rays for diffuse shading. 2022-08-04 21:43:47 -07:00
f4d3129d5a Add TODO to README. 2022-08-04 21:10:16 -07:00
e31f5e0a3a zigrtiow: add camera class and support supersampling. 2022-08-04 21:05:52 -07:00
84a0ba2ec6 zigrtiow: use Sphere, Hittable, and HittableList abstractions. 2022-08-04 20:13:31 -07:00
5043a7e526 Simplified hit_sphere. 2022-07-31 16:57:18 -07:00
622c23d5ed Use normals to color sphere. 2022-07-31 16:52:48 -07:00
f2c68e0b6f Use hit_sphere to draw red circle. 2022-07-31 16:47:59 -07:00
287344c272 Minor debug logging change. 2022-07-31 16:36:33 -07:00
8bc5e347cc Use ray casting to draw "blue sky" image. 2022-07-31 16:34:26 -07:00
f0da916a22 Use stub Vec3 / Color to implement gradient image. 2022-07-30 17:11:44 -07:00
386daf5876 zigrtiow: progress indicator. 2022-07-29 20:35:35 -07:00
a4adefdb23 zigrtiow: use signed ints to match C++ example. 2022-07-29 20:31:30 -07:00
f3aace486b Write test ppm image to stdout. 2022-07-28 22:20:42 -07:00
93bfeb9125 Initial zig shell. 2022-07-28 21:40:03 -07:00
55af087d69 cargo fmt. 2022-07-28 21:39:14 -07:00
78f7ca8956 cargo fmt. 2022-07-28 21:39:00 -07:00
51185e9e84 Merge branch 'master' of https://git.z.xinu.tv/wathiede/raytracers 2022-07-01 08:54:19 -07:00
1ca903c64b Setup rustfmt for everything and address cargo clippy. 2022-06-24 15:14:44 -07:00
5e7139f0ba rtchallenge: Address cargo clippy. 2022-06-24 14:54:49 -07:00
665ae244d7 rtiow: get build_all_features.sh working again.
All checks were successful
continuous-integration/drone Build is passing
2022-06-13 21:22:20 -07:00
e574cdb592 Random changes.
Some checks failed
continuous-integration/drone Build is failing
2022-06-11 17:46:26 -07:00
270a7ec349 eoc12: show use of cube. 2021-08-05 20:41:47 -07:00
de6cd0da4d shapes: add AABB boxes with a cube shape. 2021-08-05 20:35:55 -07:00
7a80179f41 eoc11: example illustrating concepts from chapter 11 and extended pattern concepts from chapter 10. 2021-08-01 19:18:46 -07:00
926fffa29f patterns: add ability to nest patterns 2021-08-01 19:08:36 -07:00
9befbd9ad2 matrices: moving another doctest to unit
All checks were successful
continuous-integration/drone/push Build is passing
2021-07-30 21:59:40 -07:00
c882fc81e5 transformations: moving another doctest to unit 2021-07-30 21:58:32 -07:00
1c2caf2cc5 lights: moving another doctest to unit 2021-07-30 21:51:35 -07:00
9006671a26 intersections: moving another doctest to unit 2021-07-30 21:49:48 -07:00
3838efd134 camera: moving another doctest to unit 2021-07-30 21:44:58 -07:00
e3d8988658 matrices: moving another doctest to unit 2021-07-30 21:33:43 -07:00
e4846de25b rays: move tests from doctest to unit. 2021-07-30 21:28:13 -07:00
4352c03d20 tuples: move tests from doctest to unit. 2021-07-30 21:23:56 -07:00
1d5e5a164b materials: move tests from doctest to unit. 2021-07-30 21:21:51 -07:00
f476822bcd shapes: lint 2021-07-30 21:21:16 -07:00
135a519526 shapes: move tests from doctest to unit. 2021-07-30 20:58:05 -07:00
5d6b3e6d57 patterns: move tests from doctest to unit. 2021-07-30 20:00:08 -07:00
3aea76b35c matrices: move tests from doctest to unit. 2021-07-29 20:33:03 -07:00
cd2a4770ca world: move tests from doctest to unit. 2021-07-28 20:23:59 -07:00
5debb16d10 intersections: move tests from doctest to unit. 2021-07-27 21:51:26 -07:00
42e8ebe3bd Implement transparency, reflections and refraction. 2021-07-26 21:46:04 -07:00
1d61f59935 materials: add transparency and refractive_index to Material. 2021-07-25 16:33:03 -07:00
7f36aecf5e world: add reflection to ray tracer. 2021-07-25 16:28:34 -07:00
0c7bbae4a3 rtchallenge: remove disable-inverse-cache feature. 2021-07-25 14:54:00 -07:00
eaae65712b eoc10: example showing concepts from the chapter. 2021-07-25 14:51:33 -07:00
68709da6c2 patterns: implement checker pattern. 2021-07-25 14:46:26 -07:00
77215193fa patterns: implement ring pattern 2021-07-25 14:37:56 -07:00
74fe69188a patterns: add Gradient pattern. 2021-07-25 14:11:44 -07:00
bdcee49d5a patterns: add builder pattern for creating Patterns. 2021-07-25 13:50:13 -07:00
2e4e8b3dcd patterns: make From for Pattern a little generic. 2021-07-25 13:35:14 -07:00
b9f2c3f0ec patterns: create generic Pattern modeled after StripePattern.
Add TestPattern to validate generic implementation.
Make Material.color use Pattern.
2021-07-25 13:30:40 -07:00
8b79876aee patterns: implement object and pattern transformation awareness. 2021-07-25 11:22:36 -07:00
bfa3282a37 materials: add StripePattern as a Material color option. 2021-07-24 19:36:32 -07:00
3e383c4dbd patters: implemented basic stripe pattern. 2021-07-24 18:20:29 -07:00
c158d92252 eoc9: make width and height CLI flags. 2021-07-23 22:18:20 -07:00
62ad827507 eoc9: implement using prelude and builder pattern.
All checks were successful
continuous-integration/drone/push Build is passing
2021-07-23 22:12:24 -07:00
363f15fb00 camera: implement builder pattern on Camera and add it to prelude. 2021-07-23 22:05:38 -07:00
be2041285c shapes: use a builder pattern with helps in prelude. 2021-07-23 20:56:32 -07:00
958c4c3ee8 prelude: add PointLight and PointLightBuilder. 2021-07-23 20:30:14 -07:00
62cb5e4ec4 prelude: add World and WorldBuilder. 2021-07-23 20:17:31 -07:00
4680c97adc shapes:: helpers for creating Shapes added to the prelude. 2021-07-23 20:03:34 -07:00
0965ac9ddf matrics: create helpers for Matrix4x4 and add it to prelude. 2021-07-23 18:38:35 -07:00
4936839723 prelude: create prelude and add some tuple helpers to it. 2021-07-22 21:34:18 -07:00
c500d56d4d lights: make intensity parameter generic. 2021-07-22 21:33:09 -07:00
c058d043e0 balls: more experimentation. 2021-07-22 20:56:13 -07:00
166c87dfe5 camera: add default implementations. 2021-07-22 20:44:49 -07:00
9389fed84c balls: experiement with builder pattern. 2021-07-21 21:31:04 -07:00
28fe6fe982 shapes: fix inverse_transform on ShapeBuilder. 2021-07-21 21:30:26 -07:00
44b46187a0 world: add builder pattern. 2021-07-21 20:45:33 -07:00
de898f0b0a Add builder pattern to a few core types.
Add a From<[3;Float]> impl for Color to make things nicer.
2021-07-21 20:42:08 -07:00
e041fd1f6a shapes: implement TestShape. 2021-07-21 19:41:40 -07:00
952ed8bf81 intersections: implement a PartialEq that handles floats. 2021-07-21 19:41:14 -07:00
a553786807 tuples: derive Default on Tuple. 2021-07-21 19:40:12 -07:00
b42adcebfc rays: derive Default, Clone and PartialEq on Ray. 2021-07-21 19:39:47 -07:00
d999e8196b eoc9: add ceiling. 2021-07-21 15:15:59 -07:00
fc5ef09cc3 shapes: fix translation handling in intersection test. 2021-07-21 15:06:06 -07:00
e419994fae eoc9: add third light now that the we don't normalize it 2021-07-21 14:51:51 -07:00
ee8ef4e2c5 world: don't normalize light brightness by quantity of lights.
That's not how they work in real life.
2021-07-21 14:47:00 -07:00
41f3b63ad0 eoc9: one less light, and brighter 2021-07-21 14:44:29 -07:00
b2dc0e9509 eoc9: use plane in a scene. 2021-07-21 14:40:44 -07:00
2d8a3927f4 shapes: lint cleanup. 2021-07-21 14:40:27 -07:00
5600d6c561 shapes: name space helper implementations in a sub module.
All checks were successful
continuous-integration/drone/push Build is passing
2021-07-21 13:17:25 -07:00
0e8a0e4163 shapes: implement plane geometry.
All checks were successful
continuous-integration/drone/push Build is passing
2021-07-21 12:57:16 -07:00
2f85697b88 rays: derive Debug on Ray. 2021-07-21 12:53:19 -07:00
c0e422a7eb shapes: create generic Shape object with Sphere implementation.
All checks were successful
continuous-integration/drone/push Build is passing
2021-07-20 22:17:50 -07:00
7741766635 drone: add build config for rtchallenge
All checks were successful
continuous-integration/drone/push Build is passing
2021-07-18 21:17:56 -07:00
3799f93393 eoc8: add third light for effect 2021-07-18 21:15:38 -07:00
1629b2cbfa Add multiple light support. 2021-07-18 21:10:04 -07:00
839642b886 camera: make supersampling configurable, wire it up in eoc8.
All checks were successful
continuous-integration/drone/push Build is passing
2021-07-18 20:30:06 -07:00
c4f10126e3 git-hooks: build and test with --no-default-features too.
Stores output in separate target/ subdir, so it shouldn't get clobbered.
2021-07-18 20:16:34 -07:00
7a8ed15017 eoc8: zoom the fov. 2021-07-18 20:01:40 -07:00
5d8024a485 features: rename s/_/-/g and make double sizes Floats default. 2021-07-18 17:29:30 -07:00
ecf7cd7bdc eoc: Updates to work with new Float abstraction. 2021-07-18 17:27:52 -07:00
1c06833658 Fixes for test when using Float == f64. 2021-07-18 17:27:31 -07:00
95de5863cc all: s/f32/Float/g now using customer Float alias. 2021-07-18 17:18:08 -07:00
5d57304d95 eoc8: implement the end of chapter 8 challenge. Note:
This implementation looks like crap and I'm not sure why.
2021-07-18 16:55:29 -07:00
599f484dff camera: protoype supersampling for rayon render pass. 2021-07-18 16:31:36 -07:00
ce998c68eb matrices: rename inverse_old to inverse_rtiow to indicate origin. 2021-07-18 16:30:38 -07:00
37048ddd52 spheres: implement rtiow Sphere::intersect for comparison. 2021-07-18 16:29:45 -07:00
19b2ef6ded intersections: derive Debug for PrecomputedData. 2021-07-18 16:29:21 -07:00
7e450d664e world & intersections: handle shadows. 2021-07-18 13:08:03 -07:00
09047eb713 world: implement World::is_shadowed. 2021-07-18 12:54:18 -07:00
1065702a5d materials: make lighting calculation shadow aware 2021-07-18 12:46:49 -07:00
efdc963a48 camera: update default render strategy to match CLI default 2021-07-18 12:37:29 -07:00
02ef8634c3 camera: update docstring to reflect new findings 2021-07-18 12:36:15 -07:00
7e19d7e61b camera: lint cleanup 2021-07-18 12:14:13 -07:00
dbf5451070 eoc7: make command flag for choosing rendering strategy.
All checks were successful
continuous-integration/drone/push Build is passing
2021-07-18 11:49:56 -07:00
4f88d2c101 camera: make rendering strategy configurable, add workerpool version. 2021-07-18 11:48:58 -07:00
94ea724344 lights & world: derive Clone for PointLight and World. 2021-07-18 11:14:44 -07:00
967920e1fa eoc7: show elapsed time out to milliseconds. 2021-07-18 11:12:19 -07:00
2eeeb2013b camera & spheres: add feature tag disable_inverse_cache 2021-07-18 08:51:51 -07:00
538b8ad364 camera: cache inverse tranform for huge speed up on ray_for_pixel.
WIP parallel render function.
2021-07-17 23:16:50 -07:00
2395c96e01 spheres: cache inverse transform to accelerate normal_at. 2021-07-17 23:10:54 -07:00
5f3bfd744e eoc7: implement end of chapter 7 challenge.
All checks were successful
continuous-integration/drone/push Build is passing
2021-07-17 22:05:54 -07:00
e752536430 camera: correct x/y calculations in Camera::render. 2021-07-17 21:52:11 -07:00
059f710706 camera: implement Camera::render. 2021-07-17 21:45:15 -07:00
ad02d7e945 canvas: remove unnecessary mut on Canvas::get's self parameter. 2021-07-17 21:44:48 -07:00
5911610064 camera: implement Camera::ray_for_pixel. 2021-07-17 21:34:09 -07:00
39f7f77b74 camera: add basic Camera object. 2021-07-17 21:22:07 -07:00
125c96c25f transformations: implement view_transform. 2021-07-17 17:40:35 -07:00
249a2915d9 matrices: minor tweaks to make debugging easier. 2021-07-17 17:40:14 -07:00
9f00485256 Use WHITE and BLACK constants where appropriate 2021-07-17 16:41:22 -07:00
b37398ac40 world: implement World::color_at 2021-07-17 16:12:30 -07:00
86d052d38b materials: use crate definition for BLACK. 2021-07-17 16:12:04 -07:00
b3737dcd5f lib: add constants for BLACK and WHITE colors. 2021-07-17 16:11:31 -07:00
72c6944ab9 world: implement World::shade_hit. 2021-07-17 15:58:57 -07:00
9924330f98 intersections: add inside/outside to prepare_computations. 2021-07-17 15:46:02 -07:00
2316df896a intersections: implement basic prepare_computations. 2021-07-17 15:37:46 -07:00
2c79132ebc world: cleanup lint 2021-07-17 15:37:25 -07:00
e9f2ef0118 world: implement World::intersect. 2021-07-17 15:26:18 -07:00
eebdc270eb world: add test_world constructor for book tests. 2021-07-17 15:12:56 -07:00
3bc9f6f924 world: add default empty World constructor. 2021-07-17 15:07:14 -07:00
cbecaa70ef eoc6: implement challenge for end of chapter 6. 2021-07-17 10:10:38 -07:00
6863b4ecd6 materials: small correction to specular computation. 2021-07-17 10:09:46 -07:00
bad54bb433 eoc5: remove unnecessary import. 2021-07-17 10:09:10 -07:00
d8e5476806 materials: implement Phong lighting. 2021-07-17 09:31:23 -07:00
385ed70d88 tuples: implement EPSILON aware PartialEq for Color.
Mark Color::new const so it can be used in const expressions (like
predefining colors).
2021-07-17 09:30:28 -07:00
1cbfbc8641 spheres: add material to Sphere. 2021-07-17 09:07:08 -07:00
81540cd484 rustfmt: add config to format code in doc comments. 2021-07-17 09:04:54 -07:00
b4428f924c lights & materials: implement the default light and material types. 2021-07-17 08:59:02 -07:00
bcf847660a tuples: implement reflect. 2021-07-17 08:50:26 -07:00
e529710d5d spheres: fix normal_at for transformed spheres. 2021-07-17 08:44:26 -07:00
ac4f5eb9a6 git-hooks: remove debug printing.
All checks were successful
continuous-integration/drone/push Build is passing
2021-07-17 08:14:51 -07:00
f846da18ad envmap: cargo fmt. 2021-07-17 08:14:35 -07:00
e59029a94a git-hooks: add pre-commit hook for fmt and testing checks. 2021-07-17 08:14:08 -07:00
339ce84903 tuples: whitespace cleanup for consistency. 2021-07-17 07:58:15 -07:00
6e7bd1c136 spheres: implement normal_at. 2021-07-17 07:57:59 -07:00
e430e3769e canvas: add parameter to constructor to set background color.
All checks were successful
continuous-integration/drone/push Build is passing
2021-07-16 22:33:22 -07:00
7609201c16 canvas: use ugly pink for default color to ease in debugging. 2021-07-16 22:28:05 -07:00
4bb6a72e4b eoc5: implement suggestiong at end of chapter 5.
All checks were successful
continuous-integration/drone/push Build is passing
2021-07-16 22:25:48 -07:00
cb1b3ec801 sphere: use Intersections as the return type from intersect. 2021-07-16 22:25:26 -07:00
ad7b10322f spheres: enable intersect w/ transformed spheres. 2021-07-16 22:06:37 -07:00
6e73bab37f s/translate/translation/g to match book. 2021-07-16 21:49:49 -07:00
87bf924094 spheres: add transform member to Sphere 2021-07-16 21:48:12 -07:00
9f22d820e7 rays: implement Ray::transform 2021-07-16 21:38:04 -07:00
324a26212a intersections: implement Intersections::hit 2021-07-16 21:28:28 -07:00
c9b42d94b3 intersections: now store object in Intersection. 2021-07-16 21:14:23 -07:00
0ce1e8f7af intersections: create inters module and initial functionality. 2021-07-16 21:06:57 -07:00
8b451a2395 rays: lint cleanup 2021-07-16 21:06:23 -07:00
da98744288 spheres: implement basic unit sphere and intersect. 2021-07-16 20:42:46 -07:00
f44d671573 rays: basic ray with construction and position methods 2021-07-16 20:13:20 -07:00
191760fa13 eoc4: lint cleanup. 2021-07-16 20:12:51 -07:00
12c2382327 eoc4: use Matrix4x4 to perform world to canvas scaling.
All checks were successful
continuous-integration/drone/push Build is passing
2021-07-16 19:59:42 -07:00
5df2917668 eoc4: implement suggestion at the end of chapter 4 2021-07-16 17:12:48 -07:00
af5e61136c matrices: doctest for matrix multiplication ordering. 2021-07-16 16:51:55 -07:00
4b0d882b84 lib & tuples: use crate specific EPSILON definition. 2021-07-16 16:49:55 -07:00
83799a02a9 matrices: implement Matrix4x4::shearing 2021-07-16 16:38:40 -07:00
b8df830460 tuples & matrices: remove float-cmp use.
All checks were successful
continuous-integration/drone/push Build is passing
Implement PartialEq on `Tuple` and `Matrix4x4` using a local `EPSILON`
large enough for our unit tests to pass.
2021-07-06 08:44:21 -07:00
245b02b443 matrices: implement Matrix4x4:rotation_[xyz] 2021-07-05 18:36:43 -07:00
f792d1a626 matrices: implement Matrix4x4::scaling 2021-07-05 17:51:17 -07:00
117d7185e4 matrices: implement Matrix4x4::translate 2021-07-05 17:42:19 -07:00
462c90e8c8 matrices: benchmark Matrix::inverse & inverse_old
All checks were successful
continuous-integration/drone/push Build is passing
2021-07-05 16:44:06 -07:00
ac3a18a864 matrices: implement Matrix4x4::inverse. 2021-07-05 16:26:27 -07:00
656f1c3a94 Small whitespace change. 2021-07-05 16:26:21 -07:00
762cd45f63 implement determinant on 3x3 and 4x4 matrices 2021-07-05 15:27:32 -07:00
d6ad12e344 implement cofactor of 3x3 matrix 2021-07-05 15:07:18 -07:00
f5d79908f6 implement minor for matrix3x3 2021-07-01 21:29:58 -07:00
a69e404817 implement submatrix for matrix4x4 2021-07-01 21:20:40 -07:00
43d95041af implement submatrix for matrix3x3 2021-07-01 21:12:43 -07:00
c97bc25323 Implement 2x2 determinant 2021-07-01 20:54:00 -07:00
dda29eb836 test identity transpose 2021-07-01 20:47:29 -07:00
fa5971faa4 Test identity multiplication 2021-07-01 20:46:05 -07:00
4d649c735b Implement 4x4 * 4x1 2021-06-30 21:02:42 -07:00
3d2d763a3b test 4x4 multiplication 2021-06-29 20:56:08 -07:00
ea6114b9ae test 4x4 equality and inequality 2021-06-29 20:51:38 -07:00
ec0331b88b constructor/index methods for Matrix2x2 and Matrix3x3 2021-06-29 20:48:41 -07:00
72b15e5516 Copy Matrix4x4 impl from pbrt and start on tests 2021-06-29 20:12:23 -07:00
3cf580f607 Fix tests after recent refactor 2021-06-29 19:52:57 -07:00
78a360ae89 More idiomatic constructors.
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-27 10:20:21 -07:00
f24a90b77b eoc2: print filename before saving.
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-24 16:44:45 -07:00
c9ec19c3cd eoc2: update with initial values from the book. 2021-06-24 16:43:12 -07:00
709465dafe eoc2: write up end of chapter 2 example. 2021-06-24 16:40:53 -07:00
7786aa99a1 canvas: enable saving to PNG. 2021-06-24 16:40:33 -07:00
536b6bed1f tuples: make color data pub. 2021-06-24 16:40:21 -07:00
d8f91a823e Add canvas type. 2021-06-24 15:58:18 -07:00
42455d593e Add Color type. 2021-06-24 15:48:04 -07:00
21ac03acfb Implement end of chapter 1 exercises. 2021-06-24 15:35:58 -07:00
df495feb57 Make important types/functions public. 2021-06-24 15:35:43 -07:00
21afbf8e7c Implement dot and cross product for tuples. 2021-06-24 15:15:31 -07:00
1ea90770bc Add magnitude() and normalize() methods to Tuple 2021-06-24 15:07:38 -07:00
758f94acde Implement Add, Div, Mul, Neg, Sub traits for tuple. 2021-06-24 14:47:53 -07:00
b159820bad Metadata.
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-24 11:10:29 -07:00
3952a8ba83 Implement point/vector constructors. 2021-06-24 11:10:11 -07:00
495c64249c Implement most basic tuple 2021-06-24 10:51:35 -07:00
a30a5a383c Update to 2018 edition.
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-13 18:53:36 -07:00
33a126f4d7 Fix over zealous lint. 2021-06-13 18:51:07 -07:00
4d5056428b aobench: lint. 2021-06-13 18:49:32 -07:00
4cddc8571f panic/foramt lint
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-13 18:44:16 -07:00
ea30bc9ed4 Remove direnv setup, use parent. 2021-06-13 18:39:05 -07:00
3fb564ff19 More rand version bump.
Some checks failed
continuous-integration/drone/push Build is failing
2021-06-13 17:46:45 -07:00
d1a04b9b0c Cleanup lint.
Some checks failed
continuous-integration/drone/push Build is failing
2021-06-04 14:45:53 -07:00
3e5a71440e drone: drop sccache usage.
Some checks failed
continuous-integration/drone/push Build is failing
2021-06-04 14:39:46 -07:00
bf8b533b15 Version bump rand 2021-05-31 10:10:45 -07:00
9dcb36612d Add nix / direnv setup
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone Build is failing
2021-04-03 20:28:46 -07:00
ae3b173f3f Update lock file. 2021-04-03 20:26:40 -07:00
a227d54705 rtiow: fix unused-imports build errors.
All checks were successful
continuous-integration/drone/push Build is passing
2019-12-23 15:47:11 -08:00
0249ac6db9 Merge branch 'master' of https://git.z.xinu.tv/wathiede/raytracers
Some checks failed
continuous-integration/drone/push Build is failing
2019-11-24 08:33:07 -08:00
f6e8477107 rtiow: version bumps. 2019-11-24 08:30:18 -08:00
2e13fa317e rtiow: fix doc test error. Add warp to build.
Some checks failed
continuous-integration/drone/push Build is failing
2019-11-09 13:23:57 -08:00
7ad3e82309 rtiow: latest cargo lock.
Some checks failed
continuous-integration/drone/push Build is failing
2019-11-09 13:12:40 -08:00
53398a57b7 rtiow: add noise_explorer_warp. 2019-11-09 13:00:44 -08:00
1ea4ec669a Merge branch 'master' of https://git.z.xinu.tv/wathiede/raytracers 2019-11-09 12:28:52 -08:00
ea40835125 rtiow: new debugging of spheramid scene. 2019-11-09 12:28:26 -08:00
d9d183b1e5 rtiow: break project into multiple workspaces.
Some checks failed
continuous-integration/drone/push Build is failing
2019-11-09 11:56:33 -08:00
2541b76ae6 Merge branch 'master' of https://git.z.xinu.tv/wathiede/raytracers
All checks were successful
continuous-integration/drone/push Build is passing
2019-10-30 20:06:29 -07:00
c21acb49fe rtiow: move image registration to prevent race condition. 2019-10-30 20:04:48 -07:00
f965a00e0c rtiow: buffer json output, waaay faster. 2019-10-30 20:04:18 -07:00
6bd29a2152 rtiow: include render time in metadata written. 2019-10-30 19:55:31 -07:00
5841ab61e8 rtiow: include scene config in metadata generated in output
New spheramid scene forked from tutorial.
2019-10-26 16:10:29 -07:00
56743b5d77 Add checker texture to tutorial image to spruce things up. 2019-10-26 15:21:45 -07:00
bda42922e4 rtiow: new data.json format to support better debugging. 2019-10-26 12:00:28 -07:00
96e74b3ebf rtiow: add greyscale images for debugging. 2019-10-24 14:27:40 -07:00
ea31b570db rtiow: remove prometheus monitoring support. 2019-10-23 18:51:05 -07:00
Bill Thiede
62ea19f6c7 rtiow: don't batch status updates per-line (do per-pixel).
All checks were successful
continuous-integration/drone/push Build is passing
2019-10-22 07:28:00 -07:00
c903a743b5 rtiow: option to send Request::Line results as Line or Pixel.
All checks were successful
continuous-integration/drone/push Build is passing
2019-10-21 21:27:12 -07:00
848e9879cb rtiow: lockless non-global stats keeping. 2019-10-21 21:11:15 -07:00
27ca936264 rtiow: add core affinity to each render thread.
All checks were successful
continuous-integration/drone/push Build is passing
2019-10-21 10:51:33 -07:00
af6cda7349 Print pixel/s and ray/s at end of render.
All checks were successful
continuous-integration/drone/push Build is passing
2019-10-16 21:11:40 -07:00
12da8b2d16 Print out args at start. 2019-10-16 21:11:30 -07:00
5d9e180817 First version of adaptive subsampling.
All checks were successful
continuous-integration/drone/push Build is passing
Add debugging images, and move rendering to output module.
2019-10-16 20:21:16 -07:00
f0f90a6b80 Fix doc tests for human.
All checks were successful
continuous-integration/drone/push Build is passing
2019-10-13 12:19:51 -07:00
fd7b9fd1b8 drone: reorder debug messages.
Some checks failed
continuous-integration/drone/push Build is failing
2019-10-13 10:09:46 -07:00
e8be4d2e0d drone: add rust versions to debugging phase.
Some checks failed
continuous-integration/drone/push Build is failing
2019-10-13 09:56:52 -07:00
690048cbef Add build vs watch scripts, tell drone to build_all_features.
Some checks failed
continuous-integration/drone/push Build is failing
2019-10-13 08:25:47 -07:00
fcc22b24cd Cleanup more lint.
Add script for building all configs with all lint as errors.
2019-10-13 08:23:07 -07:00
90c4e15ad1 Cleaned up lint when building with prom or profile features. 2019-10-13 08:04:49 -07:00
38317de40d Cleanup lint in human. 2019-10-13 07:55:06 -07:00
92c8f1980c Cleanup lint in renderer. 2019-10-13 07:51:16 -07:00
5d5f3c7244 Lint cleanup when prometheus not enabled. 2019-10-12 20:42:41 -07:00
051482e7fe Plumb --adaptive flag for adaptive subsampling. 2019-10-12 20:41:07 -07:00
7b5571344e Add Default implementation for Scene.
Some checks failed
continuous-integration/drone/push Build is failing
This makes it so adding new fields doesn't require changing all the
Scene's at once.
2019-10-12 20:27:57 -07:00
Bill Thiede
d796896f26 Remove unnecessary human_format package.
Some checks failed
continuous-integration/drone/push Build is failing
2019-10-12 19:32:01 -07:00
Bill Thiede
2b1112d39e Print human friendly rays / second.
Some checks failed
continuous-integration/drone/push Build is failing
2019-10-12 17:47:19 -07:00
Bill Thiede
c440c518d2 Remove debug prints. 2019-10-12 17:45:19 -07:00
Bill Thiede
fa168966eb Import from upstream.
https://raw.githubusercontent.com/BobGneu/human-format-rs/master/src/lib.rs
2019-10-12 17:45:05 -07:00
Bill Thiede
7cc4dec3a6 Make prometheus monitoring optional and remove stdout logging.
All checks were successful
continuous-integration/drone/push Build is passing
2019-10-12 17:18:39 -07:00
b51b94e0b6 Better progress output
All checks were successful
continuous-integration/drone/push Build is passing
2019-10-12 16:22:26 -07:00
fa02334c78 Merge branch 'master' of https://git.z.xinu.tv/wathiede/raytracers
Some checks failed
continuous-integration/drone/push Build is failing
2019-10-12 16:16:38 -07:00
ccffa690ba Add pixel counting. 2019-10-12 16:16:26 -07:00
Bill Thiede
8f73d5a25a Release lock while rendering.
All checks were successful
continuous-integration/drone/push Build is passing
2019-10-12 15:46:35 -07:00
7f28a321e3 Use std lib primitives for cross thread messaging.
All checks were successful
continuous-integration/drone/push Build is passing
2019-10-12 15:22:43 -07:00
ddb0bd893d Merge branch 'master' of https://git.z.xinu.tv/wathiede/raytracers 2019-10-12 15:11:33 -07:00
Bill Thiede
235a9d1204 Add flag to set number of threads.
All checks were successful
continuous-integration/drone/push Build is passing
2019-10-12 14:29:36 -07:00
83515c60bf Fix 'dyn' lint on trait objects. 2019-10-12 08:38:19 -07:00
Bill Thiede
1687077f4a Render whole lines at a time. 2019-10-11 11:14:52 -07:00
Bill Thiede
b0dafe4739 Actually disable push metrics when disabled.
All checks were successful
continuous-integration/drone/push Build is passing
2019-10-11 08:05:27 -07:00
Bill Thiede
8b99a1f487 Make render req/response enums.
All checks were successful
continuous-integration/drone/push Build is passing
First step in some optimizations to limit overhead of locking between
threads.
2019-10-11 08:03:32 -07:00
bcf6b6d5d0 Remove unnecessary crates.
All checks were successful
continuous-integration/drone/push Build is passing
2019-10-10 21:37:21 -07:00
9a45ba6d72 Async send the pixels to the workers, and bound queue to 2xthreads.
All checks were successful
continuous-integration/drone/push Build is passing
This should allow the program to start recieving rendered pixels before
all of the x,y's have been sent to the workers.  Reducing the time to
first pixel.
2019-10-10 21:20:48 -07:00
2a0460f2cb Version bump crossbeam-channel.
All checks were successful
continuous-integration/drone/push Build is passing
2019-10-10 21:08:56 -07:00
c929f5c967 Error if profiling disabled and -pprof specifed.
All checks were successful
continuous-integration/drone/push Build is passing
2019-10-10 19:55:25 -07:00
53e0230852 Updated Cargo.lock 2019-10-10 19:40:39 -07:00
148 changed files with 15770 additions and 2931 deletions

View File

@@ -5,30 +5,34 @@ steps:
- name: debug - name: debug
image: registry.z.xinu.tv/drone/omnibus image: registry.z.xinu.tv/drone/omnibus
commands: commands:
- cargo version
- rustc -V
- env | sort - env | sort
- find $PWD - 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 - name: rtiow
image: registry.z.xinu.tv/drone/omnibus image: registry.z.xinu.tv/drone/omnibus
commands: commands:
- sccache -s
- apt-get update && apt-get install -y libgoogle-perftools-dev - 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) - (cd rtiow && cargo test --verbose --all)
- sccache -s
- name: aobench - name: aobench
image: registry.z.xinu.tv/drone/omnibus image: registry.z.xinu.tv/drone/omnibus
commands: commands:
- sccache -s
- (cd aobench && cargo build --verbose --all) - (cd aobench && cargo build --verbose --all)
- (cd aobench && cargo test --verbose --all) - (cd aobench && cargo test --verbose --all)
- sccache -s
- name: bheisler - name: bheisler
image: registry.z.xinu.tv/drone/omnibus image: registry.z.xinu.tv/drone/omnibus
commands: commands:
- sccache -s
- (cd bheisler && cargo build --verbose --all) - (cd bheisler && cargo build --verbose --all)
- (cd bheisler && cargo test --verbose --all) - (cd bheisler && cargo test --verbose --all)
- sccache -s

2
.gitignore vendored
View File

@@ -1,2 +1,4 @@
**/target **/target
**/*.rs.bk **/*.rs.bk
**/zig-out
**/zig-cache

View File

@@ -5,3 +5,5 @@ Raytracers
Random collection of ray tracing experiments. Random collection of ray tracing experiments.
# TODO
Implement http://www.kevinbeason.com/smallpt/

292
aobench/Cargo.lock generated
View File

@@ -1,63 +1,72 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]] [[package]]
name = "ansi_term" name = "ansi_term"
version = "0.11.0" version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ 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]] [[package]]
name = "aobench" name = "aobench"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"log 0.4.3 (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.4 (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.1 (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.10 (registry+https://github.com/rust-lang/crates.io-index)", "structopt 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]
name = "atty" name = "atty"
version = "0.2.10" version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"libc 0.2.42 (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.1 (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.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 = "autocfg"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.0.3" version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "0.1.4" version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.4" version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.97 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", "num-integer 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.40 (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]] [[package]]
name = "clap" name = "clap"
version = "2.32.0" version = "2.33.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"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)",
"strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
"textwrap 0.10.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.5 (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.1 (registry+https://github.com/rust-lang/crates.io-index)", "vec_map 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]
@@ -65,57 +74,65 @@ name = "cloudabi"
version = "0.0.3" version = "0.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ 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]] [[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" version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-segmentation 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
"fuchsia-zircon-sys 0.3.3 (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]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.0.2" version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.42" version = "0.2.97"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.3" version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ 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]] [[package]]
name = "num-integer" name = "num-integer"
version = "0.1.39" version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ 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]] [[package]]
name = "num-traits" 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" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "0.4.9" version = "0.4.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -123,135 +140,154 @@ dependencies = [
[[package]] [[package]]
name = "quote" name = "quote"
version = "0.6.3" version = "0.6.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ 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]] [[package]]
name = "rand" name = "rand"
version = "0.5.4" version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.42 (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.2.1 (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.5 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]
name = "rand_core" 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" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.1.40" version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "redox_termios" name = "redox_termios"
version = "0.1.1" version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ 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]] [[package]]
name = "stderrlog" name = "stderrlog"
version = "0.4.1" version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"chrono 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", "atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.19 (registry+https://github.com/rust-lang/crates.io-index)",
"termcolor 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.14 (registry+https://github.com/rust-lang/crates.io-index)",
"thread_local 0.3.5 (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]] [[package]]
name = "strsim" name = "strsim"
version = "0.7.0" version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "structopt" name = "structopt"
version = "0.2.10" version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"clap 2.32.0 (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.10 (registry+https://github.com/rust-lang/crates.io-index)", "structopt-derive 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]
name = "structopt-derive" name = "structopt-derive"
version = "0.2.10" version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"proc-macro2 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)", "heck 0.3.3 (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)",
"syn 0.14.4 (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]] [[package]]
name = "syn" name = "syn"
version = "0.14.4" version = "0.15.44"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ 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)",
"quote 0.6.3 (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)", "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]
name = "termcolor" name = "termcolor"
version = "0.3.6" version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ 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]] [[package]]
name = "termion" name = "termion"
version = "1.5.1" version = "1.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.97 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", "numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_termios 0.1.1 (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]] [[package]]
name = "textwrap" name = "textwrap"
version = "0.10.0" version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ 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]] [[package]]
name = "thread_local" name = "thread_local"
version = "0.3.5" version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ 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)", "unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]
name = "time" name = "time"
version = "0.1.40" version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.97 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_syscall 0.1.40 (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.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 = "unicode-segmentation"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.1.5" version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
@@ -269,7 +305,7 @@ dependencies = [
[[package]] [[package]]
name = "vec_map" name = "vec_map"
version = "0.8.1" version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
@@ -277,9 +313,14 @@ name = "void"
version = "1.0.2" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.5" version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "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" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "winapi-x86_64-pc-windows-gnu" name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" 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] [metadata]
"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" "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 atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652"
"checksum bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d0c54bb8f454c567f21197eefcdbf5679d0bd99f2ddbe52e84c77061952e6789" "checksum autocfg 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
"checksum cfg-if 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "efe5c877e17a9c717a0bf3613b2709f723202c4e4675cc8f12926ded29bcb17e" "checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
"checksum chrono 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "6962c635d530328acc53ac6a955e83093fedc91c5809dfac1fa60fa470830a37" "checksum cfg-if 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
"checksum clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b957d88f4b6a63b9d70d5f454ac8011819c6efa7727858f458ab71c756ce2d3e" "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 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-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
"checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" "checksum heck 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
"checksum lazy_static 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "fb497c35d362b6a331cfd94956a07fc2c78a4604cdbee844a81170386b996dd3" "checksum lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73"
"checksum libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)" = "b685088df2b950fccadf07a7187c8ef846a959c142338a48f9dc0b94517eb5f1" "checksum libc 0.2.97 (registry+https://github.com/rust-lang/crates.io-index)" = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6"
"checksum log 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "61bd98ae7f7b754bc53dca7d44b604f733c6bba044ea6f41bc8d89272d8161d2" "checksum log 0.4.14 (registry+https://github.com/rust-lang/crates.io-index)" = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
"checksum num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "e83d528d2677f0518c570baf2b7abdcf0cd2d248860b68507bdcb3e91d4c0cea" "checksum num-integer 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)" = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
"checksum num-traits 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "630de1ef5cc79d0cdd78b7e33b81f083cbfe90de0f4b2b2f07f905867c70e9fe" "checksum num-traits 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
"checksum proc-macro2 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)" = "cccdc7557a98fe98453030f077df7f3a042052fae465bb61d2c2c41435cfd9b6" "checksum numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
"checksum quote 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e44651a0dc4cdd99f71c83b561e221f714912d11af1a4dff0631f923d53af035" "checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759"
"checksum rand 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)" = "12397506224b2f93e6664ffc4f664b29be8208e5157d3d90b44f09b5fae470ea" "checksum quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1"
"checksum rand_core 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "edecf0f94da5551fc9b492093e30b041a891657db7940ee221f9d2f66e82eef2" "checksum rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c618c47cd3ebd209790115ab837de41425723956ad3ce2e6a7f09890947cacb9"
"checksum redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "c214e91d3ecf43e9a4e41e578973adeb14b474f2bee858742d127af75a0112b1" "checksum rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" "checksum rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
"checksum stderrlog 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "61dc66b7ae72b65636dbf36326f9638fb3ba27871bb737a62e2c309b87d91b70" "checksum redox_syscall 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc"
"checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550" "checksum redox_termios 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f"
"checksum structopt 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d8e9ad6a11096cbecdcca0cc6aa403fdfdbaeda2fb3323a39c98e6a166a1e45a" "checksum stderrlog 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "32e5ee9b90a5452c570a0b0ac1c99ae9498db7e56e33d74366de7f2a7add7f25"
"checksum structopt-derive 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4cbce8ccdc62166bd594c14396a3242bf94c337a51dbfa9be1076dd74b3db2af" "checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
"checksum syn 0.14.4 (registry+https://github.com/rust-lang/crates.io-index)" = "2beff8ebc3658f07512a413866875adddd20f4fd47b2a4e6c9da65cd281baaea" "checksum structopt 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "16c2cdbf9cc375f15d1b4141bc48aeef444806655cd0e904207edc8d68d86ed7"
"checksum termcolor 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "adc4587ead41bf016f11af03e55a624c06568b5a19db4e90fde573d805074f83" "checksum structopt-derive 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "53010261a84b37689f9ed7d395165029f9cc7abb9f56bbfe86bee2597ed25107"
"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" "checksum syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)" = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5"
"checksum textwrap 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "307686869c93e71f94da64286f9a9524c0f308a9e1c87a583de8e9c9039ad3f6" "checksum termcolor 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
"checksum thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "279ef31c19ededf577bfd12dfae728040a21f635b06a24cd670ff510edd38963" "checksum termion 1.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e"
"checksum time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "d825be0eb33fda1a7e68012d51e9c7f451dc1a69391e7fdc197060bb8c56667b" "checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
"checksum unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526" "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 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 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 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-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 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"

View File

@@ -2,9 +2,10 @@
name = "aobench" name = "aobench"
version = "0.1.0" version = "0.1.0"
authors = ["Bill Thiede <rust@xinu.tv>"] authors = ["Bill Thiede <rust@xinu.tv>"]
edition = "2018"
[dependencies] [dependencies]
log = "0.4" log = "0.4"
rand = "0.5"
stderrlog = "0.4.1" stderrlog = "0.4.1"
structopt = "0.2.10" structopt = "0.2.10"
rand = "0.5"

View File

@@ -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::clone::Clone;
use std::f64; use std::f64;
use std::fmt; use std::fmt;
@@ -14,6 +6,9 @@ use std::io::Write;
use std::ops::Add; use std::ops::Add;
use std::ops::Mul; use std::ops::Mul;
use std::ops::Sub; use std::ops::Sub;
use log::info;
use rand::Rng;
use structopt::StructOpt; use structopt::StructOpt;
fn vdot(v0: &Vector, v1: &Vector) -> f64 { fn vdot(v0: &Vector, v1: &Vector) -> f64 {

2
bheisler/Cargo.lock generated
View File

@@ -1,3 +1,5 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]] [[package]]
name = "adler32" name = "adler32"
version = "1.0.3" version = "1.0.3"

8
git-hooks/README.md Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

34
rtchallenge/Cargo.toml Normal file
View 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
View 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.

View 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);

View 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(())
}

View 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;
}
}
}

View 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(())
}

View 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(())
}

View 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(())
}

View 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(())
}

View 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(())
}

View 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(())
}

View 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(())
}

View 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(())
}

View 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(())
}

View 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(())
}

View 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(())
}

View 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(())
}

View 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
View File

@@ -0,0 +1,2 @@
imports_granularity = "Crate"
format_code_in_doc_comments = true

436
rtchallenge/src/camera.rs Normal file
View 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
View 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);
}
}

View 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
View 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
View 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
View File

@@ -0,0 +1,3 @@
fn main() {
println!("Hello, world!");
}

View 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
View 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 GaussJordan 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
View 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
View 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
View 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);
}
}
}

View 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
View 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
View 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(())
}
}
}

4174
rtiow/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,41 +1,15 @@
[package] [workspace]
authors = ["Bill Thiede <rust@xinu.tv>"] resolver = "2"
edition = "2018"
name = "rtiow"
version = "0.1.0"
[[bench]] members = [
harness = false "noise_explorer",
name = "spheres" "noise_explorer_warp",
"renderer",
[dependencies] "tracer",
actix-web = "0.7.8" "vec3",
askama = "0.7.1" ]
chrono = "*"
cpuprofiler = { version = "0.0.3", optional = true }
crossbeam-channel = "0.2.4"
getopts = "*"
image = "0.19.0"
lazy_static = "1.1.0"
log = "0.4.5"
num_cpus = "1.8.0"
rand = "0.5.5"
rayon = "1.0.2"
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"
[dev-dependencies]
criterion = "0.2"
[profile]
[profile.release] [profile.release]
debug = true debug = true
[features] [profile.dev]
profile = ["cpuprofiler"] opt-level = 3

View File

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

@@ -0,0 +1,4 @@
set -e
export RUSTFLAGS="-D warnings"
cargo build --all
cargo build --all --features=profile

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

View File

@@ -0,0 +1,2 @@
imports_granularity = "Crate"
format_code_in_doc_comments = true

View File

@@ -1,56 +1,26 @@
extern crate actix_web; use std::{fmt, time::SystemTime};
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;
extern crate structopt; use actix_web::{http, middleware, server, App, HttpRequest, HttpResponse, Path, Query, Result};
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 askama::Template; use askama::Template;
use log::info;
use rand::SeedableRng; use rand::SeedableRng;
use rand::XorShiftRng; use rand_xorshift::XorShiftRng;
use serde_derive::Deserialize;
use structopt::StructOpt; use structopt::StructOpt;
use rtiow::noise; use renderer::{
use rtiow::noise::lode::Lode; noise,
use rtiow::noise::perlin::Perlin; noise::{lode::Lode, perlin::Perlin, NoiseType},
use rtiow::noise::NoiseType; texture::{NoiseTexture, Texture},
use rtiow::texture::NoiseTexture; vec3::Vec3,
use rtiow::texture::Texture; };
use rtiow::vec3::Vec3;
#[derive(Debug, StructOpt)] #[derive(StructOpt)]
#[structopt(name = "noise_explorer", about = "CLI for exploring Perlin noise")] #[structopt(name = "noise_explorer", about = "CLI for exploring Perlin noise")]
struct Opt { struct Opt {
/// HTTP listen address /// HTTP listen address
#[structopt(long = "addr", default_value = "0.0.0.0:8889")] #[structopt(long = "addr", default_value = "0.0.0.0:8889")]
pub addr: String, 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)] #[derive(Copy, Clone, Debug, Deserialize, PartialEq)]
@@ -75,11 +45,6 @@ struct OptionalParams {
pixel_scale: f32, pixel_scale: f32,
} }
#[derive(Debug, Deserialize)]
struct NoiseSourceParam {
noise_source: NoiseSource,
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct NoiseParams { struct NoiseParams {
width: u32, width: u32,
@@ -151,13 +116,15 @@ fn render_noise(noise_params: NoiseParams) -> image::GrayImage {
.optional .optional
.unwrap_or(OptionalParams { pixel_scale: 1.0 }); .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]; 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 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 => { 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); info!("{:?}", noise_params);
@@ -364,7 +331,7 @@ fn build_specimens(
} }
fn style(_req: &HttpRequest) -> Result<HttpResponse> { 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[..])) Ok(HttpResponse::Ok().content_type("text/css").body(&bytes[..]))
} }
@@ -428,7 +395,6 @@ fn noise_marble(
np: Path<NoiseParamsMarble>, np: Path<NoiseParamsMarble>,
optional: Option<Query<OptionalParams>>, optional: Option<Query<OptionalParams>>,
) -> Result<HttpResponse> { ) -> Result<HttpResponse> {
info!("optional {:?}", optional);
noise(NoiseParams { noise(NoiseParams {
width: np.width, width: np.width,
height: np.height, height: np.height,
@@ -477,16 +443,9 @@ fn main() -> Result<(), std::io::Error> {
mod tests { mod tests {
use std::str; use std::str;
use actix_web::http::Method; use actix_web::{http::Method, test::TestServer, HttpMessage, Path, Query};
use actix_web::test::TestServer;
use actix_web::HttpMessage;
use actix_web::Path;
use actix_web::Query;
use super::NoiseParamsMarble; use super::{NoiseParamsMarble, NoiseParamsScale, NoiseParamsTurbulence, OptionalParams};
use super::NoiseParamsScale;
use super::NoiseParamsTurbulence;
use super::OptionalParams;
#[test] #[test]
fn noise_param_from_req() { fn noise_param_from_req() {

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

View File

@@ -0,0 +1,2 @@
imports_granularity = "Crate"
format_code_in_doc_comments = true

View File

@@ -0,0 +1,3 @@
fn main() {
println!("Hello, world!");
}

42
rtiow/renderer/Cargo.toml Normal file
View 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"]

View 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);

View 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);

View File

Before

Width:  |  Height:  |  Size: 4.4 MiB

After

Width:  |  Height:  |  Size: 4.4 MiB

View File

Before

Width:  |  Height:  |  Size: 295 KiB

After

Width:  |  Height:  |  Size: 295 KiB

View File

@@ -0,0 +1,2 @@
imports_granularity = "Crate"
format_code_in_doc_comments = true

377
rtiow/renderer/src/aabb.rs Normal file
View 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,
}
}

View File

@@ -1,18 +1,17 @@
use std; use std::{self, fmt, time::Instant};
use std::fmt;
use std::time::Instant;
use rand; use log::info;
use rand::Rng; use rand::{self, Rng};
use crate::aabb::surrounding_box; use crate::{
use crate::aabb::AABB; aabb::{surrounding_box, AABB},
use crate::hitable::Hit; hitable::{Hit, HitRecord},
use crate::hitable::HitRecord; ray::Ray,
use crate::ray::Ray; };
#[derive(Debug)]
enum BVHNode { enum BVHNode {
Leaf(Box<Hit>), Leaf(Box<dyn Hit>),
Branch { Branch {
left: Box<BVHNode>, left: Box<BVHNode>,
right: Box<BVHNode>, right: Box<BVHNode>,
@@ -43,7 +42,7 @@ impl fmt::Display for BVHNode {
// Lint is wrong when dealing with Box<Trait> // Lint is wrong when dealing with Box<Trait>
#[allow(clippy::borrowed_box)] #[allow(clippy::borrowed_box)]
fn box_x_compare(ah: &Box<Hit>, bh: &Box<Hit>) -> std::cmp::Ordering { fn box_x_compare(ah: &Box<dyn Hit>, bh: &Box<dyn Hit>) -> std::cmp::Ordering {
match (ah.bounding_box(0., 0.), bh.bounding_box(0., 0.)) { match (ah.bounding_box(0., 0.), bh.bounding_box(0., 0.)) {
(Some(box_left), Some(box_right)) => { (Some(box_left), Some(box_right)) => {
box_left.min().x.partial_cmp(&box_right.min().x).unwrap() box_left.min().x.partial_cmp(&box_right.min().x).unwrap()
@@ -53,7 +52,7 @@ fn box_x_compare(ah: &Box<Hit>, bh: &Box<Hit>) -> std::cmp::Ordering {
} }
#[allow(clippy::borrowed_box)] #[allow(clippy::borrowed_box)]
fn box_y_compare(ah: &Box<Hit>, bh: &Box<Hit>) -> std::cmp::Ordering { fn box_y_compare(ah: &Box<dyn Hit>, bh: &Box<dyn Hit>) -> std::cmp::Ordering {
match (ah.bounding_box(0., 0.), bh.bounding_box(0., 0.)) { match (ah.bounding_box(0., 0.), bh.bounding_box(0., 0.)) {
(Some(box_left), Some(box_right)) => { (Some(box_left), Some(box_right)) => {
box_left.min().y.partial_cmp(&box_right.min().y).unwrap() box_left.min().y.partial_cmp(&box_right.min().y).unwrap()
@@ -63,7 +62,7 @@ fn box_y_compare(ah: &Box<Hit>, bh: &Box<Hit>) -> std::cmp::Ordering {
} }
#[allow(clippy::borrowed_box)] #[allow(clippy::borrowed_box)]
fn box_z_compare(ah: &Box<Hit>, bh: &Box<Hit>) -> std::cmp::Ordering { fn box_z_compare(ah: &Box<dyn Hit>, bh: &Box<dyn Hit>) -> std::cmp::Ordering {
match (ah.bounding_box(0., 0.), bh.bounding_box(0., 0.)) { match (ah.bounding_box(0., 0.), bh.bounding_box(0., 0.)) {
(Some(box_left), Some(box_right)) => { (Some(box_left), Some(box_right)) => {
box_left.min().z.partial_cmp(&box_right.min().z).unwrap() box_left.min().z.partial_cmp(&box_right.min().z).unwrap()
@@ -77,10 +76,10 @@ fn box_z_compare(ah: &Box<Hit>, bh: &Box<Hit>) -> std::cmp::Ordering {
// returns a reference.) // returns a reference.)
fn vec_first_into<T>(v: Vec<T>) -> T { fn vec_first_into<T>(v: Vec<T>) -> T {
if v.len() != 1 { if v.len() != 1 {
panic!(format!( panic!(
"vec_first_into called for vector length != 1, length {}", "vec_first_into called for vector length != 1, length {}",
v.len() v.len()
)); );
} }
v.into_iter().next().unwrap() v.into_iter().next().unwrap()
} }
@@ -99,13 +98,13 @@ fn vec_split_into<T>(v: Vec<T>, offset: usize) -> (Vec<T>, Vec<T>) {
} }
impl BVHNode { impl BVHNode {
fn new(l: Vec<Box<Hit>>, t_min: f32, t_max: f32) -> BVHNode { fn new(l: Vec<Box<dyn Hit>>, t_min: f32, t_max: f32) -> BVHNode {
if l.len() == 1 { if l.len() == 1 {
BVHNode::Leaf(vec_first_into(l)) BVHNode::Leaf(vec_first_into(l))
} else { } else {
let mut l: Vec<Box<Hit>> = l.into_iter().collect(); let mut l: Vec<Box<dyn Hit>> = l.into_iter().collect();
let mut rng = rand::thread_rng(); 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), 0 => l.sort_by(box_x_compare),
1 => l.sort_by(box_y_compare), 1 => l.sort_by(box_y_compare),
2 => l.sort_by(box_z_compare), 2 => l.sort_by(box_z_compare),
@@ -121,7 +120,7 @@ impl BVHNode {
} }
} }
fn surrounding_box(left: &Hit, right: &Hit, t_min: f32, t_max: f32) -> Option<AABB> { fn surrounding_box(left: &dyn Hit, right: &dyn Hit, t_min: f32, t_max: f32) -> Option<AABB> {
match ( match (
left.bounding_box(t_min, t_max), left.bounding_box(t_min, t_max),
right.bounding_box(t_min, t_max), right.bounding_box(t_min, t_max),
@@ -180,12 +179,13 @@ impl Hit for BVHNode {
} }
} }
#[derive(Debug)]
pub struct BVH { pub struct BVH {
root: BVHNode, root: BVHNode,
} }
impl BVH { impl BVH {
pub fn new(l: Vec<Box<Hit>>, t_min: f32, t_max: f32) -> BVH { pub fn new(l: Vec<Box<dyn Hit>>, t_min: f32, t_max: f32) -> BVH {
let count = l.len(); let count = l.len();
let start = Instant::now(); let start = Instant::now();
let bvh = BVH { let bvh = BVH {
@@ -231,12 +231,13 @@ impl Hit for BVH {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::aabb::AABB; use crate::{
use crate::material::Lambertian; aabb::AABB,
use crate::material::Metal; material::{Lambertian, Metal},
use crate::sphere::Sphere; sphere::Sphere,
use crate::texture::ConstantTexture; texture::ConstantTexture,
use crate::vec3::Vec3; vec3::Vec3,
};
use super::*; use super::*;

View 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)=>(),
}
}
}
}

View File

@@ -1,28 +1,24 @@
extern crate rand;
use std::f32::consts::PI; use std::f32::consts::PI;
use self::rand::Rng; use rand::{self, Rng};
use crate::ray::Ray; use crate::{
use crate::vec3::cross; ray::Ray,
use crate::vec3::Vec3; vec3::{cross, Vec3},
};
fn random_in_unit_disk() -> Vec3 { fn random_in_unit_disk() -> Vec3 {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let v = Vec3::new(1., 1., 0.); let v = Vec3::new(1., 1., 0.);
loop { loop {
let p = let p = 2. * Vec3::new(rng.gen(), rng.gen(), 0.) - v;
2. * Vec3::new(
rng.gen_range::<f32>(0., 1.),
rng.gen_range::<f32>(0., 1.),
0.,
) - v;
if p.squared_length() < 1. { if p.squared_length() < 1. {
return p; return p;
} }
} }
} }
#[derive(Debug)]
pub struct Camera { pub struct Camera {
origin: Vec3, origin: Vec3,
lower_left_corner: Vec3, lower_left_corner: Vec3,
@@ -77,7 +73,7 @@ impl Camera {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let rd = self.lens_radius * random_in_unit_disk(); let rd = self.lens_radius * random_in_unit_disk();
let offset = self.u * rd.x + self.v * rd.y; 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( Ray::new(
self.origin + offset, self.origin + offset,
self.lower_left_corner + self.horizontal * u + self.vertical * v - self.origin - offset, self.lower_left_corner + self.horizontal * u + self.vertical * v - self.origin - offset,

View 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()
}

View File

@@ -1,16 +1,17 @@
use std; use std;
use rand; use rand::{self, Rng};
use rand::Rng;
use crate::aabb::AABB; use crate::{
use crate::hitable::Hit; aabb::AABB,
use crate::hitable::HitRecord; hitable::{Hit, HitRecord},
use crate::material::Isotropic; material::Isotropic,
use crate::ray::Ray; ray::Ray,
use crate::texture::Texture; texture::Texture,
use crate::vec3::Vec3; vec3::Vec3,
};
#[derive(Debug)]
pub struct ConstantMedium<H, T> pub struct ConstantMedium<H, T>
where where
H: Hit, H: Hit,
@@ -60,14 +61,10 @@ where
rec1.t = 0.; rec1.t = 0.;
} }
let distance_inside_boundary = (rec2.t - rec1.t) * r.direction.length(); 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 { if hit_distance < distance_inside_boundary {
let t = rec1.t + hit_distance / r.direction.length(); let t = rec1.t + hit_distance / r.direction.length();
let normal = Vec3::new( let normal = Vec3::new(rng.gen(), rng.gen(), rng.gen()).unit_vector();
rng.gen_range::<f32>(0., 1.),
rng.gen_range::<f32>(0., 1.),
rng.gen_range::<f32>(0., 1.),
).unit_vector();
return Some(HitRecord { return Some(HitRecord {
t, t,
p: r.point_at_parameter(t), p: r.point_at_parameter(t),

View File

@@ -1,17 +1,17 @@
use std::sync::Arc; use std::sync::Arc;
use crate::aabb::AABB; use crate::{
use crate::flip_normals::FlipNormals; aabb::AABB,
use crate::hitable::Hit; flip_normals::FlipNormals,
use crate::hitable::HitRecord; hitable::{Hit, HitRecord},
use crate::hitable_list::HitableList; hitable_list::HitableList,
use crate::material::Material; material::Material,
use crate::ray::Ray; ray::Ray,
use crate::rect::XYRect; rect::{XYRect, XZRect, YZRect},
use crate::rect::XZRect; vec3::Vec3,
use crate::rect::YZRect; };
use crate::vec3::Vec3;
#[derive(Debug)]
pub struct Cuboid { pub struct Cuboid {
p_min: Vec3, p_min: Vec3,
p_max: Vec3, p_max: Vec3,
@@ -21,7 +21,10 @@ pub struct Cuboid {
impl Cuboid { impl Cuboid {
// This clippy doesn't work right with Arc. // This clippy doesn't work right with Arc.
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]
pub fn new(p_min: Vec3, p_max: Vec3, material: Arc<Material>) -> Cuboid { 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 { Cuboid {
p_min, p_min,
p_max, p_max,

View 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)
}
}

View File

@@ -1,8 +1,10 @@
use crate::aabb::AABB; use crate::{
use crate::hitable::Hit; aabb::AABB,
use crate::hitable::HitRecord; hitable::{Hit, HitRecord},
use crate::ray::Ray; ray::Ray,
};
#[derive(Debug)]
pub struct FlipNormals<H> pub struct FlipNormals<H>
where where
H: Hit, H: Hit,

View 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))
}
}

View File

@@ -1,24 +1,31 @@
use std::sync::Arc; use std::{fmt::Debug, sync::Arc};
use crate::aabb::AABB; use crate::{aabb::AABB, material::Material, ray::Ray, vec3::Vec3};
use crate::material::Material;
use crate::ray::Ray;
use crate::vec3::Vec3;
#[derive(Debug)]
pub struct HitRecord<'m> { pub struct HitRecord<'m> {
pub t: f32, pub t: f32,
pub uv: (f32, f32), pub uv: (f32, f32),
pub p: Vec3, pub p: Vec3,
pub normal: Vec3, pub normal: Vec3,
pub material: &'m Material, 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 hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option<HitRecord>;
fn bounding_box(&self, t_min: f32, t_max: f32) -> Option<AABB>; fn bounding_box(&self, t_min: f32, t_max: f32) -> Option<AABB>;
} }
impl Hit for Arc<Hit> { impl Hit for Arc<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)
}
}
impl Hit for Box<dyn Hit> {
fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option<HitRecord> { fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option<HitRecord> {
(**self).hit(r, t_min, t_max) (**self).hit(r, t_min, t_max)
} }

View File

@@ -1,19 +1,19 @@
use std; use std;
use crate::aabb::surrounding_box; use crate::{
use crate::aabb::AABB; aabb::{surrounding_box, AABB},
use crate::hitable::Hit; hitable::{Hit, HitRecord},
use crate::hitable::HitRecord; ray::Ray,
use crate::ray::Ray; vec3::Vec3,
use crate::vec3::Vec3; };
#[derive(Default)] #[derive(Debug, Default)]
pub struct HitableList { pub struct HitableList {
list: Vec<Box<Hit>>, list: Vec<Box<dyn Hit>>,
} }
impl HitableList { impl HitableList {
pub fn new(list: Vec<Box<Hit>>) -> HitableList { pub fn new(list: Vec<Box<dyn Hit>>) -> HitableList {
HitableList { list } HitableList { list }
} }
} }

235
rtiow/renderer/src/human.rs Normal file
View File

@@ -0,0 +1,235 @@
#![allow(dead_code)]
//! From https://raw.githubusercontent.com/BobGneu/human-format-rs/master/src/lib.rs
//! `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.
//!
//! Print some human readable strings
//!
//! ```rust
//! use renderer::human;
//!
//! // "1.00 k"
//! let tmpStr = human::Formatter::new().format(1000.0);
//! # assert_eq!(tmpStr, "1.00 k");
//!
//! // "1.00 M"
//! let tmpStr2 = human::Formatter::new().format(1000000.0);
//! # assert_eq!(tmpStr2, "1.00 M");
//!
//! // "1.00 B"
//! 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 {
value: f32,
suffix: String,
}
/// Entry point to the lib. Use this to handle your formatting needs.
#[derive(Debug)]
pub struct Formatter {
decimals: usize,
separator: String,
scales: Scales,
forced_units: String,
forced_suffix: String,
}
/// Provide a customized scaling scheme for your own modeling.
#[derive(Debug)]
pub struct Scales {
base: u32,
suffixes: Vec<String>,
}
impl Formatter {
/// Initializes a new `Formatter` with default values.
pub fn new() -> Self {
Formatter {
decimals: 2,
separator: " ".to_owned(),
scales: Scales::si(),
forced_units: "".to_owned(),
forced_suffix: "".to_owned(),
}
}
/// Sets the decimals value for formatting the string.
pub fn with_decimals(&mut self, decimals: usize) -> &mut Self {
self.decimals = decimals;
self
}
/// Sets the separator value for formatting the string.
pub fn with_separator(&mut self, separator: &str) -> &mut Self {
self.separator = separator.to_owned();
self
}
/// Sets the scales value.
pub fn with_scales(&mut self, scales: Scales) -> &mut Self {
self.scales = scales;
self
}
/// Sets the units value.
pub fn with_units(&mut self, units: &str) -> &mut Self {
self.forced_units = units.to_owned();
self
}
/// Sets the expected suffix value.
pub fn with_suffix(&mut self, suffix: &str) -> &mut Self {
self.forced_suffix = suffix.to_owned();
self
}
/// Formats the number into a string
pub fn format(&self, value: f64) -> String {
if value < 0.0 {
return format!("-{}", self.format(value * -1.0));
}
let scaled_value = self.scales.to_scaled_value(value);
format!(
"{:.width$}{}{}{}",
scaled_value.value,
self.separator,
scaled_value.suffix,
self.forced_units,
width = self.decimals
)
}
/// Parse a string back into a float value.
pub fn parse(&self, value: &str) -> f64 {
let v: Vec<&str> = value.split(&self.separator).collect();
let result = v.get(0).unwrap().parse::<f64>().unwrap();
let mut suffix = v.get(1).unwrap().to_string();
let new_len = suffix.len() - self.forced_units.len();
suffix.truncate(new_len);
let magnitude_multiplier = self.scales.get_magnitude_multipler(&suffix);
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()
}
/// Instantiates a new `Scales` with SI keys
pub fn si() -> Self {
Scales {
base: 1000,
suffixes: vec![
"".to_owned(),
"k".to_owned(),
"M".to_owned(),
"B".to_owned(),
"T".to_owned(),
"P".to_owned(),
"E".to_owned(),
"Z".to_owned(),
"Y".to_owned(),
],
}
}
/// Instantiates a new `Scales` with Binary keys
pub fn binary() -> Self {
Scales {
base: 1000,
suffixes: vec![
"".to_owned(),
"ki".to_owned(),
"Mi".to_owned(),
"Gi".to_owned(),
"Ti".to_owned(),
"Pi".to_owned(),
"Ei".to_owned(),
"Zi".to_owned(),
"Yi".to_owned(),
],
}
}
/// Sets the base for the `Scales`
pub fn with_base(&mut self, base: u32) -> &mut Self {
self.base = base;
self
}
/// Sets the suffixes listing appropriately
pub fn with_suffixes(&mut self, suffixes: Vec<&str>) -> &mut Self {
self.suffixes = Vec::new();
for suffix in suffixes {
// This should be to_owned to be clear about intent.
// https://users.rust-lang.org/t/to-string-vs-to-owned-for-string-literals/1441/6
self.suffixes.push(suffix.to_owned());
}
self
}
fn get_magnitude_multipler(&self, value: &str) -> f64 {
for ndx in 0..self.suffixes.len() {
if value == self.suffixes[ndx] {
return self.base.pow(ndx as u32) as f64;
}
}
0.0
}
fn to_scaled_value(&self, value: f64) -> ScaledValue {
let mut index: usize = 0;
let mut _value: f64 = value;
loop {
if _value < (self.base as f64) {
break;
}
_value /= self.base as f64;
index += 1;
}
ScaledValue {
value: (value / self.base.pow((index) as u32) as f64) as f32,
suffix: self.suffixes[index].to_owned(),
}
}
}
impl Default for Scales {
fn default() -> Self {
Self::new()
}
}

View File

@@ -1,13 +1,17 @@
use std::fmt; use std::fmt;
use crate::aabb::surrounding_box; use log::info;
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},
hitable_list::HitableList,
ray::Ray,
};
#[derive(Debug)]
pub enum KDTree { pub enum KDTree {
Leaf(Box<Hit>), Leaf(Box<dyn Hit>),
Branch { Branch {
left: Box<KDTree>, left: Box<KDTree>,
right: Box<KDTree>, right: Box<KDTree>,
@@ -20,10 +24,10 @@ pub enum KDTree {
// returns a reference.) // returns a reference.)
fn vec_first_into<T>(v: Vec<T>) -> T { fn vec_first_into<T>(v: Vec<T>) -> T {
if v.len() != 1 { if v.len() != 1 {
panic!(format!( panic!(
"vec_first_into called for vector length != 1, length {}", "vec_first_into called for vector length != 1, length {}",
v.len() v.len()
)); );
} }
v.into_iter().next().unwrap() v.into_iter().next().unwrap()
} }
@@ -42,7 +46,7 @@ fn vec_split_into<T>(v: Vec<T>, offset: usize) -> (Vec<T>, Vec<T>) {
} }
impl KDTree { impl KDTree {
pub fn new(l: Vec<Box<Hit>>, t_min: f32, t_max: f32) -> KDTree { pub fn new(l: Vec<Box<dyn Hit>>, t_min: f32, t_max: f32) -> KDTree {
if l.is_empty() { if l.is_empty() {
panic!("Attempt to build k-d tree with no Hit objects"); panic!("Attempt to build k-d tree with no Hit objects");
} }
@@ -100,6 +104,14 @@ impl KDTree {
}), }),
_ => panic!("Unreachable"), _ => 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 { KDTree::Branch {
left: Box::new(KDTree::new(left_half, t_min, t_max)), left: Box::new(KDTree::new(left_half, t_min, t_max)),
right: Box::new(KDTree::new(right_half, t_min, t_max)), right: Box::new(KDTree::new(right_half, t_min, t_max)),
@@ -144,15 +156,17 @@ fn print_tree(f: &mut fmt::Formatter, depth: usize, kdt: &KDTree) -> fmt::Result
impl fmt::Display for KDTree { impl fmt::Display for KDTree {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "kd-tree")?; writeln!(f, "kd-tree")?;
print_tree(f, 1, &self) print_tree(f, 1, self)
} }
} }
impl Hit for KDTree { impl Hit for KDTree {
fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option<HitRecord> { fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option<HitRecord> {
match self.bounding_box(t_min, t_max) { match self.bounding_box(t_min, t_max) {
Some(bbox) => if !bbox.hit(r, t_min, t_max) { Some(bbox) => {
return None; if !bbox.hit(r, t_min, t_max) {
}, return None;
}
}
None => { None => {
info!("KDTree::hit no bbox: {:?}", r); info!("KDTree::hit no bbox: {:?}", r);
return None; return None;
@@ -163,11 +177,13 @@ impl Hit for KDTree {
KDTree::Branch { left, right, bbox } => match bbox { KDTree::Branch { left, right, bbox } => match bbox {
None => None, None => None,
Some(_bbox) => match (left.hit(r, t_min, t_max), right.hit(r, t_min, t_max)) { Some(_bbox) => match (left.hit(r, t_min, t_max), right.hit(r, t_min, t_max)) {
(Some(hit_left), Some(hit_right)) => if hit_left.t < hit_right.t { (Some(hit_left), Some(hit_right)) => {
Some(hit_left) if hit_left.t < hit_right.t {
} else { Some(hit_left)
Some(hit_right) } else {
}, Some(hit_right)
}
}
(Some(hit_left), None) => Some(hit_left), (Some(hit_left), None) => Some(hit_left),
(None, Some(hit_right)) => Some(hit_right), (None, Some(hit_right)) => Some(hit_right),
(None, None) => None, (None, None) => None,

View File

@@ -1,37 +1,30 @@
pub mod aabb; pub mod aabb;
pub mod bvh; pub mod bvh;
pub mod bvh_triangles;
pub mod camera; pub mod camera;
pub mod colors;
pub mod constant_medium; pub mod constant_medium;
pub mod cuboid; pub mod cuboid;
pub mod debug_hit;
pub mod flip_normals; pub mod flip_normals;
pub mod glowybox;
pub mod hitable; pub mod hitable;
pub mod hitable_list; pub mod hitable_list;
pub mod human;
pub mod kdtree; pub mod kdtree;
pub mod material; pub mod material;
pub mod moving_sphere; pub mod moving_sphere;
pub mod noise; pub mod noise;
pub mod output;
pub mod parser;
pub mod ray; pub mod ray;
pub mod rect; pub mod rect;
pub mod renderer; pub mod renderer;
pub mod rotate; pub mod rotate;
pub mod scale;
pub mod scenes; pub mod scenes;
pub mod sphere; pub mod sphere;
pub mod texture; pub mod texture;
pub mod translate; pub mod translate;
pub mod triangles;
pub mod vec3; pub mod vec3;
extern crate crossbeam_channel;
extern crate image;
#[macro_use]
extern crate log;
extern crate num_cpus;
extern crate rand;
extern crate rayon;
#[macro_use]
extern crate structopt;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate prometheus;

View File

@@ -1,44 +1,40 @@
use std::sync::Arc; use std::{fmt::Debug, sync::Arc};
extern crate rand; use rand::{self, Rng};
use self::rand::Rng;
use crate::hitable::HitRecord; use crate::{
use crate::ray::Ray; hitable::HitRecord,
use crate::texture::Texture; ray::Ray,
use crate::vec3::dot; texture::Texture,
use crate::vec3::Vec3; vec3::{dot, Vec3},
};
fn random_in_unit_sphere() -> Vec3 { fn random_in_unit_sphere() -> Vec3 {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let v = Vec3::new(1., 1., 1.); let v = Vec3::new(1., 1., 1.);
loop { loop {
let p = 2. * Vec3::new( let p = 2. * Vec3::new(rng.gen(), rng.gen(), rng.gen()) - v;
rng.gen_range::<f32>(0., 1.),
rng.gen_range::<f32>(0., 1.),
rng.gen_range::<f32>(0., 1.),
) - v;
if p.squared_length() < 1. { if p.squared_length() < 1. {
return p; return p;
} }
} }
} }
#[derive(Default)] #[derive(Default, Debug)]
pub struct ScatterResponse { pub struct ScatterResponse {
pub scattered: Ray, pub scattered: Ray,
pub attenutation: Vec3, pub attenutation: Vec3,
pub reflected: bool, pub reflected: bool,
} }
pub trait Material: Send + Sync { pub trait Material: Send + Sync + Debug {
fn scatter(&self, r_in: &Ray, rec: &HitRecord) -> ScatterResponse; fn scatter(&self, r_in: &Ray, rec: &HitRecord) -> ScatterResponse;
fn emitted(&self, _u: f32, _v: f32, _p: Vec3) -> Vec3 { fn emitted(&self, _u: f32, _v: f32, _p: Vec3) -> Vec3 {
Vec3::new(0., 0., 0.) Vec3::new(0., 0., 0.)
} }
} }
impl Material for Arc<Material> { impl Material for Arc<dyn Material> {
fn scatter(&self, r_in: &Ray, rec: &HitRecord) -> ScatterResponse { fn scatter(&self, r_in: &Ray, rec: &HitRecord) -> ScatterResponse {
(**self).scatter(r_in, rec) (**self).scatter(r_in, rec)
} }
@@ -47,7 +43,7 @@ impl Material for Arc<Material> {
} }
} }
impl Material for Box<Material> { impl Material for Box<dyn Material> {
fn scatter(&self, r_in: &Ray, rec: &HitRecord) -> ScatterResponse { fn scatter(&self, r_in: &Ray, rec: &HitRecord) -> ScatterResponse {
(**self).scatter(r_in, rec) (**self).scatter(r_in, rec)
} }
@@ -56,6 +52,7 @@ impl Material for Box<Material> {
} }
} }
#[derive(Clone, Debug)]
pub struct Isotropic<T> pub struct Isotropic<T>
where where
T: Texture, T: Texture,
@@ -86,6 +83,7 @@ where
} }
} }
#[derive(Clone, Debug)]
pub struct Lambertian<T> pub struct Lambertian<T>
where where
T: Texture, T: Texture,
@@ -117,6 +115,7 @@ where
} }
} }
#[derive(Clone, Debug)]
pub struct Metal { pub struct Metal {
albedo: Vec3, albedo: Vec3,
fuzzy: f32, fuzzy: f32,
@@ -171,6 +170,7 @@ fn schlick(cosine: f32, ref_idx: f32) -> f32 {
r0 + (1. - r0) * (1. - cosine).powf(5.) r0 + (1. - r0) * (1. - cosine).powf(5.)
} }
#[derive(Clone, Debug)]
pub struct Dielectric { pub struct Dielectric {
ref_idx: f32, ref_idx: f32,
} }
@@ -200,7 +200,7 @@ impl Material for Dielectric {
let scattered = if let Some(refracted) = refract(r_in.direction, outward_normal, ni_over_nt) let scattered = if let Some(refracted) = refract(r_in.direction, outward_normal, ni_over_nt)
{ {
let mut rng = rand::thread_rng(); 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) Ray::new(rec.p, reflected, r_in.time)
} else { } else {
Ray::new(rec.p, refracted, r_in.time) Ray::new(rec.p, refracted, r_in.time)
@@ -217,6 +217,7 @@ impl Material for Dielectric {
} }
} }
#[derive(Debug)]
pub struct DiffuseLight<T> pub struct DiffuseLight<T>
where where
T: Texture, T: Texture,
@@ -250,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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -257,7 +272,7 @@ mod tests {
#[test] #[test]
fn arc_material() { fn arc_material() {
let white: Arc<Material> = let white: Arc<dyn Material> =
Arc::new(Lambertian::new(ConstantTexture::new([0.73, 0.73, 0.73]))); Arc::new(Lambertian::new(ConstantTexture::new([0.73, 0.73, 0.73])));
fn material_fn<M>(m: M) fn material_fn<M>(m: M)

View File

@@ -1,13 +1,13 @@
use crate::aabb::surrounding_box; use crate::{
use crate::aabb::AABB; aabb::{surrounding_box, AABB},
use crate::hitable::Hit; hitable::{Hit, HitRecord},
use crate::hitable::HitRecord; material::Material,
use crate::material::Material; ray::Ray,
use crate::ray::Ray; sphere::get_sphere_uv,
use crate::sphere::get_sphere_uv; vec3::{dot, Vec3},
use crate::vec3::dot; };
use crate::vec3::Vec3;
#[derive(Debug)]
pub struct MovingSphere<M> pub struct MovingSphere<M>
where where
M: Material, M: Material,

View File

@@ -1,10 +1,11 @@
use log::trace;
/// Implements the concepts from https://lodev.org/cgtutor/randomnoise.html /// Implements the concepts from https://lodev.org/cgtutor/randomnoise.html
use rand; use rand;
use crate::noise::NoiseSource; use crate::{noise::NoiseSource, vec3::Vec3};
use crate::vec3::Vec3;
const NOISE_SIZE: usize = 128; const NOISE_SIZE: usize = 128;
#[derive(Debug)]
pub struct Lode { pub struct Lode {
// Using fixed array causes stack overflow. // Using fixed array causes stack overflow.
noise: Vec<Vec<Vec<f32>>>, //[[[f32; NOISE_SIZE]; NOISE_SIZE]; NOISE_SIZE], 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 x in 0..NOISE_SIZE {
for y in 0..NOISE_SIZE { for y in 0..NOISE_SIZE {
for z 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();
} }
} }
} }

View File

@@ -1,11 +1,13 @@
pub mod lode; pub mod lode;
pub mod perlin; pub mod perlin;
use std::f32::consts::PI; use std::{f32::consts::PI, fmt::Debug};
use serde_derive::Deserialize;
use crate::vec3::Vec3; use crate::vec3::Vec3;
pub trait NoiseSource: Send + Sync { pub trait NoiseSource: Send + Sync + Debug {
/// value returns noise on the interval [0., 1.). /// value returns noise on the interval [0., 1.).
fn value(&self, p: Vec3) -> f32; fn value(&self, p: Vec3) -> f32;
@@ -38,7 +40,7 @@ pub trait NoiseSource: Send + Sync {
} }
} }
impl NoiseSource for Box<NoiseSource> { impl NoiseSource for Box<dyn NoiseSource> {
fn value(&self, p: Vec3) -> f32 { fn value(&self, p: Vec3) -> f32 {
(**self).value(p) (**self).value(p)
} }

View File

@@ -1,11 +1,14 @@
// There are many math functions in this file, so we allow single letter variable names. // There are many math functions in this file, so we allow single letter variable names.
#![allow(clippy::many_single_char_names)] #![allow(clippy::many_single_char_names)]
use rand::Rng; use log::trace;
use rand::{seq::SliceRandom, Rng};
use crate::noise::NoiseSource; use crate::{
use crate::vec3::dot; noise::NoiseSource,
use crate::vec3::Vec3; vec3::{dot, Vec3},
};
#[derive(Debug)]
pub struct Perlin { pub struct Perlin {
ran_vec: Vec<Vec3>, ran_vec: Vec<Vec3>,
perm_x: Vec<usize>, perm_x: Vec<usize>,
@@ -20,9 +23,9 @@ where
(0..256) (0..256)
.map(|_| { .map(|_| {
Vec3::new( Vec3::new(
rng.gen_range::<f32>(-1., 1.), rng.gen_range(-1. ..1.),
rng.gen_range::<f32>(-1., 1.), rng.gen_range(-1. ..1.),
rng.gen_range::<f32>(-1., 1.), rng.gen_range(-1. ..1.),
) )
.unit_vector() .unit_vector()
}) })
@@ -33,8 +36,8 @@ fn perlin_generate_perm<R>(rng: &mut R) -> Vec<usize>
where where
R: Rng, R: Rng,
{ {
let mut p: Vec<usize> = (0..256).map(|i| i).collect(); let mut p: Vec<usize> = (0..256).collect();
rng.shuffle(&mut p); p.shuffle(rng);
p p
} }

View 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;
}

View 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,
}

View File

@@ -5,6 +5,9 @@ pub struct Ray {
pub origin: Vec3, pub origin: Vec3,
pub direction: Vec3, pub direction: Vec3,
pub time: f32, 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 inv_direction: Vec3,
pub sign: [usize; 3], pub sign: [usize; 3],
} }
@@ -14,7 +17,7 @@ impl Ray {
where where
V: Into<Vec3>, V: Into<Vec3>,
{ {
let direction = direction.into(); let direction: Vec3 = direction.into();
let origin = origin.into(); let origin = origin.into();
let inv = 1. / direction; let inv = 1. / direction;
Ray { Ray {

View File

@@ -1,12 +1,14 @@
// There are many math functions in this file, so we allow single letter variable names. // There are many math functions in this file, so we allow single letter variable names.
#![allow(clippy::many_single_char_names)] #![allow(clippy::many_single_char_names)]
use crate::aabb::AABB; use crate::{
use crate::hitable::Hit; aabb::AABB,
use crate::hitable::HitRecord; hitable::{Hit, HitRecord},
use crate::material::Material; material::Material,
use crate::ray::Ray; ray::Ray,
use crate::vec3::Vec3; vec3::Vec3,
};
#[derive(Debug)]
pub struct XYRect<M> pub struct XYRect<M>
where where
M: Material, M: Material,
@@ -68,6 +70,7 @@ where
} }
} }
#[derive(Debug)]
pub struct XZRect<M> pub struct XZRect<M>
where where
M: Material, M: Material,
@@ -129,6 +132,7 @@ where
} }
} }
#[derive(Debug)]
pub struct YZRect<M> pub struct YZRect<M>
where where
M: Material, M: Material,

View 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,
&current_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(),
&current_stat,
time_diff,
pixel_total
)
);
output.write_images(&scene, time_diff, output_dir)
}

View File

@@ -1,13 +1,13 @@
use std::f32::consts::PI; use std::f32::{consts::PI, MAX, MIN};
use std::f32::MAX;
use std::f32::MIN;
use crate::aabb::AABB; use crate::{
use crate::hitable::Hit; aabb::AABB,
use crate::hitable::HitRecord; hitable::{Hit, HitRecord},
use crate::ray::Ray; ray::Ray,
use crate::vec3::Vec3; vec3::Vec3,
};
#[derive(Debug)]
pub struct RotateY<H> pub struct RotateY<H>
where where
H: Hit, H: Hit,

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

View File

@@ -1,17 +1,18 @@
use rand; use log::trace;
use rand::Rng; use rand::{self, Rng};
use crate::bvh::BVH; use crate::{
use crate::camera::Camera; bvh::BVH,
use crate::hitable::Hit; camera::Camera,
use crate::hitable_list::HitableList; hitable::Hit,
use crate::kdtree::KDTree; hitable_list::HitableList,
use crate::material::Lambertian; kdtree::KDTree,
use crate::renderer::Opt; material::Lambertian,
use crate::renderer::Scene; renderer::{Opt, Scene},
use crate::sphere::Sphere; sphere::Sphere,
use crate::texture::ConstantTexture; texture::ConstantTexture,
use crate::vec3::Vec3; vec3::Vec3,
};
pub fn new(opt: &Opt) -> Scene { pub fn new(opt: &Opt) -> Scene {
let lookfrom = Vec3::new(20., 20., 20.); let lookfrom = Vec3::new(20., 20., 20.);
@@ -32,16 +33,16 @@ pub fn new(opt: &Opt) -> Scene {
time_max, time_max,
); );
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let mut grid: Vec<Box<Hit>> = Vec::new(); let mut grid: Vec<Box<dyn Hit>> = Vec::new();
let len = 1000; let len = 1000;
for x in 0..len { for x in 0..len {
for z in 0..len { for z in 0..len {
let r = rng.gen_range::<f32>(0., 1.); let r = rng.gen();
let g = rng.gen_range::<f32>(0., 1.); let g = rng.gen();
let b = rng.gen_range::<f32>(0., 1.); let b = rng.gen();
let x_pos = (x - 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); let z_pos = (z - len / 2) as f32 + rng.gen_range(-0.1..0.1);
grid.push(Box::new(Sphere::new( grid.push(Box::new(Sphere::new(
Vec3::new(x_pos, 0., z_pos), Vec3::new(x_pos, 0., z_pos),
@@ -50,7 +51,7 @@ pub fn new(opt: &Opt) -> Scene {
))); )));
} }
} }
let world: Box<Hit>; let world: Box<dyn Hit>;
if opt.use_accel { if opt.use_accel {
let use_kd = true; let use_kd = true;
if use_kd { if use_kd {
@@ -69,9 +70,9 @@ pub fn new(opt: &Opt) -> Scene {
camera, camera,
world, world,
subsamples: opt.subsamples, subsamples: opt.subsamples,
num_threads: opt.num_threads,
width: opt.width, width: opt.width,
height: opt.height, height: opt.height,
global_illumination: true, ..Default::default()
env_map: None,
} }
} }

View File

@@ -1,21 +1,16 @@
use rand; use rand::{self, Rng};
use rand::Rng;
use crate::camera::Camera; use crate::{
use crate::hitable::Hit; camera::Camera,
use crate::hitable_list::HitableList; hitable::Hit,
use crate::kdtree::KDTree; hitable_list::HitableList,
use crate::material::Dielectric; kdtree::KDTree,
use crate::material::Lambertian; material::{Dielectric, Lambertian, Material, Metal},
use crate::material::Material; renderer::{Opt, Scene},
use crate::material::Metal; sphere::Sphere,
use crate::renderer::Opt; texture::{CheckerTexture, ConstantTexture, EnvMap},
use crate::renderer::Scene; vec3::Vec3,
use crate::sphere::Sphere; };
use crate::texture::CheckerTexture;
use crate::texture::ConstantTexture;
use crate::texture::EnvMap;
use crate::vec3::Vec3;
pub fn new(opt: &Opt) -> Scene { pub fn new(opt: &Opt) -> Scene {
let lookfrom = Vec3::new(13., 2., 3.); let lookfrom = Vec3::new(13., 2., 3.);
@@ -40,7 +35,7 @@ pub fn new(opt: &Opt) -> Scene {
} else { } else {
[0.4, 1.0, 0.4] [0.4, 1.0, 0.4]
}; };
let world: Box<Hit> = if opt.use_accel { let world: Box<dyn Hit> = if opt.use_accel {
Box::new(KDTree::new(random_scene(ground_color), time_min, time_max)) Box::new(KDTree::new(random_scene(ground_color), time_min, time_max))
} else { } else {
Box::new(HitableList::new(random_scene(ground_color))) Box::new(HitableList::new(random_scene(ground_color)))
@@ -51,20 +46,23 @@ pub fn new(opt: &Opt) -> Scene {
camera, camera,
world, world,
subsamples: opt.subsamples, subsamples: opt.subsamples,
adaptive_subsampling: Some(0.5),
num_threads: opt.num_threads,
width: opt.width, width: opt.width,
height: opt.height, height: opt.height,
global_illumination: true, global_illumination: true,
env_map: Some(EnvMap::new(skybox)), env_map: Some(EnvMap::new(skybox)),
..Default::default()
} }
} }
fn random_scene<V>(ground_color: V) -> Vec<Box<Hit>> fn random_scene<V>(ground_color: V) -> Vec<Box<dyn Hit>>
where where
V: Into<Vec3>, V: Into<Vec3>,
{ {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let checker = true; let checker = true;
let ground_material: Box<Material> = if checker { let ground_material: Box<dyn Material> = if checker {
Box::new(Lambertian::new(CheckerTexture::new( Box::new(Lambertian::new(CheckerTexture::new(
ConstantTexture::new([0., 0., 0.]), ConstantTexture::new([0., 0., 0.]),
ConstantTexture::new(ground_color), ConstantTexture::new(ground_color),
@@ -73,19 +71,19 @@ where
Box::new(Lambertian::new(ConstantTexture::new(ground_color))) Box::new(Lambertian::new(ConstantTexture::new(ground_color)))
}; };
let mut objects: Vec<Box<Hit>> = vec![Box::new(Sphere::new( let mut objects: Vec<Box<dyn Hit>> = vec![Box::new(Sphere::new(
[0., -1000., 0.], [0., -1000., 0.],
1000., 1000.,
ground_material, ground_material,
))]; ))];
let mut random = || rng.gen_range::<f32>(0., 1.); let mut random = || rng.gen::<f32>();
for a in -11..11 { for a in -11..11 {
for b in -11..11 { for b in -11..11 {
let choose_mat = random(); let choose_mat = random();
let center = Vec3::new(a as f32 + 0.9 * random(), 0.2, b as f32 + 0.9 * random()); let center = Vec3::new(a as f32 + 0.9 * random(), 0.2, b as f32 + 0.9 * random());
if (center - Vec3::new(4., 0.2, 0.)).length() > 0.9 { if (center - Vec3::new(4., 0.2, 0.)).length() > 0.9 {
let sphere: Box<Hit> = if choose_mat < 0.8 { let sphere: Box<dyn Hit> = if choose_mat < 0.8 {
// diffuse // diffuse
Box::new(Sphere::new( Box::new(Sphere::new(
center, center,
@@ -119,7 +117,7 @@ where
} }
} }
let more: Vec<Box<Hit>> = vec![ let more: Vec<Box<dyn Hit>> = vec![
Box::new(Sphere::new( Box::new(Sphere::new(
Vec3::new(0., 1., 0.), Vec3::new(0., 1., 0.),
1.0, 1.0,

View File

@@ -1,14 +1,16 @@
use crate::bvh::BVH; use log::trace;
use crate::camera::Camera;
use crate::hitable::Hit; use crate::{
use crate::material::Lambertian; bvh::BVH,
use crate::material::Metal; camera::Camera,
use crate::moving_sphere::MovingSphere; hitable::Hit,
use crate::renderer::Opt; material::{Lambertian, Metal},
use crate::renderer::Scene; moving_sphere::MovingSphere,
use crate::sphere::Sphere; renderer::{Opt, Scene},
use crate::texture::ConstantTexture; sphere::Sphere,
use crate::vec3::Vec3; texture::ConstantTexture,
vec3::Vec3,
};
pub fn new(opt: &Opt) -> Scene { pub fn new(opt: &Opt) -> Scene {
let lookfrom = Vec3::new(3., 3., 2.); let lookfrom = Vec3::new(3., 3., 2.);
@@ -58,14 +60,14 @@ pub fn new(opt: &Opt) -> Scene {
time_max, time_max,
); );
trace!(target: "bvh", "World {}", b); trace!(target: "bvh", "World {}", b);
let world: Box<Hit> = Box::new(b); let world: Box<dyn Hit> = Box::new(b);
Scene { Scene {
camera, camera,
world, world,
subsamples: opt.subsamples, subsamples: opt.subsamples,
num_threads: opt.num_threads,
width: opt.width, width: opt.width,
height: opt.height, height: opt.height,
global_illumination: true, ..Default::default()
env_map: None,
} }
} }

View File

@@ -1,22 +1,20 @@
use std::sync::Arc; use std::sync::Arc;
use crate::camera::Camera; use crate::{
use crate::cuboid::Cuboid; camera::Camera,
use crate::flip_normals::FlipNormals; cuboid::Cuboid,
use crate::hitable::Hit; flip_normals::FlipNormals,
use crate::hitable_list::HitableList; hitable::Hit,
use crate::kdtree::KDTree; hitable_list::HitableList,
use crate::material::DiffuseLight; kdtree::KDTree,
use crate::material::Lambertian; material::{DiffuseLight, Lambertian},
use crate::rect::XYRect; rect::{XYRect, XZRect, YZRect},
use crate::rect::XZRect; renderer::{Opt, Scene},
use crate::rect::YZRect; rotate::RotateY,
use crate::renderer::Opt; texture::ConstantTexture,
use crate::renderer::Scene; translate::Translate,
use crate::rotate::RotateY; vec3::Vec3,
use crate::texture::ConstantTexture; };
use crate::translate::Translate;
use crate::vec3::Vec3;
pub fn new(opt: &Opt) -> Scene { pub fn new(opt: &Opt) -> Scene {
let lookfrom = Vec3::new(278., 278., -800.); let lookfrom = Vec3::new(278., 278., -800.);
@@ -37,7 +35,7 @@ pub fn new(opt: &Opt) -> Scene {
time_max, time_max,
); );
let objects: Vec<Box<Hit>> = vec![ let objects: Vec<Box<dyn Hit>> = vec![
// Box1 // Box1
Box::new(Translate::new( Box::new(Translate::new(
RotateY::new( RotateY::new(
@@ -121,7 +119,7 @@ pub fn new(opt: &Opt) -> Scene {
Lambertian::new(ConstantTexture::new(Vec3::new(0.73, 0.73, 0.73))), Lambertian::new(ConstantTexture::new(Vec3::new(0.73, 0.73, 0.73))),
))), ))),
]; ];
let world: Box<Hit> = if opt.use_accel { let world: Box<dyn Hit> = if opt.use_accel {
Box::new(KDTree::new(objects, time_min, time_max)) Box::new(KDTree::new(objects, time_min, time_max))
} else { } else {
Box::new(HitableList::new(objects)) Box::new(HitableList::new(objects))
@@ -130,9 +128,10 @@ pub fn new(opt: &Opt) -> Scene {
camera, camera,
world, world,
subsamples: opt.subsamples, subsamples: opt.subsamples,
num_threads: opt.num_threads,
width: opt.width, width: opt.width,
height: opt.height, height: opt.height,
global_illumination: false, global_illumination: false,
env_map: None, ..Default::default()
} }
} }

View File

@@ -1,24 +1,21 @@
use std::sync::Arc; use std::sync::Arc;
use crate::camera::Camera; use crate::{
use crate::constant_medium::ConstantMedium; camera::Camera,
use crate::cuboid::Cuboid; constant_medium::ConstantMedium,
use crate::flip_normals::FlipNormals; cuboid::Cuboid,
use crate::hitable::Hit; flip_normals::FlipNormals,
use crate::hitable_list::HitableList; hitable::Hit,
use crate::kdtree::KDTree; hitable_list::HitableList,
use crate::material::DiffuseLight; kdtree::KDTree,
use crate::material::Lambertian; material::{DiffuseLight, Lambertian, Material},
use crate::material::Material; rect::{XYRect, XZRect, YZRect},
use crate::rect::XYRect; renderer::{Opt, Scene},
use crate::rect::XZRect; rotate::RotateY,
use crate::rect::YZRect; texture::ConstantTexture,
use crate::renderer::Opt; translate::Translate,
use crate::renderer::Scene; vec3::Vec3,
use crate::rotate::RotateY; };
use crate::texture::ConstantTexture;
use crate::translate::Translate;
use crate::vec3::Vec3;
pub fn new(opt: &Opt) -> Scene { pub fn new(opt: &Opt) -> Scene {
let lookfrom = Vec3::new(278., 278., -800.); let lookfrom = Vec3::new(278., 278., -800.);
@@ -40,11 +37,12 @@ pub fn new(opt: &Opt) -> Scene {
); );
let red = Lambertian::new(ConstantTexture::new([0.65, 0.05, 0.05])); let red = Lambertian::new(ConstantTexture::new([0.65, 0.05, 0.05]));
let white: Arc<Material> = Arc::new(Lambertian::new(ConstantTexture::new([0.73, 0.73, 0.73]))); let white: Arc<dyn Material> =
Arc::new(Lambertian::new(ConstantTexture::new([0.73, 0.73, 0.73])));
let green = Lambertian::new(ConstantTexture::new([0.12, 0.45, 0.15])); let green = Lambertian::new(ConstantTexture::new([0.12, 0.45, 0.15]));
let light = DiffuseLight::new(ConstantTexture::new([7., 7., 7.])); let light = DiffuseLight::new(ConstantTexture::new([7., 7., 7.]));
let objects: Vec<Box<Hit>> = vec![ let objects: Vec<Box<dyn Hit>> = vec![
// White smoke box on the right // White smoke box on the right
Box::new(ConstantMedium::new( Box::new(ConstantMedium::new(
Translate::new( Translate::new(
@@ -106,7 +104,7 @@ pub fn new(opt: &Opt) -> Scene {
Arc::clone(&white), Arc::clone(&white),
))), ))),
]; ];
let world: Box<Hit> = if opt.use_accel { let world: Box<dyn Hit> = if opt.use_accel {
Box::new(KDTree::new(objects, time_min, time_max)) Box::new(KDTree::new(objects, time_min, time_max))
} else { } else {
Box::new(HitableList::new(objects)) Box::new(HitableList::new(objects))
@@ -115,9 +113,9 @@ pub fn new(opt: &Opt) -> Scene {
camera, camera,
world, world,
subsamples: opt.subsamples, subsamples: opt.subsamples,
num_threads: opt.num_threads,
width: opt.width, width: opt.width,
height: opt.height, height: opt.height,
global_illumination: false, ..Default::default()
env_map: None,
} }
} }

View 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()
}
}

View File

@@ -1,33 +1,26 @@
use std::sync::Arc; use std::sync::Arc;
use image; use image;
use rand; use rand::{self, Rng};
use rand::Rng;
use crate::camera::Camera; use crate::{
use crate::constant_medium::ConstantMedium; camera::Camera,
use crate::cuboid::Cuboid; constant_medium::ConstantMedium,
use crate::hitable::Hit; cuboid::Cuboid,
use crate::hitable_list::HitableList; hitable::Hit,
use crate::kdtree::KDTree; hitable_list::HitableList,
use crate::material::Dielectric; kdtree::KDTree,
use crate::material::DiffuseLight; material::{Dielectric, DiffuseLight, Lambertian, Material, Metal},
use crate::material::Lambertian; moving_sphere::MovingSphere,
use crate::material::Material; noise::{perlin::Perlin, NoiseType},
use crate::material::Metal; rect::XZRect,
use crate::moving_sphere::MovingSphere; renderer::{Opt, Scene},
use crate::noise::perlin::Perlin; rotate::RotateY,
use crate::noise::NoiseType; sphere::Sphere,
use crate::rect::XZRect; texture::{ConstantTexture, ImageTexture, NoiseTexture},
use crate::renderer::Opt; translate::Translate,
use crate::renderer::Scene; vec3::Vec3,
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;
pub fn new(opt: &Opt) -> Scene { pub fn new(opt: &Opt) -> Scene {
let lookfrom = Vec3::new(478., 278., -600.); let lookfrom = Vec3::new(478., 278., -600.);
@@ -51,10 +44,12 @@ pub fn new(opt: &Opt) -> Scene {
let nb = 20; let nb = 20;
let rng = &mut rand::thread_rng(); let rng = &mut rand::thread_rng();
let mut boxlist: Vec<Box<Hit>> = Vec::with_capacity(10000); let mut boxlist: Vec<Box<dyn Hit>> = Vec::with_capacity(10000);
let mut list: Vec<Box<Hit>> = Vec::new(); let mut list: Vec<Box<dyn Hit>> = Vec::new();
let white: Arc<Material> = Arc::new(Lambertian::new(ConstantTexture::new([0.73, 0.73, 0.73]))); let white: Arc<dyn Material> =
let ground: Arc<Material> = Arc::new(Lambertian::new(ConstantTexture::new([0.48, 0.83, 0.53]))); Arc::new(Lambertian::new(ConstantTexture::new([0.73, 0.73, 0.73])));
let ground: Arc<dyn Material> =
Arc::new(Lambertian::new(ConstantTexture::new([0.48, 0.83, 0.53])));
for i in 0..nb { for i in 0..nb {
for j in 0..nb { for j in 0..nb {
let w = 100.; let w = 100.;
@@ -62,7 +57,7 @@ pub fn new(opt: &Opt) -> Scene {
let z0 = -1000. + j as f32 * w; let z0 = -1000. + j as f32 * w;
let y0 = 0.; let y0 = 0.;
let x1 = x0 + w; 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; let z1 = z0 + w;
boxlist.push(Box::new(Cuboid::new( boxlist.push(Box::new(Cuboid::new(
Vec3::new(x0, y0, z0), Vec3::new(x0, y0, z0),
@@ -102,7 +97,8 @@ pub fn new(opt: &Opt) -> Scene {
))); )));
// Blue smokey glass ball lower-left // Blue smokey glass ball lower-left
let boundary: Arc<Hit> = Arc::new(Sphere::new([360., 150., 145.], 70., Dielectric::new(1.5))); let boundary: Arc<dyn Hit> =
Arc::new(Sphere::new([360., 150., 145.], 70., Dielectric::new(1.5)));
list.push(Box::new(Arc::clone(&boundary))); list.push(Box::new(Arc::clone(&boundary)));
list.push(Box::new(ConstantMedium::new( list.push(Box::new(ConstantMedium::new(
Arc::clone(&boundary), Arc::clone(&boundary),
@@ -111,7 +107,7 @@ pub fn new(opt: &Opt) -> Scene {
))); )));
// General white mist over whole scene // General white mist over whole scene
let boundary: Arc<Hit> = Arc::new(Sphere::new([0., 0., 0.], 5000., Dielectric::new(1.5))); let boundary: Arc<dyn Hit> = Arc::new(Sphere::new([0., 0., 0.], 5000., Dielectric::new(1.5)));
list.push(Box::new(Arc::clone(&boundary))); list.push(Box::new(Arc::clone(&boundary)));
list.push(Box::new(ConstantMedium::new( list.push(Box::new(ConstantMedium::new(
Arc::clone(&boundary), Arc::clone(&boundary),
@@ -145,13 +141,13 @@ pub fn new(opt: &Opt) -> Scene {
// White 'cube' made of 1000 spheres // White 'cube' made of 1000 spheres
let ns = 1000; let ns = 1000;
let mut boxlist: Vec<Box<Hit>> = Vec::with_capacity(1000); let mut boxlist: Vec<Box<dyn Hit>> = Vec::with_capacity(1000);
for _ in 0..ns { for _ in 0..ns {
boxlist.push(Box::new(Sphere::new( boxlist.push(Box::new(Sphere::new(
[ [
165. * rng.gen_range::<f32>(0., 1.), 165. * rng.gen::<f32>(),
165. * rng.gen_range::<f32>(0., 1.), 165. * rng.gen::<f32>(),
165. * rng.gen_range::<f32>(0., 1.), 165. * rng.gen::<f32>(),
], ],
10., 10.,
Arc::clone(&white), Arc::clone(&white),
@@ -166,9 +162,9 @@ pub fn new(opt: &Opt) -> Scene {
camera, camera,
world: Box::new(HitableList::new(list)), world: Box::new(HitableList::new(list)),
subsamples: opt.subsamples, subsamples: opt.subsamples,
num_threads: opt.num_threads,
width: opt.width, width: opt.width,
height: opt.height, height: opt.height,
global_illumination: false, ..Default::default()
env_map: None,
} }
} }

View File

@@ -1,23 +1,18 @@
use rand; use rand;
use crate::camera::Camera; use crate::{
use crate::hitable::Hit; camera::Camera,
use crate::hitable_list::HitableList; hitable::Hit,
use crate::kdtree::KDTree; hitable_list::HitableList,
use crate::material::DiffuseLight; kdtree::KDTree,
use crate::material::Lambertian; material::{DiffuseLight, Lambertian},
use crate::noise::perlin::Perlin; noise::{perlin::Perlin, NoiseType},
use crate::noise::NoiseType; rect::{XYRect, XZRect, YZRect},
use crate::rect::XYRect; renderer::{Opt, Scene},
use crate::rect::XZRect; sphere::Sphere,
use crate::rect::YZRect; texture::{ConstantTexture, ImageTexture, Mandelbrot, NoiseTexture},
use crate::renderer::Opt; vec3::Vec3,
use crate::renderer::Scene; };
use crate::sphere::Sphere;
use crate::texture::ConstantTexture;
use crate::texture::Mandelbrot;
use crate::texture::NoiseTexture;
use crate::vec3::Vec3;
pub fn new(opt: &Opt) -> Scene { pub fn new(opt: &Opt) -> Scene {
let lookfrom = Vec3::new(20., 20., 20.); let lookfrom = Vec3::new(20., 20., 20.);
@@ -44,28 +39,32 @@ pub fn new(opt: &Opt) -> Scene {
Box::new(ConstantTexture::new(Vec3::new(0.4, 1.0, 0.4))) Box::new(ConstantTexture::new(Vec3::new(0.4, 1.0, 0.4)))
}; };
let world_image_bytes = include_bytes!("../../images/world.jpg");
let it = ImageTexture::new(image::load_from_memory(world_image_bytes).unwrap().to_rgb());
let noise_source = Perlin::new(rng); let noise_source = Perlin::new(rng);
let noise_type = NoiseType::Scale(10.); let noise_type = NoiseType::Scale(10.);
let objects: Vec<Box<Hit>> = vec![ let objects: Vec<Box<dyn Hit>> = vec![
// Textured globe // Textured globe
// Box::new(Sphere::new(Vec3::new(0., 2., 0.), 2.0, Lambertian::new(it))), // Box::new(Sphere::new(Vec3::new(0., 2., 0.), 2.0, Lambertian::new(it))),
Box::new(Sphere::new( Box::new(Sphere::new(
Vec3::new(0., 2., 0.), Vec3::new(0., 2., 0.),
2.0, 2.0,
DiffuseLight::new(Mandelbrot::default()), DiffuseLight::new(it),
)),
// Earth sized sphere
Box::new(Sphere::new(
Vec3::new(0., -1000., 0.),
1000.,
// Box::new(Lambertian::new(ground_color)),
Lambertian::new(NoiseTexture::new(noise_source, noise_type)),
)), )),
// Ground
Box::new(XZRect::new( Box::new(XZRect::new(
-100., -100.,
100., 100.,
-100., -100.,
1000., 1000.,
-60.,
DiffuseLight::new(Mandelbrot::default()),
)),
Box::new(XZRect::new(
-100.,
100.,
-100.,
200.,
60., 60.,
DiffuseLight::new(ConstantTexture::new(Vec3::new(1., 1., 1.))), DiffuseLight::new(ConstantTexture::new(Vec3::new(1., 1., 1.))),
)), )),
@@ -107,7 +106,7 @@ pub fn new(opt: &Opt) -> Scene {
1., 1.,
3., 3.,
4., 4.,
DiffuseLight::new(Mandelbrot::default()), Lambertian::new(NoiseTexture::new(noise_source, noise_type)),
)), )),
/* /*
Box::new(Sphere::new( Box::new(Sphere::new(
@@ -135,7 +134,7 @@ pub fn new(opt: &Opt) -> Scene {
)), )),
*/ */
]; ];
let world: Box<Hit> = if opt.use_accel { let world: Box<dyn Hit> = if opt.use_accel {
Box::new(KDTree::new(objects, time_min, time_max)) Box::new(KDTree::new(objects, time_min, time_max))
} else { } else {
Box::new(HitableList::new(objects)) Box::new(HitableList::new(objects))
@@ -144,9 +143,9 @@ pub fn new(opt: &Opt) -> Scene {
camera, camera,
world, world,
subsamples: opt.subsamples, subsamples: opt.subsamples,
num_threads: opt.num_threads,
width: opt.width, width: opt.width,
height: opt.height, height: opt.height,
global_illumination: false, ..Default::default()
env_map: None,
} }
} }

View File

@@ -3,8 +3,12 @@ pub mod book;
pub mod bvh; pub mod bvh;
pub mod cornell_box; pub mod cornell_box;
pub mod cornell_smoke; pub mod cornell_smoke;
pub mod dragon;
pub mod final_scene; pub mod final_scene;
pub mod mandelbrot; pub mod mandelbrot;
pub mod perlin_debug; pub mod perlin_debug;
pub mod spheramid;
pub mod stltest;
pub mod test; pub mod test;
pub mod tron;
pub mod tutorial; pub mod tutorial;

Some files were not shown because too many files have changed in this diff Show More