Compare commits

...

313 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
147 changed files with 14873 additions and 2498 deletions

View File

@@ -10,27 +10,29 @@ steps:
- env | sort
- find $PWD
- name: rtchallenge
image: registry.z.xinu.tv/drone/omnibus
commands:
- (cd rtchallenge && cargo build --examples)
- (cd rtchallenge && cargo test)
- (cd rtchallenge && cargo build --examples --no-default-features)
- (cd rtchallenge && cargo test --no-default-features)
- name: rtiow
image: registry.z.xinu.tv/drone/omnibus
commands:
- sccache -s
- apt-get update && apt-get install -y libgoogle-perftools-dev
- (cd rtiow && ./build_all_features.sh)
- (cd rtiow && cargo test --verbose --all)
- sccache -s
- name: aobench
image: registry.z.xinu.tv/drone/omnibus
commands:
- sccache -s
- (cd aobench && cargo build --verbose --all)
- (cd aobench && cargo test --verbose --all)
- sccache -s
- name: bheisler
image: registry.z.xinu.tv/drone/omnibus
commands:
- sccache -s
- (cd bheisler && cargo build --verbose --all)
- (cd bheisler && cargo test --verbose --all)
- sccache -s

2
.gitignore vendored
View File

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

View File

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

View File

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

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

2
bheisler/Cargo.lock generated
View File

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

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

3891
rtiow/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,36 +1,15 @@
[package]
authors = ["Bill Thiede <rust@xinu.tv>"]
edition = "2018"
name = "rtiow"
version = "0.1.0"
[workspace]
resolver = "2"
[[bench]]
harness = false
name = "spheres"
[dependencies]
actix-web = "0.7.8"
askama = "0.7.1"
chrono = "*"
core_affinity = "0.5"
cpuprofiler = { version = "0.0.3", optional = true }
image = "0.19.0"
lazy_static = "1.1.0"
log = "0.4.5"
num_cpus = "1.8.0"
rand = "0.5.5"
serde = "1.0.79"
serde_derive = "1.0.79"
serde_json = "1.0.41"
stderrlog = "0.4.1"
structopt = "0.2.10"
[dev-dependencies]
criterion = "0.2"
[profile]
members = [
"noise_explorer",
"noise_explorer_warp",
"renderer",
"tracer",
"vec3",
]
[profile.release]
debug = true
[features]
profile = ["cpuprofiler"]
[profile.dev]
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);

View File

@@ -1,4 +1,4 @@
set -e
export RUSTFLAGS="-D warnings"
cargo build
cargo build --features=profile
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;
extern crate askama;
#[macro_use]
extern crate log;
extern crate image;
extern crate rand;
#[macro_use]
extern crate serde_derive;
extern crate serde;
extern crate stderrlog;
use std::{fmt, time::SystemTime};
extern crate structopt;
extern crate rtiow;
use std::fmt;
use std::time::SystemTime;
use actix_web::http;
use actix_web::middleware;
use actix_web::server;
use actix_web::App;
use actix_web::HttpRequest;
use actix_web::HttpResponse;
use actix_web::Path;
use actix_web::Query;
use actix_web::Result;
use actix_web::{http, middleware, server, App, HttpRequest, HttpResponse, Path, Query, Result};
use askama::Template;
use log::info;
use rand::SeedableRng;
use rand::XorShiftRng;
use rand_xorshift::XorShiftRng;
use serde_derive::Deserialize;
use structopt::StructOpt;
use rtiow::noise;
use rtiow::noise::lode::Lode;
use rtiow::noise::perlin::Perlin;
use rtiow::noise::NoiseType;
use rtiow::texture::NoiseTexture;
use rtiow::texture::Texture;
use rtiow::vec3::Vec3;
use renderer::{
noise,
noise::{lode::Lode, perlin::Perlin, NoiseType},
texture::{NoiseTexture, Texture},
vec3::Vec3,
};
#[derive(Debug, StructOpt)]
#[derive(StructOpt)]
#[structopt(name = "noise_explorer", about = "CLI for exploring Perlin noise")]
struct Opt {
/// HTTP listen address
#[structopt(long = "addr", default_value = "0.0.0.0:8889")]
pub addr: String,
/// Width of noise images
#[structopt(short = "w", long = "width", default_value = "128")]
pub width: u32,
/// Height of noise images
#[structopt(short = "h", long = "height", default_value = "128")]
pub height: u32,
}
#[derive(Copy, Clone, Debug, Deserialize, PartialEq)]
@@ -75,11 +45,6 @@ struct OptionalParams {
pixel_scale: f32,
}
#[derive(Debug, Deserialize)]
struct NoiseSourceParam {
noise_source: NoiseSource,
}
#[derive(Debug, Deserialize)]
struct NoiseParams {
width: u32,
@@ -151,13 +116,15 @@ fn render_noise(noise_params: NoiseParams) -> image::GrayImage {
.optional
.unwrap_or(OptionalParams { pixel_scale: 1.0 });
const SEED: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
let rng: &mut XorShiftRng = &mut SeedableRng::from_seed(SEED);
let mut rng: XorShiftRng = SeedableRng::from_seed(SEED);
let mut img = image::GrayImage::new(noise_params.width, noise_params.height);
let tex: NoiseTexture<Box<dyn noise::NoiseSource>> = match noise_params.noise_source {
NoiseSource::Perlin => {
NoiseTexture::new(Box::new(Perlin::new(rng)), noise_params.noise_type)
NoiseTexture::new(Box::new(Perlin::new(&mut rng)), noise_params.noise_type)
}
NoiseSource::Lode => {
NoiseTexture::new(Box::new(Lode::new(&mut rng)), noise_params.noise_type)
}
NoiseSource::Lode => NoiseTexture::new(Box::new(Lode::new(rng)), noise_params.noise_type),
};
info!("{:?}", noise_params);
@@ -364,7 +331,7 @@ fn build_specimens(
}
fn style(_req: &HttpRequest) -> Result<HttpResponse> {
let bytes = include_bytes!("../../templates/style.css");
let bytes = include_bytes!("../templates/style.css");
Ok(HttpResponse::Ok().content_type("text/css").body(&bytes[..]))
}
@@ -428,7 +395,6 @@ fn noise_marble(
np: Path<NoiseParamsMarble>,
optional: Option<Query<OptionalParams>>,
) -> Result<HttpResponse> {
info!("optional {:?}", optional);
noise(NoiseParams {
width: np.width,
height: np.height,
@@ -477,16 +443,9 @@ fn main() -> Result<(), std::io::Error> {
mod tests {
use std::str;
use actix_web::http::Method;
use actix_web::test::TestServer;
use actix_web::HttpMessage;
use actix_web::Path;
use actix_web::Query;
use actix_web::{http::Method, test::TestServer, HttpMessage, Path, Query};
use super::NoiseParamsMarble;
use super::NoiseParamsScale;
use super::NoiseParamsTurbulence;
use super::OptionalParams;
use super::{NoiseParamsMarble, NoiseParamsScale, NoiseParamsTurbulence, OptionalParams};
#[test]
fn noise_param_from_req() {

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,16 +1,15 @@
use std;
use std::fmt;
use std::time::Instant;
use std::{self, fmt, time::Instant};
use rand;
use rand::Rng;
use log::info;
use rand::{self, Rng};
use crate::aabb::surrounding_box;
use crate::aabb::AABB;
use crate::hitable::Hit;
use crate::hitable::HitRecord;
use crate::ray::Ray;
use crate::{
aabb::{surrounding_box, AABB},
hitable::{Hit, HitRecord},
ray::Ray,
};
#[derive(Debug)]
enum BVHNode {
Leaf(Box<dyn Hit>),
Branch {
@@ -77,10 +76,10 @@ fn box_z_compare(ah: &Box<dyn Hit>, bh: &Box<dyn Hit>) -> std::cmp::Ordering {
// returns a reference.)
fn vec_first_into<T>(v: Vec<T>) -> T {
if v.len() != 1 {
panic!(format!(
panic!(
"vec_first_into called for vector length != 1, length {}",
v.len()
));
);
}
v.into_iter().next().unwrap()
}
@@ -105,7 +104,7 @@ impl BVHNode {
} else {
let mut l: Vec<Box<dyn Hit>> = l.into_iter().collect();
let mut rng = rand::thread_rng();
match rng.gen_range::<u16>(0, 3) {
match rng.gen_range(0..3) {
0 => l.sort_by(box_x_compare),
1 => l.sort_by(box_y_compare),
2 => l.sort_by(box_z_compare),
@@ -180,6 +179,7 @@ impl Hit for BVHNode {
}
}
#[derive(Debug)]
pub struct BVH {
root: BVHNode,
}
@@ -231,12 +231,13 @@ impl Hit for BVH {
#[cfg(test)]
mod tests {
use crate::aabb::AABB;
use crate::material::Lambertian;
use crate::material::Metal;
use crate::sphere::Sphere;
use crate::texture::ConstantTexture;
use crate::vec3::Vec3;
use crate::{
aabb::AABB,
material::{Lambertian, Metal},
sphere::Sphere,
texture::ConstantTexture,
vec3::Vec3,
};
use super::*;

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

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

View File

@@ -1,17 +1,17 @@
use std::sync::Arc;
use crate::aabb::AABB;
use crate::flip_normals::FlipNormals;
use crate::hitable::Hit;
use crate::hitable::HitRecord;
use crate::hitable_list::HitableList;
use crate::material::Material;
use crate::ray::Ray;
use crate::rect::XYRect;
use crate::rect::XZRect;
use crate::rect::YZRect;
use crate::vec3::Vec3;
use crate::{
aabb::AABB,
flip_normals::FlipNormals,
hitable::{Hit, HitRecord},
hitable_list::HitableList,
material::Material,
ray::Ray,
rect::{XYRect, XZRect, YZRect},
vec3::Vec3,
};
#[derive(Debug)]
pub struct Cuboid {
p_min: Vec3,
p_max: Vec3,
@@ -22,6 +22,9 @@ impl Cuboid {
// This clippy doesn't work right with Arc.
#[allow(clippy::needless_pass_by_value)]
pub fn new(p_min: Vec3, p_max: Vec3, material: Arc<dyn Material>) -> Cuboid {
assert!(p_min.x <= p_max.x);
assert!(p_min.y <= p_max.y);
assert!(p_min.z <= p_max.z);
Cuboid {
p_min,
p_max,

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::hitable::Hit;
use crate::hitable::HitRecord;
use crate::ray::Ray;
use crate::{
aabb::AABB,
hitable::{Hit, HitRecord},
ray::Ray,
};
#[derive(Debug)]
pub struct FlipNormals<H>
where
H: Hit,

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

View File

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

View File

@@ -6,28 +6,24 @@
//! Print some human readable strings
//!
//! ```rust
//! use rtiow::human;
//! use renderer::human;
//!
//! // "1.00 k"
//! let tmpStr = human::Formatter::new()
//! .format(1000.0);
//! let tmpStr = human::Formatter::new().format(1000.0);
//! # assert_eq!(tmpStr, "1.00 k");
//!
//! // "1.00 M"
//! let tmpStr2 = human::Formatter::new()
//! .format(1000000.0);
//! let tmpStr2 = human::Formatter::new().format(1000000.0);
//! # assert_eq!(tmpStr2, "1.00 M");
//!
//! // "1.00 B"
//! let tmpStr3 = human::Formatter::new()
//! .format(1000000000.0);
//! let tmpStr3 = human::Formatter::new().format(1000000000.0);
//! # assert_eq!(tmpStr3, "1.00 B");
//! ```
//!
//! If you are so inspired you can even try playing with units and customizing your `Scales`
//!
//! For more examples you should review the examples on github: [tests/demo.rs](https://github.com/BobGneu/human-format-rs/blob/master/tests/demo.rs)
//!
#[derive(Debug)]
struct ScaledValue {
@@ -130,7 +126,13 @@ impl Formatter {
let magnitude_multiplier = self.scales.get_magnitude_multipler(&suffix);
(result * magnitude_multiplier)
result * magnitude_multiplier
}
}
impl Default for Formatter {
fn default() -> Self {
Self::new()
}
}
@@ -203,7 +205,7 @@ impl Scales {
}
}
return 0.0;
0.0
}
fn to_scaled_value(&self, value: f64) -> ScaledValue {
@@ -225,3 +227,9 @@ impl Scales {
}
}
}
impl Default for Scales {
fn default() -> Self {
Self::new()
}
}

View File

@@ -1,11 +1,15 @@
use std::fmt;
use crate::aabb::surrounding_box;
use crate::aabb::AABB;
use crate::hitable::Hit;
use crate::hitable::HitRecord;
use crate::ray::Ray;
use log::info;
use crate::{
aabb::{surrounding_box, AABB},
hitable::{Hit, HitRecord},
hitable_list::HitableList,
ray::Ray,
};
#[derive(Debug)]
pub enum KDTree {
Leaf(Box<dyn Hit>),
Branch {
@@ -20,10 +24,10 @@ pub enum KDTree {
// returns a reference.)
fn vec_first_into<T>(v: Vec<T>) -> T {
if v.len() != 1 {
panic!(format!(
panic!(
"vec_first_into called for vector length != 1, length {}",
v.len()
));
);
}
v.into_iter().next().unwrap()
}
@@ -100,6 +104,14 @@ impl KDTree {
}),
_ => panic!("Unreachable"),
};
//info!("left_half {:?}", left_half);
//info!("right_half {:?}", right_half);
if left_half.is_empty() {
return KDTree::Leaf(Box::new(HitableList::new(right_half)));
};
if right_half.is_empty() {
return KDTree::Leaf(Box::new(HitableList::new(left_half)));
};
KDTree::Branch {
left: Box::new(KDTree::new(left_half, t_min, t_max)),
right: Box::new(KDTree::new(right_half, t_min, t_max)),
@@ -144,7 +156,7 @@ fn print_tree(f: &mut fmt::Formatter, depth: usize, kdt: &KDTree) -> fmt::Result
impl fmt::Display for KDTree {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "kd-tree")?;
print_tree(f, 1, &self)
print_tree(f, 1, self)
}
}
impl Hit for KDTree {

View File

@@ -1,9 +1,13 @@
pub mod aabb;
pub mod bvh;
pub mod bvh_triangles;
pub mod camera;
pub mod colors;
pub mod constant_medium;
pub mod cuboid;
pub mod debug_hit;
pub mod flip_normals;
pub mod glowybox;
pub mod hitable;
pub mod hitable_list;
pub mod human;
@@ -12,22 +16,15 @@ pub mod material;
pub mod moving_sphere;
pub mod noise;
pub mod output;
pub mod parser;
pub mod ray;
pub mod rect;
pub mod renderer;
pub mod rotate;
pub mod scale;
pub mod scenes;
pub mod sphere;
pub mod texture;
pub mod translate;
pub mod triangles;
pub mod vec3;
extern crate image;
#[macro_use]
extern crate log;
extern crate num_cpus;
extern crate rand;
#[macro_use]
extern crate structopt;
#[macro_use]
extern crate serde_derive;

View File

@@ -1,38 +1,33 @@
use std::sync::Arc;
use std::{fmt::Debug, sync::Arc};
extern crate rand;
use self::rand::Rng;
use rand::{self, Rng};
use crate::hitable::HitRecord;
use crate::ray::Ray;
use crate::texture::Texture;
use crate::vec3::dot;
use crate::vec3::Vec3;
use crate::{
hitable::HitRecord,
ray::Ray,
texture::Texture,
vec3::{dot, Vec3},
};
fn random_in_unit_sphere() -> Vec3 {
let mut rng = rand::thread_rng();
let v = Vec3::new(1., 1., 1.);
loop {
let p =
2. * Vec3::new(
rng.gen_range::<f32>(0., 1.),
rng.gen_range::<f32>(0., 1.),
rng.gen_range::<f32>(0., 1.),
) - v;
let p = 2. * Vec3::new(rng.gen(), rng.gen(), rng.gen()) - v;
if p.squared_length() < 1. {
return p;
}
}
}
#[derive(Default)]
#[derive(Default, Debug)]
pub struct ScatterResponse {
pub scattered: Ray,
pub attenutation: Vec3,
pub reflected: bool,
}
pub trait Material: Send + Sync {
pub trait Material: Send + Sync + Debug {
fn scatter(&self, r_in: &Ray, rec: &HitRecord) -> ScatterResponse;
fn emitted(&self, _u: f32, _v: f32, _p: Vec3) -> Vec3 {
Vec3::new(0., 0., 0.)
@@ -57,6 +52,7 @@ impl Material for Box<dyn Material> {
}
}
#[derive(Clone, Debug)]
pub struct Isotropic<T>
where
T: Texture,
@@ -87,6 +83,7 @@ where
}
}
#[derive(Clone, Debug)]
pub struct Lambertian<T>
where
T: Texture,
@@ -118,6 +115,7 @@ where
}
}
#[derive(Clone, Debug)]
pub struct Metal {
albedo: Vec3,
fuzzy: f32,
@@ -172,6 +170,7 @@ fn schlick(cosine: f32, ref_idx: f32) -> f32 {
r0 + (1. - r0) * (1. - cosine).powf(5.)
}
#[derive(Clone, Debug)]
pub struct Dielectric {
ref_idx: f32,
}
@@ -201,7 +200,7 @@ impl Material for Dielectric {
let scattered = if let Some(refracted) = refract(r_in.direction, outward_normal, ni_over_nt)
{
let mut rng = rand::thread_rng();
if rng.gen_range::<f32>(0., 1.) < schlick(cosine, self.ref_idx) {
if rng.gen::<f32>() < schlick(cosine, self.ref_idx) {
Ray::new(rec.p, reflected, r_in.time)
} else {
Ray::new(rec.p, refracted, r_in.time)
@@ -218,6 +217,7 @@ impl Material for Dielectric {
}
}
#[derive(Debug)]
pub struct DiffuseLight<T>
where
T: Texture,
@@ -251,6 +251,20 @@ where
}
}
#[derive(Debug)]
pub struct DebugMaterial {}
impl Material for DebugMaterial {
fn scatter(&self, _r_in: &Ray, rec: &HitRecord) -> ScatterResponse {
let dir = Vec3::new(0., -1., -1.).unit_vector();
ScatterResponse {
scattered: Ray::new(rec.p, dir, 0.),
attenutation: [1., 1., 1.].into(),
reflected: true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

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

View File

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

View File

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

View File

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

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 direction: Vec3,
pub time: f32,
// Precache 1/direction, a single ray intersects multiple AABB's, and divides are more
// expensive than multiplies.
pub inv_direction: Vec3,
pub sign: [usize; 3],
}
@@ -14,7 +17,7 @@ impl Ray {
where
V: Into<Vec3>,
{
let direction = direction.into();
let direction: Vec3 = direction.into();
let origin = origin.into();
let inv = 1. / direction;
Ray {

View File

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

View File

@@ -1,36 +1,41 @@
use std::fmt;
use std::ops::AddAssign;
use std::ops::Range;
use std::path::Path;
use std::path::PathBuf;
use std::str;
use std::sync;
use std::sync::mpsc::sync_channel;
use std::sync::mpsc::Receiver;
use std::sync::mpsc::SyncSender;
use std::sync::Arc;
use std::sync::Mutex;
use std::thread;
use std::time;
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;
use rand::Rng;
use rand::{self, Rng};
use serde_derive::Serialize;
use structopt::StructOpt;
use crate::camera::Camera;
use crate::hitable::Hit;
use crate::human;
use crate::material::Lambertian;
use crate::output;
use crate::ray::Ray;
use crate::scenes;
use crate::sphere::Sphere;
use crate::texture::ConstantTexture;
use crate::texture::EnvMap;
use crate::vec3::Vec3;
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)]
#[derive(Debug, EnumString, EnumVariantNames, strum::Display)]
#[strum(serialize_all = "snake_case")]
pub enum Model {
BVH,
Bench,
@@ -41,24 +46,30 @@ pub enum Model {
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::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::Test => scenes::test::new(&opt),
Model::Tutorial => scenes::tutorial::new(&opt),
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),
}
}
}
@@ -72,44 +83,6 @@ impl fmt::Display for ModelParseError {
}
}
impl str::FromStr for Model {
type Err = ModelParseError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"bench" => Ok(Model::Bench),
"book" => Ok(Model::Book),
"bvh" => Ok(Model::BVH),
"cornell_box" => Ok(Model::CornellBox),
"cornell_smoke" => Ok(Model::CornellSmoke),
"final" => Ok(Model::Final),
"mandelbrot" => Ok(Model::Mandelbrot),
"perlin_debug" => Ok(Model::PerlinDebug),
"spheramid" => Ok(Model::Spheramid),
"test" => Ok(Model::Test),
"tutorial" => Ok(Model::Tutorial),
_ => Err(ModelParseError(s.to_owned())),
}
}
}
impl std::string::ToString for Model {
fn to_string(&self) -> String {
match self {
Model::BVH => "bvh".to_string(),
Model::Bench => "bench".to_string(),
Model::Book => "book".to_string(),
Model::CornellBox => "cornell_box".to_string(),
Model::CornellSmoke => "cornell_smoke".to_string(),
Model::Final => "final".to_string(),
Model::Mandelbrot => "mandelbrot".to_string(),
Model::PerlinDebug => "perlin_debug".to_string(),
Model::Spheramid => "spheramid".to_string(),
Model::Test => "test".to_string(),
Model::Tutorial => "tutorial".to_string(),
}
}
}
#[derive(Debug, StructOpt)]
#[structopt(name = "tracer", about = "An experimental ray tracer.")]
pub struct Opt {
@@ -125,19 +98,29 @@ pub struct Opt {
/// Sub-samples per pixel
#[structopt(short = "s", long = "subsample", default_value = "8")]
pub subsamples: usize,
/// Select scene to render, one of: "bench", "book", "tutorial", "bvh", "test", "cornell_box",
/// "cornell_smoke", "perlin_debug", "final"
#[structopt(long = "model", default_value = "book")]
pub model: Model,
/// 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(parse(from_os_str), default_value = "/tmp/tracer")]
#[structopt(
short = "o",
long = "output",
parse(from_os_str),
default_value = "/tmp/tracer"
)]
pub output: PathBuf,
}
@@ -149,18 +132,20 @@ pub fn opt_hash(opt: &Opt) -> String {
opt.height,
opt.subsamples,
opt.pprof.is_some(),
opt.model.to_string(),
opt.model.as_ref().unwrap().to_string(),
opt.use_accel,
opt.output.display().to_string().replace("/", "_")
opt.output.display().to_string().replace('/', "_")
)
}
// TODO(wathiede): implement the skips and then the renderer could use json as an input file type.
#[derive(Serialize)]
#[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,
@@ -265,6 +250,7 @@ fn trace_pixel_adaptive(
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;
@@ -279,7 +265,7 @@ fn trace_pixel_adaptive(
&scene.env_map,
);
if depth == 0 {
output::set_pixel(output::ADAPTIVE_DEPTH, x, y, [1., 0., 0.].into());
output.set_pixel(output::ADAPTIVE_DEPTH, x, y, [1., 0., 0.].into());
return (center, rays);
}
// t = top
@@ -321,6 +307,7 @@ fn trace_pixel_adaptive(
x_range.start..x_mid,
y_range.start..y_mid,
scene,
output,
);
let tr = trace_pixel_adaptive(
depth - 1,
@@ -330,6 +317,7 @@ fn trace_pixel_adaptive(
x_mid..x_range.end,
y_range.start..y_mid,
scene,
output,
);
let bl = trace_pixel_adaptive(
depth - 1,
@@ -339,6 +327,7 @@ fn trace_pixel_adaptive(
x_range.start..x_mid,
y_mid..y_range.end,
scene,
output,
);
let br = trace_pixel_adaptive(
depth - 1,
@@ -348,13 +337,14 @@ fn trace_pixel_adaptive(
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());
output.set_pixel(output::ADAPTIVE_DEPTH, x, y, [0., 1., 0.].into());
}
(corners, rays)
}
@@ -362,8 +352,8 @@ fn trace_pixel_adaptive(
fn trace_pixel_random(x: usize, y: usize, scene: &Scene) -> (Vec3, usize) {
let mut rng = rand::thread_rng();
let u = (rng.gen_range::<f32>(0., 1.) + x as f32) / scene.width as f32;
let v = (rng.gen_range::<f32>(0., 1.) + y as f32) / scene.height as f32;
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,
@@ -374,7 +364,7 @@ fn trace_pixel_random(x: usize, y: usize, scene: &Scene) -> (Vec3, usize) {
)
}
#[derive(Clone, Copy)]
#[derive(Clone, Copy, Default)]
struct RenderStats {
rays: usize,
pixels: usize,
@@ -390,33 +380,37 @@ impl AddAssign for RenderStats {
}
fn progress(
start_time: Instant,
last_stat: &RenderStats,
current_stat: &RenderStats,
time_diff: time::Duration,
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}%) {:7}pixels/s {:7}rays/s",
"{: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),
100 * current_stat.pixels / pixel_total,
percent,
human.format(pixel_diff as f64 / time_diff.as_secs_f64()),
human.format(ray_diff as f64 / time_diff.as_secs_f64())
human.format(ray_diff as f64 / time_diff.as_secs_f64()),
eta
)
}
impl Default for RenderStats {
fn default() -> Self {
RenderStats { rays: 0, pixels: 0 }
}
}
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 {
@@ -433,7 +427,7 @@ enum Response {
},
}
fn render_pixel(scene: &Scene, x: usize, y: usize) -> (Vec3, usize) {
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,
@@ -443,6 +437,7 @@ fn render_pixel(scene: &Scene, x: usize, y: usize) -> (Vec3, usize) {
0.0..1.0,
0.0..1.0,
scene,
output,
)
} else {
let (pixel, rays) = (0..scene.subsamples)
@@ -451,7 +446,7 @@ fn render_pixel(scene: &Scene, x: usize, y: usize) -> (Vec3, usize) {
([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);
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
@@ -467,6 +462,7 @@ fn render_worker(
scene: &Scene,
input_chan: Arc<Mutex<Receiver<Request>>>,
output_chan: &SyncSender<Response>,
output: &OutputManager,
) {
loop {
let job = { input_chan.lock().unwrap().recv() };
@@ -481,7 +477,7 @@ fn render_worker(
let batch = false;
if batch {
let (pixels, rays): (Vec<Vec3>, Vec<usize>) = (0..width)
.map(|x| render_pixel(scene, x, y))
.map(|x| render_pixel(scene, x, y, output))
.collect::<Vec<(_, _)>>()
.into_iter()
.unzip();
@@ -498,7 +494,7 @@ fn render_worker(
.expect("failed to send pixel response");
} else {
(0..width).for_each(|x| {
let (pixel, rays) = render_pixel(scene, x, y);
let (pixel, rays) = render_pixel(scene, x, y, output);
output_chan
.send(Response::Pixel {
x,
@@ -512,7 +508,7 @@ fn render_worker(
}
Request::Pixel { x, y } => {
trace!("tid {} x {} y {}", tid, x, y);
let (pixel, rays) = render_pixel(scene, x, y);
let (pixel, rays) = render_pixel(scene, x, y, output);
output_chan
.send(Response::Pixel {
x,
@@ -527,8 +523,19 @@ fn render_worker(
}
}
pub fn render(scene: Scene, output_dir: &Path) -> std::result::Result<(), std::io::Error> {
let num_threads = scene.num_threads.unwrap_or_else(num_cpus::get);
/*
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);
@@ -542,20 +549,23 @@ pub fn render(scene: Scene, output_dir: &Path) -> std::result::Result<(), std::i
} 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.register_image(
output::MAIN_IMAGE.to_string(),
(scene.width, scene.height),
output::ImageType::RGB01,
);
if scene.adaptive_subsampling.is_some() {
output::register_image(
output.register_image(
output::ADAPTIVE_DEPTH.to_string(),
(scene.width, scene.height),
output::ImageType::RGB01,
);
}
output::register_image(
output.register_image(
output::RAYS_PER_PIXEL.to_string(),
(scene.width, scene.height),
output::ImageType::GreyNormalized,
@@ -569,19 +579,19 @@ pub fn render(scene: Scene, output_dir: &Path) -> std::result::Result<(), std::i
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);
render_worker(i, &s, pixel_req_rx, &pixel_resp_tx, &output);
})
})
.collect::<Vec<_>>();
drop(pixel_req_rx);
drop(pixel_resp_tx);
let start_time = time::Instant::now();
let (w, h) = (scene.width, scene.height);
handles.push(thread::spawn(move || {
let batch_line_requests = true;
let batch_line_requests = false;
if batch_line_requests {
for y in 0..h {
pixel_req_tx
@@ -603,29 +613,36 @@ pub fn render(scene: Scene, output_dir: &Path) -> std::result::Result<(), std::i
info!("Rendering with {} subsamples", scene.subsamples);
let pixel_total = scene.width * scene.height;
let mut last_time = time::Instant::now();
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);
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);
output.set_pixel(output::MAIN_IMAGE, x, y, *pixel);
}
}
}
let now = time::Instant::now();
let now = Instant::now();
let time_diff = now - last_time;
if time_diff > time::Duration::from_secs(1) {
info!(
if time_diff > Duration::from_secs(5) {
println!(
"{}",
progress(&last_stat, &current_stat, time_diff, pixel_total)
progress(
render_start_time,
&last_stat,
&current_stat,
time_diff,
pixel_total
)
);
last_stat = current_stat;
last_time = now;
@@ -634,12 +651,18 @@ pub fn render(scene: Scene, output_dir: &Path) -> std::result::Result<(), std::i
for thr in handles {
thr.join().expect("thread join");
}
let time_diff = time::Instant::now() - start_time;
info!(
"Runtime {} seconds {}",
let time_diff = Instant::now() - render_start_time;
println!(
"Render {} seconds {}",
time_diff.as_secs_f32(),
progress(&Default::default(), &current_stat, time_diff, pixel_total)
progress(
render_start_time,
&Default::default(),
&current_stat,
time_diff,
pixel_total
)
);
output::write_images(&scene, time_diff, output_dir)
output.write_images(&scene, time_diff, output_dir)
}

View File

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

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 rand::Rng;
use log::trace;
use rand::{self, Rng};
use crate::bvh::BVH;
use crate::camera::Camera;
use crate::hitable::Hit;
use crate::hitable_list::HitableList;
use crate::kdtree::KDTree;
use crate::material::Lambertian;
use crate::renderer::Opt;
use crate::renderer::Scene;
use crate::sphere::Sphere;
use crate::texture::ConstantTexture;
use crate::vec3::Vec3;
use crate::{
bvh::BVH,
camera::Camera,
hitable::Hit,
hitable_list::HitableList,
kdtree::KDTree,
material::Lambertian,
renderer::{Opt, Scene},
sphere::Sphere,
texture::ConstantTexture,
vec3::Vec3,
};
pub fn new(opt: &Opt) -> Scene {
let lookfrom = Vec3::new(20., 20., 20.);
@@ -36,12 +37,12 @@ pub fn new(opt: &Opt) -> Scene {
let len = 1000;
for x in 0..len {
for z in 0..len {
let r = rng.gen_range::<f32>(0., 1.);
let g = rng.gen_range::<f32>(0., 1.);
let b = rng.gen_range::<f32>(0., 1.);
let r = rng.gen();
let g = rng.gen();
let b = rng.gen();
let x_pos = (x - len / 2) as f32 + rng.gen_range(-0.1, 0.1);
let z_pos = (z - len / 2) as f32 + rng.gen_range(-0.1, 0.1);
let x_pos = (x - len / 2) as f32 + rng.gen_range(-0.1..0.1);
let z_pos = (z - len / 2) as f32 + rng.gen_range(-0.1..0.1);
grid.push(Box::new(Sphere::new(
Vec3::new(x_pos, 0., z_pos),

View File

@@ -1,21 +1,16 @@
use rand;
use rand::Rng;
use rand::{self, Rng};
use crate::camera::Camera;
use crate::hitable::Hit;
use crate::hitable_list::HitableList;
use crate::kdtree::KDTree;
use crate::material::Dielectric;
use crate::material::Lambertian;
use crate::material::Material;
use crate::material::Metal;
use crate::renderer::Opt;
use crate::renderer::Scene;
use crate::sphere::Sphere;
use crate::texture::CheckerTexture;
use crate::texture::ConstantTexture;
use crate::texture::EnvMap;
use crate::vec3::Vec3;
use crate::{
camera::Camera,
hitable::Hit,
hitable_list::HitableList,
kdtree::KDTree,
material::{Dielectric, Lambertian, Material, Metal},
renderer::{Opt, Scene},
sphere::Sphere,
texture::{CheckerTexture, ConstantTexture, EnvMap},
vec3::Vec3,
};
pub fn new(opt: &Opt) -> Scene {
let lookfrom = Vec3::new(13., 2., 3.);
@@ -81,7 +76,7 @@ where
1000.,
ground_material,
))];
let mut random = || rng.gen_range::<f32>(0., 1.);
let mut random = || rng.gen::<f32>();
for a in -11..11 {
for b in -11..11 {

View File

@@ -1,14 +1,16 @@
use crate::bvh::BVH;
use crate::camera::Camera;
use crate::hitable::Hit;
use crate::material::Lambertian;
use crate::material::Metal;
use crate::moving_sphere::MovingSphere;
use crate::renderer::Opt;
use crate::renderer::Scene;
use crate::sphere::Sphere;
use crate::texture::ConstantTexture;
use crate::vec3::Vec3;
use log::trace;
use crate::{
bvh::BVH,
camera::Camera,
hitable::Hit,
material::{Lambertian, Metal},
moving_sphere::MovingSphere,
renderer::{Opt, Scene},
sphere::Sphere,
texture::ConstantTexture,
vec3::Vec3,
};
pub fn new(opt: &Opt) -> Scene {
let lookfrom = Vec3::new(3., 3., 2.);

View File

@@ -1,22 +1,20 @@
use std::sync::Arc;
use crate::camera::Camera;
use crate::cuboid::Cuboid;
use crate::flip_normals::FlipNormals;
use crate::hitable::Hit;
use crate::hitable_list::HitableList;
use crate::kdtree::KDTree;
use crate::material::DiffuseLight;
use crate::material::Lambertian;
use crate::rect::XYRect;
use crate::rect::XZRect;
use crate::rect::YZRect;
use crate::renderer::Opt;
use crate::renderer::Scene;
use crate::rotate::RotateY;
use crate::texture::ConstantTexture;
use crate::translate::Translate;
use crate::vec3::Vec3;
use crate::{
camera::Camera,
cuboid::Cuboid,
flip_normals::FlipNormals,
hitable::Hit,
hitable_list::HitableList,
kdtree::KDTree,
material::{DiffuseLight, Lambertian},
rect::{XYRect, XZRect, YZRect},
renderer::{Opt, Scene},
rotate::RotateY,
texture::ConstantTexture,
translate::Translate,
vec3::Vec3,
};
pub fn new(opt: &Opt) -> Scene {
let lookfrom = Vec3::new(278., 278., -800.);

View File

@@ -1,24 +1,21 @@
use std::sync::Arc;
use crate::camera::Camera;
use crate::constant_medium::ConstantMedium;
use crate::cuboid::Cuboid;
use crate::flip_normals::FlipNormals;
use crate::hitable::Hit;
use crate::hitable_list::HitableList;
use crate::kdtree::KDTree;
use crate::material::DiffuseLight;
use crate::material::Lambertian;
use crate::material::Material;
use crate::rect::XYRect;
use crate::rect::XZRect;
use crate::rect::YZRect;
use crate::renderer::Opt;
use crate::renderer::Scene;
use crate::rotate::RotateY;
use crate::texture::ConstantTexture;
use crate::translate::Translate;
use crate::vec3::Vec3;
use crate::{
camera::Camera,
constant_medium::ConstantMedium,
cuboid::Cuboid,
flip_normals::FlipNormals,
hitable::Hit,
hitable_list::HitableList,
kdtree::KDTree,
material::{DiffuseLight, Lambertian, Material},
rect::{XYRect, XZRect, YZRect},
renderer::{Opt, Scene},
rotate::RotateY,
texture::ConstantTexture,
translate::Translate,
vec3::Vec3,
};
pub fn new(opt: &Opt) -> Scene {
let lookfrom = Vec3::new(278., 278., -800.);

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 image;
use rand;
use rand::Rng;
use rand::{self, Rng};
use crate::camera::Camera;
use crate::constant_medium::ConstantMedium;
use crate::cuboid::Cuboid;
use crate::hitable::Hit;
use crate::hitable_list::HitableList;
use crate::kdtree::KDTree;
use crate::material::Dielectric;
use crate::material::DiffuseLight;
use crate::material::Lambertian;
use crate::material::Material;
use crate::material::Metal;
use crate::moving_sphere::MovingSphere;
use crate::noise::perlin::Perlin;
use crate::noise::NoiseType;
use crate::rect::XZRect;
use crate::renderer::Opt;
use crate::renderer::Scene;
use crate::rotate::RotateY;
use crate::sphere::Sphere;
use crate::texture::ConstantTexture;
use crate::texture::ImageTexture;
use crate::texture::NoiseTexture;
use crate::translate::Translate;
use crate::vec3::Vec3;
use crate::{
camera::Camera,
constant_medium::ConstantMedium,
cuboid::Cuboid,
hitable::Hit,
hitable_list::HitableList,
kdtree::KDTree,
material::{Dielectric, DiffuseLight, Lambertian, Material, Metal},
moving_sphere::MovingSphere,
noise::{perlin::Perlin, NoiseType},
rect::XZRect,
renderer::{Opt, Scene},
rotate::RotateY,
sphere::Sphere,
texture::{ConstantTexture, ImageTexture, NoiseTexture},
translate::Translate,
vec3::Vec3,
};
pub fn new(opt: &Opt) -> Scene {
let lookfrom = Vec3::new(478., 278., -600.);
@@ -64,7 +57,7 @@ pub fn new(opt: &Opt) -> Scene {
let z0 = -1000. + j as f32 * w;
let y0 = 0.;
let x1 = x0 + w;
let y1 = 100. * (rng.gen_range::<f32>(0., 1.) + 0.01);
let y1 = 100. * (rng.gen::<f32>() + 0.01);
let z1 = z0 + w;
boxlist.push(Box::new(Cuboid::new(
Vec3::new(x0, y0, z0),
@@ -152,9 +145,9 @@ pub fn new(opt: &Opt) -> Scene {
for _ in 0..ns {
boxlist.push(Box::new(Sphere::new(
[
165. * rng.gen_range::<f32>(0., 1.),
165. * rng.gen_range::<f32>(0., 1.),
165. * rng.gen_range::<f32>(0., 1.),
165. * rng.gen::<f32>(),
165. * rng.gen::<f32>(),
165. * rng.gen::<f32>(),
],
10.,
Arc::clone(&white),

View File

@@ -1,24 +1,18 @@
use rand;
use crate::camera::Camera;
use crate::hitable::Hit;
use crate::hitable_list::HitableList;
use crate::kdtree::KDTree;
use crate::material::DiffuseLight;
use crate::material::Lambertian;
use crate::noise::perlin::Perlin;
use crate::noise::NoiseType;
use crate::rect::XYRect;
use crate::rect::XZRect;
use crate::rect::YZRect;
use crate::renderer::Opt;
use crate::renderer::Scene;
use crate::sphere::Sphere;
use crate::texture::ConstantTexture;
use crate::texture::ImageTexture;
use crate::texture::Mandelbrot;
use crate::texture::NoiseTexture;
use crate::vec3::Vec3;
use crate::{
camera::Camera,
hitable::Hit,
hitable_list::HitableList,
kdtree::KDTree,
material::{DiffuseLight, Lambertian},
noise::{perlin::Perlin, NoiseType},
rect::{XYRect, XZRect, YZRect},
renderer::{Opt, Scene},
sphere::Sphere,
texture::{ConstantTexture, ImageTexture, Mandelbrot, NoiseTexture},
vec3::Vec3,
};
pub fn new(opt: &Opt) -> Scene {
let lookfrom = Vec3::new(20., 20., 20.);

View File

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

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