diff --git a/.gitignore b/.gitignore index 988ac934b..66027362c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ # Git Ignore Rules for raytracing.github.io -/build/ +build/ +/*.ppm diff --git a/CHANGELOG.md b/CHANGELOG.md index 413b60ed5..312b5a33c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,36 +1,574 @@ -Change Log -- Ray Tracing in One Weekend +Change Log / Ray Tracing in One Weekend ==================================================================================================== +# v4.0.2 (2025-04-24) + +### Common + - Fix -- Fixed some dangling references to `random_in_unit_sphere()` (#1637) + - Fix -- Clarify `uniform_real_distribution` usage for `random_double()` (#1680) + - Update -- CMake minimum required version max now at 4.0.0. + +### In One Weekend + - Fix -- Fix equation for refracted rays of non-unit length (#1644) + - Fix -- Typo "trigonometric qualities" -> "trigonometric identities" + +### The Rest of Your Life + - Fix -- Typo in equation in book 3, section 12.3 (#1686) + + +---------------------------------------------------------------------------------------------------- +# v4.0.1 (2024-08-31) + +### Common + - Change -- Include hittable.h from material.h; drop `hit_record` forward declaration (#1609) + - Change -- Refactor sphere to use ray representation for animate center (#1621) + - Change -- All headers assume implicit rtweekend.h include (#1628) + - Fix -- Big improvement to print version listing font size (#1595) and more compact line + height for code listings in both print and browser. + - Fix -- Slight improvement to `rotate_y::hit()` function (#1484) + - Fix -- Fixed possible bogus values from `random_unit_vector()` due to underflow (#1606) + +### In One Weekend + - Fix -- Fixed usage of the term "unit cube" for a cube of diameter two (#1555, #1603) + - Fix -- Fixed broken highlighting on some code listings (#1600) + +### The Next Week + - Fix -- Add missing ellipsis in listing 2.62 (#1612) + +### The Rest of Your Life + - Fix -- Fix typo of "arbitrary" (#1589) + - Fix -- Fix X-axis label for figure 3.08 (Approximating the nonuniform f()) (#1532) + - Fix -- Corrected scatter angle theta range in section 3.5.3 (The Scattering PDF) (#1331) + - Fix -- Clarify the distinction between average and expected value (#1535) + - New -- Added a bit more explanation of Buffon's needle problem (#1529) + + +---------------------------------------------------------------------------------------------------- +# v4.0.0 (2024-07-26) + +From our last official v3.2.3 release (three and a half years ago!), this major release includes all +changes in the v4.0.0-alpha.1 and v4.0.0-alpha.2 releases, plus the changes listed immediately +below. Generally, this represents a large overhaul of all three books and their code, and will +require large changes to any code you've based on the prior v3.2.3 version. Going forward, we plan +to avoid such massive, long-running development branches, at the expense of more frequent minor and +major releases. + +There's still a fair amount of work remaining on book three, which we'll work on after this release. + +### Common + - Change -- Use delegating constructors where helpful (#1489) + - Change -- Standardized our use of `begin`/`end` standard C++ iterators (#1551) + - Fix -- CSS reformatting and fixes (#1567) + - Fix -- Add workaround for image and figure captions using latest Markdeep versions (#1583) + - New -- Add DOCTYPE declaration to all Markdeep documents (#1566) + - New -- Add explicit std:: namespacing almost everywhere (#1487) + +### The Next Week + - Delete -- Remove debug output code from `constant_medium::hit()` function (#1495) + - Change -- Convert `perlin` class to use static arrays instead of dynamically allocated (#1483) + - Fix -- Workaround Markdeep issue for code listings with tag-like tokens (#1463) + +### The Rest of Your Life + - Change -- Simplified the `onb` class, and renamed or deleted functions (#1080) + - Change -- Many small updates following walkthrough of book 3 (#988, #1317) + - Change -- Use plain array for `estimate_halfway` program (#1523) + - Change -- Refactored the ONB class to remove unused methods and generally simplify (#1088) + - Change -- Use `ICD(d)` instead of `f(d)` for inverse cumulative distribution for clarity (#1537) + - Fix -- Add missing signature updates for `material::scatter()` functions + - Fix -- Avoid `hittable_list` of lights in book until code is ready (#1318) + + +---------------------------------------------------------------------------------------------------- +# v4.0.0-alpha.2 (2024-04-07) + +This alpha wraps up most of the major changes we expect to make to book 2 for the impending v4.0.0 +release, along with a bunch of updates to the other two books. Since the alpha.1 release last +August, we've been lucky to have onboarded two new contributors: Arman Uguray and Nate Rupsis. +They've been helping out a ton with this release, and Arman is also developing his GPU Ray Tracing +book at the same time! + +This release is a bit faster, thanks to some new BVH optimizations. We've eliminated the negative +radius sphere hack to model hollow spheres, instead accomplishing this with refraction indices. This +eliminates a bunch of places in the code where we had to accomodate this, and probably a bunch of +bugs we still haven't found. We now load texture images in linear color space, fixing a long-running +bug where we were gamma-correcting our textures twice -- you'll notice object texture maps look a +bit darker and less washed out. Refraction text has gotten a bit of an overhaul, and a better +example of total internal reflection. Of course, this also includes a load of small fixes, tweaks, +and improvements. + +Our current plan is to get the final v4.0.0 release out the door by SIGGRAPH 2024, targeting July +28. With that, here are the latest changes since our alpha.1 release: + +### Common + - Delete -- Removed `rtw_stb_image.h` header from book 1 source, as it's unused there. + - Change -- Increase compile warning levels for MSVC, and corrected newly-flagged code. + - Change -- Default to using post-increment everywhere + - Change -- We've removed the few cases where we used C++ default constructors. Instead, we either + require all parameters, or use operator overloading to use default values. + - Change -- For clarity across audiences with broad programming backgrounds, we now use + `double(x)` instead of `static_cast(x)`, and similarly for other types, for + easier readability for non-C++ programmers. + - Change -- The `ray` class constructors no longer use C++ default parameter values + - Change -- Remove pixel sampling knowledge from `write_color()`. This simplifies `write_color()` + to take only the desired output color, and made each phase in color computation easier + to understand. + - Change -- `ray::origin()` and `ray::direction()` getters now return const references, avoiding + unnecessary copies. + - Change -- Cleaned up the use of the `hit_record` class in `material.h` + - Change -- All books now point to the project wiki instead of the in1weekend blog for further + reading links. + - Change -- New BVH optimization splits the bounds according to the longest bounding box + dimension, yielding a 15-20% speedup (#1007) + - Change -- Reversed the ray-sphere direction and calculations throughout equations and code for + all books. This ended up simplifying equations and code in several places (#1191) + - Change -- Pass `vec3`, `point3`, `ray`, and `color` parameters by const reference where + possible (#1250) + - Change -- Changed BVH construction (removed const qualifer for objects vector) so sorting is + done in place, and copying of vector is avoided, yielding better BVH build performance + (#1327, #1388, #1391) + - Change -- Implement hollow spheres using refraction index instead of negative radii. + Additionally, we now block negative radius spheres. This fixes a bunch of corner + cases with inverted spheres (#1420) + - Change -- Refactor pixel subsampling to make the sampling functions simpler and better focused + in scope (#1421) + - Change -- All constructor parameter names now match their member names if assigned directly. C++ + can handle this without ambiguity, and it means we don't have to come up with + alternate names for everything (#1427) + - Change -- `material::scatter()` gets a trivial default implementation (#1455) + - Fix -- Fixed section describing total internal reflection. It turns out that spheres with + refraction index greater than the surrounding atmosphere cannot exhibit total internal + reflection. Changed example to instead model a bubble of air in water, and updated the + rendered images to match (#900) + - Fix -- Fix references from `random_in_hemisphere()` to `random_on_hemisphere()` (#1198) + - Fix -- The `linear_to_gamma()` function has been hardened against negative inputs (#1202) + - Fix -- Fixed default camera look-from and look-at values (#1341) + - Fix -- The `quad` bounding box now considers all four vertices instead of erroneously only + using two (#1402) + - New -- Added PRINTING.md to give information about how to print these books to PDF or paper. + We will also be including PDFs of each book with each new GitHub release going + forward. + +### In One Weekend + - Change -- Update reference to "Fundamentals of Interactive Computer Graphics" to "Computer + Graphics: Principles and Practice". This is the name used by newer editions of the + book. + - Change -- Updated the "Next Steps" section at the end of book 1 (#1209) + - Change -- Update rtweekend.h header introduction and use (#1473) + - Fix -- Fix code listing ordering bug with `lambertian` texture support (#1258) + - New -- Improved help for the very first build and run. + - New -- Define albedo prior to first use (#1430) + +### The Next Week + - Change -- Lots of miscellaneous edits and clarifications to book two as we encountered them. + This also includes various improvements to code listings to provide better context and + address discrepancies between the listings and the actual source code. + - Change -- `perlin::turb()` no longer defaults the value for the depth parameter. + - Change -- AABB automatically pads to mininmum size for any dimension; no longer requires + primitives to call aabb::pad() function. + - Change -- Switch from ray = A + tb to ray = Q + td in AABB text. + - Change -- Update checker scale to 0.32 + - Change -- Refactor AABB class. Renamed `aabb::axis()` to `aabb::axis_interval()`. Minor + refactoring of `aabb::hit()` function. (#927, #1270) + - Change -- Reworked the AABB chapter. Created skippable sections for planar coordinates + derivation (#1236) + - Fix -- Updated book 2 images to match the latest code. + - Fix -- Images loaded for texture mapping are now converted from their original gamma to + linear color space for use. Rendered images are still gamma corrected to 2.0 on + output (#842) + - Fix -- Fix regression in calls to Perlin `turb()` functions with scaled points (these should + be unscaled). (#1286) + - New -- Add section on alternative 2D primitives such as triangle, ellipse and annulus (#1204, + #1205) + +### The Rest of Your Life + - Fix -- Add missing backslash for LaTeX `operatorname` (#1311) + - Fix -- Fix LaTeX functions with underscore (#1330) + + +---------------------------------------------------------------------------------------------------- +# v4.0.0-alpha.1 (2023-08-06) + +It's been quite a while since our last release of v3.2.3 at the end of 2020. For this cycle, we've +tackled a load of significant backlog items, including rewrites of much of our underlying code. As +always, the primary idea isn't to provide the best or most optimal implementation, but instead to put +out simple, sometimes crude first approximations of the main components of writing a ray tracer. + +Highlights include large rewrites and expansions of the book text, a large refactoring of our camera +class, folding `moving_sphere` functionality into `sphere`, adding a new `interval` class for use in +multiple contexts, creating a new general `quad` primitive to replace the old `*_rect` primitives, +and the addressing of hundreds of issues and requested features. The line-item changes below should +give you an idea of v4 includes. + +In order to drive this release to resolution, we're releasing our alpha.1 version to coincide with +the start of SIGGRAPH 2023. We've pretty much finished with book one, though there's a fair amount +left for books two and three. Our plan is to keep crunching for a final v4.0.0 release by the end of +2023. + +Since this is an alpha, we would greatly appreciate any feedback you might have. Let us know by +creating issues up on the GitHub project. + +### Common + - Delete -- `box`, `xy_rect`, `yz_rect`, `xz_rect` classes. These are replaced with new `quad` + primitive (#292, #780, #681) + - Change -- Use `class` instead of `struct` throughout for simpler C++ (#781) + - Change -- Moved all class method definitions inside class definition (#802) + - Change -- Class public/private access labels get consistent two-space indents (#782) + - Change -- Updated classes to use private access for class-private variables (#869) + - Change -- Made our code `inline` clean. We now use `inline` in all header function definitions + to guard against copies in multiple C++ translation units (#803) + - Change -- Retired the `src/common/` directory. Each book now has complete source in one + directory + - Change -- Significant rewrite and expansion of the `camera` class + - Change -- `aabb` class constructor treats two params as extreme points in any orientation (#733) + - Change -- `hittable:hit()` methods use new interval class for ray-t parameter + - Change -- `interval::clamp()` replaces standalone `clamp` utility function + - Change -- `aabb` class uses intervals for each axis (#796) + - Change -- `hittable` member variable `ptr` renamed to `object` + - Change -- General rename of `mat_ptr` to `mat` (material) + - Change -- `hittable::bounding_box()` signature has changed to always return a value (#859) + - Change -- Replaced random vector in `isotropic` with `random_unit_vector` + - Change -- Use std::clog instead of std::cerr to log scanline progress (#935) + - Change -- Updated figures throughout for improved clarity when possible + - Change -- Generated images are now output gamma-corrected rather than in linear space + (#980, #1033) + - Change -- The `camera` class now handles images with width or height of one (#682, #1040) + - Fix -- CSS fix for cases where code listing overflows; change to fit content (#826) + - Fix -- Enabled compiler warnings for MSVC, Clang, GNU. Cleaned up warnings as fit (#865) + - Fix -- Remove redundant `virtual` keyword for methods with `override` (#805) + - Fix -- `rect` hit returning NaNs and infinities (#681) + - Fix -- Add `\mathit` to italic math variables to fix slight kerning issues in equations + (#839) + - Fix -- Fixed issues in Bib(La)TeX entries. + - New -- Introduce new `interval` class used throughout codebase (#777) + - New -- `rtw_image` class for easier image data loading, better texture file search (#807) + - New -- 2D `quad` primitive of arbitrary orientation (#756) + - New -- `box()` utility function returns `hittable_list` of new `quad` primitives (#780) + +### In One Weekend + - Change -- Updated all rendered images in text + - Change -- Significant update to the diffuse reflection section (#696, #992) + - Change -- Updated and clarified text around ray generation and the camera model + - New -- More commentary about the choice between `double` and `float` (#752) + - New -- Software context around the shadow acne listing + +### The Next Week + - Delete -- The `moving_sphere` class is deprecated, and functionality moved to `sphere` (#1125) + - Change -- Rearranged the texture-mapping presentation. The three types (solid, spatial, image) + are now sequenced in that order, and the checker texture presented more explicitly as + an illustration of a spatial texture. + - Change -- Broad rewrite of time management for moving objects, primarily `camera` and + `sphere`, but also impacting the API for `hittable::bounding_box()` (#799) + - Change -- The `sphere` class now includes animation capability originally in `moving_sphere` + (#1125) + - Fix -- Fixed `bvh_node` constructor definition signature (#872) + - Fix -- Fixed scaling for final Perlin noise texture (#896). + - New -- Add listing to use new `bvh_node` class in the `random_spheres` scene (#715). + +### The Rest of Your Life + - Fix -- Added missing functionality for `isotropic` (#664) + - Fix -- Variable `direction` was used without being defined in listing 11 (#831) + - Fix -- Fixed uniform sampling (#934) + + +---------------------------------------------------------------------------------------------------- +# v3.2.3 (2020-12-07) + +### Common + - Change -- Markdeep library URL updated to new location + +### The Next Week + - Fix -- Correct parameter name typo for `bvh_node` constructor parameter `src_objects` + + +---------------------------------------------------------------------------------------------------- +# v3.2.2 (2020-10-31) + +### Common + - Change -- Refactor `sphere::hit()` method to reuse common blocks of code. + - Change -- Improved the explanation and calculation of sphere UV coordinates (#533) + - Fix -- Added `fmin` to book text for `cos_theta` of `refract` (#732) + - Fix -- Standardized naming for ray-t and time parameters (#746) + - Fix -- `random_unit_vector()` was incorrect (#697) + - Fix -- Synchronize text and copies of `hittable.h` + - Fix -- Synchronize copies of `hittable_list.h`, `material.h`, `sphere.h` + +### In One Weekend + - Change -- Wrote brief explanation waving away negative t values in initial normal sphere + - Fix -- Catch cases where `lambertian::scatter()` yields degenerate scatter rays (#619) + - Fix -- Syntax error in listing 58 (Dielectric material class with reflection) (#768) + - Fix -- Correct wording for ray traversal text (#766) + +### The Next Week + - Fix -- Catch cases where `lambertian::scatter()` yields degenerate scatter rays (#619) + +### The Rest of Your Life + - Fix -- Missing `override` keyword for `xz_rect::pdf_value()` and `xz_rect::random()` methods + (#748) + - Fix -- Synchronize book and source for `cornell_box()` function. + - Fix -- Introduction of light code was introduced out of sequence (#738, #740) + - Fix -- `ray_color()` was creating a new light for every ray bounce (#759) + + +---------------------------------------------------------------------------------------------------- +# v3.2.1 (2020-10-03) + +### Common + - Change -- Refactored dielectric class for clarity + - Fix -- Update local Markdeep library (for offline reading) to v1.11. The prior version had + incorrect content (#712) + - Fix -- Image texture destructor should call `STBI_FREE` instead of delete (#734) + +### In One Weekend + - Delete -- Remove premature `cstdlib` include; not needed until we use `rand()` (#687) + - Fix -- Replace old anti-alias result image with before-and-after image (#679) + - Fix -- Listing 29: Added missing `rtweekend.h` include (#691) + - Fix -- Undefined `vup` variable in camera definition (#686) + - Fix -- Listing 51: Add missing `hittable.h`, `rtweekend.h` includes (#693) + - Fix -- Listing 59: ["Full glass material"] Diverged from source + - Fix -- Fix error in citation section (#721) + - Fix -- Listings 33, 39: Add consistent function signature for `trilinear_interp` (#722) + +### The Next Week + - Delete -- Remove unused u,v,w variables in initial `perlin::noise()` function (#684) + - Change -- `bvh_node` no longer reorders the source vector of scene objects; uses local copy + instead (#701) + - Fix -- Listing 5: Neglected to add ray time for metal and dielectric materials (#133) + - Fix -- Listing 15: In `bvh.h`, add missing `hittable_list.h` include (#690) + - Fix -- Listing 33, 34, 38: Change implicit casts to explicit ones (#692) + - Fix -- Listing 40: Change `perlin.h` in the caption to `texture.h` (#698) + - Fix -- Listing 70: Add missing `bvh.h` (#694) + - Fix -- Listing 70 and `main.cc`: Change a fuzz value of a metal sphere to 1.0 which is the + maximum value (#694) + - Fix -- Fix error in citation section (#721) + +### The Rest of Your Life + - Fix -- Fix errors in citation section (#721) + - Fix -- Area equation in section 3.3 Constructing a PDF and nearby text (#735) + - New -- Listing 36: Add missing updates to dielectric class for updating specular in scatter + record + + +---------------------------------------------------------------------------------------------------- +# v3.2.0 (2020-07-18) + +We're still chasing that elusive stable project state where we're mostly done with large changes, +yet we keep finding more and more to tweak and improve. Besides the usual batch of corrections and +small improvements, for this change we plodded through the complete code progression for both books +one and two (_In One Weekend_ and _The Next Week_). This caught a _lot_ of issues (to our dismay), +and allowed us to generate a complete set of new render images for both books, to catch up with all +of the changes we've been making. The end result is that readers should find a significantly better +agreement between the book and their code as they progress, and their renders should also generally +match. + +Besides the new rendered images, we also much improved the image parameters, which were frequently +missing from the previous version, leaving readers to guess at their values, or go to the code to +try to figure out how we created some of the images. In general, our working renders are now 400 +pixels wide, usually 16:9 aspect ratio. We now use an explicit aspect ratio and deduce the image +height and other camera values, so you can tweak your render size just by changing the image width +(instead of updating a bunch of dependent parameters). + +One interesting late change we made was adding explicit C++ `override` labels to subclass methods. +We did this mostly to aid code readers, but were surprised to find that it actually caught a pretty +significant bug hiding in our code (see entry in common changes below). + +You'll also see a new citation section at the end of the books, to encourage uniform citations out +in the world, making it easier for people to refer to and track these books. + +As is typical, though we roughly follow [semantic versioning](https://semver.org/), we're +considering this release a minor change instead of a major one. It's a common reflex, because people +generally have a (misguided) aversion to bumping the major version a lot. We consider it minor +because most of the changes are quite local, some classes get new constructors and any variances +should be quite simple and easy to fix up. Still, one might consider this more properly a major +version bump. + +For our next larger-than-patch release, we're beginning a large revisit of book 3, +_Ray Tracing: The Rest of Your Life_. There's a lot of work to do, and this will likely be a +significant change and improvement. We're hoping that changes to books one and two will be small, +but that's never worked out for us before. Ah, dreams. + +### Common + - Delete -- Vestigial `vec3::write_color()` method (now in color.h) + - Change -- All images and figures renamed to follow more logical convention, using the following + pattern: `{fig,img}-.-.<filetype>` (#495) + - Change -- `main()` function gets organized into image, world, camera, and render chunks + - Change -- Added header guards to the text of all three books whenever a new header file was + introduced, consistent with source code (#645) + - Change -- Added `override` keywords throughout. This keyword marks a subclass method as one that + is intended to override a superclass method. It makes the code a bit easier to + understand, and ensures that your function is actually overriding the method you think + it is. Which is good, because it already caught an existing bug in _The Rest of Your + Life_ source. This change includes commenting out the book 3 `isotropic::scatter()` + method, which was accidentally ignored anyway. (#639, #669) + - Fix -- Found a bug in book 3 source `isotropic::scatter()` method. Commented out, using + default (as it was previously). (#669) + - New -- Added constructors that take `color` arguments in addition to the constructors + taking `shared_ptr<texture>` arguments, simplifying calling code. Applies to + `checker_texture`, `constant_medium`, `diffuse_light`, `lambertian`, and `isotropic` + (#516, #644) + - New -- Each book gets a section of recommended citation examples (#500) + +### In One Weekend + - Change -- Updated all rendered images except for 1.13, 1.14 (#179, #547, #548, #549, #550, #551, + #552, #553, #554, #555, #556, #557, #560, #561, #562, #563, #564, #565, #566) + - Change -- Standard working render width changed to 400 pixels + - Change -- Image 6 is now a before-and-after pair to illustrate antialiasing + - Change -- Listing 48: Refactored material and geometry declarations + - Change -- Listing 52: Refactored assignment of `etai_over_etat` + - Change -- Listing 56: Refactored material declarations + - Change -- Listing 61: Refactored material and geometry declarations + - Fix -- Corrected various missed change highlights in code listings + - Fix -- Listing 7: Added missing `color.h`, `vec3.h` includes + - Fix -- Listing 18: Add missing `double t` member of struct `hit_record` (#428) + - Fix -- Listing 24: Add missing `color.h` include + - Fix -- Listing 30: Add missing `camera.h` include + - Fix -- Listing 42: Don't need to include `ray.h` when using `rtweekend.h` + - Fix -- Listing 48: Add missing `material.h` include + - Fix -- Listing 51: `refract()` function was missing `fabs()` on `sqrt()` argument (#559) + - Fix -- Listing 61: Include updated `cam` declaration, show context w/highlighting + - Fix -- Listing 62: Highlight rename of `camera::get_ray()` parameters to s, t (#616) + - Fix -- Listing 63: Show reverted scene declarations + - Fix -- Listing 68: Show final scene render parameters with highlighting + - Fix -- Rewrote refracted ray perpendicular and parallel components for correctness (#526) + - New -- Listing 50: Show the updated material definitions + +### The Next Week + - Delete -- Deleted the section covering the old `flip_face` class, renumbered images as this + eliminated the rendering with missing Cornell box faces (#270, #482, #661) + - Delete -- Scenes 7 & 9 from the original (`cornell_balls` and `cornell_final`), as these were + not covered in the book. Made the source and book consistent with each other. There + are now a total of eight scenes for the second book (#653, #620) + - Change -- Listing 10: Separate out world & camera definitions in main (#646) + - Change -- Updated most rendered images for book 2: 2.01-2.03, 2.07-2.13, 2.15-2.22. + - Change -- Scenes get custom image parameters (#650) + - Fix -- Reduced code duplication in `dielectric::scatter()` function + - Fix -- "Intance" typo in Chapter 8.1 to "Instance" (#629) + - Fix -- Listing 7: Show reverted viewing parameters from book 1 final scene + - Fix -- Typo in listing caption for filename `moving-sphere.h` + +### The Rest of Your Life + - Change -- Use `vup` for camera, as in other two books + - Fix -- World and camera setup in `main()`, and include full body in book listing (#646) + - New -- `flip_face` moved to book 3, where it's needed for the light source (#661) + + +---------------------------------------------------------------------------------------------------- +# v3.1.2 (2020-06-03) + +### In One Weekend + - Fix -- Correct typo: "Intance Translation" -> "Instance Translation" + - Fix -- Corrected geometry type when computing distance between two points, final scene (#609) + +### The Rest of Your Life + - Fix -- Missing closing parenthesis in listing 10 (#603) + - Fix -- Tiny improvements to the lambertian::scatter() development (#604) + - Fix -- Correct geometry type and unit vector method in `ray_color()`, listing 20 (#606) + - Fix -- Listing 26: alternate `random_double()` switched `distribution`, `generator` (#621) + - Fix -- Listing 28, 30: `light_shape` should have default material, not `0` (#607) + - Fix -- Listing 30: `mixture_pdf` needs `shared_ptr` arguments (#608) + + +---------------------------------------------------------------------------------------------------- +# v3.1.1 (2020-05-16) + +### Common + - Change -- Camera code improvements to make it more robust when any particular value changes. + Also, the code develops in a smoother series of iterations as the book progresses. + (#536) + - Fix -- Refactoring the camera code in v3.1.0 missed updating the viewport to match, resulting + in distorted renders (#536) + +### In One Weekend + - Change -- The C++ `<random>` version of `random_double()` no longer depends on `<functional>` + header. + - Change -- Refactored `random_scene()`. More named intermediate values, sync'ed with source. + (#489) + - Fix -- Camera initialization with explicit up vector (#537) + - Fix -- Changed some text around the camera model and the camera defocus blur model (#536) + +### The Next Week + - Change -- Refactored `random_scene()`. Added more named intermediate values, sync'ed with + version in _In One Weekend_ and with source. Added highlight for update from last + version in book 1. (#489) + - Change -- The C++ `<random>` version of `random_double()` no longer depends on `<functional>` + header. + - Fix -- Added clarification about updating lambertian variables from `color` to `solid_color`. + - Fix -- Corrected for-loop indices (they differed from the version in book 1) in + `random_scene()`. + - Fix -- Introduce "Texture Coordinates for Spheres" in Chapter 4 to support (u,v) coordinates + in `hit_record` (#496) + - Fix -- Small correction: we now use `std::sort` instead of `qsort` (#490) + + +---------------------------------------------------------------------------------------------------- +# v3.1.0 (2020-05-03) + +This minor upgrade adds some fixes and changes that are a bit more than just patches. The text now +has subchapter headings to help readers browse content and get a bit more context. We're introducing +new type aliases `point3` and `color` for `vec3` to better indicate the underlying mathematical +types of parameters and variables. Overall, a bunch of small improvements that we'd recommend +adopting, but may warrant comparison with any current projects. + +### Common + - Change -- Minor change to use new `point3` and `color` type aliases for `vec3` (#422) + - Change -- Renamed `constant_texture` to `solid_color`, add RGB constructor (#452) + - Change -- Moved `vec3::write_color()` method to utility function in `color.h` header (#502) + - Change -- Switch from `ffmin`/`ffmax` to standard `fmin`/`fmax` (#444, #491) + - Change -- Math notation to bold uppercase points, bold lowercase no-barb vectors (#412) + - Change -- Books use Markdeep's image class=pixel for rendered image fidelity (#498) + - Fix -- Include cmath in vec3.h (#501) + - Fix -- Scattered improvements to the text + - New -- Subchapters throughout all three books (#267) + - New -- Add explanation for padding `aarect` in the zero dimension (#488) + +### In One Weekend + - Change -- Define image aspect ratio up front, then image height from that and the image width + - Change -- Default image sizes changed from 200x100 to 384x216 + - Change -- First image size changed to 256x256 + - Fix -- Improve image size and aspect ratio calculation to make size changes easier + - Fix -- Added `t` parameter back into `hit_record` at correct place + - Fix -- Image basic vectors off by one + - Fix -- Update image and size for first PPM image + - Fix -- Update image and size for blue-to-white gradient image + - Fix -- Update image and size for simple red sphere render + - Fix -- Update image and size for sphere with normal-vector coloring + - Fix -- Correct typo in "What's next?" list to rejoin split paragraph on "Lights." Adjust + numbering in rest of list. + +### The Next Week + - Change -- Large rewrite of the `image_texture` class. Now handles image loading too. (#434) + + ---------------------------------------------------------------------------------------------------- # v3.0.2 (2020-04-11) ### Common -- Fix: code styling for source code both inline and in fenced blocks (#430) -- Change: Every book source now includes from a single common acknowledgments document + - Change -- Every book source now includes from a single common acknowledgments document + - Fix -- Code styling for source code both inline and in fenced blocks (#430) -### _In One Weekend_ -- Fix: Correct typo: "consine" to "cosine" +### In One Weekend + - Fix -- Correct typo: "consine" to "cosine" -### _The Next Week_ -- Fix: `shared_ptr` dereference and simplify code in `hittable_list::bounding_box()` (#435) -- Fix: Erroneous en-dash in code block. Replace `–>` with `->` (#439) -- Fix: Introduce `u`,`v` surface coordinates to `hit_record` (#441) -- Fix: Add highlight to new `hittable::bounding_box()` method (#442) +### The Next Week + - Fix -- `shared_ptr` dereference and simplify code in `hittable_list::bounding_box()` (#435) + - Fix -- Erroneous en-dash in code block. Replace `–>` with `->` (#439) + - Fix -- Introduce `u`,`v` surface coordinates to `hit_record` (#441) + - Fix -- Add highlight to new `hittable::bounding_box()` method (#442) -### _The Rest of Your Life_ -- Fix: unitialized variable in first version of `integrate_x_sq.cc` -- Fix: remove unreferenced variables in several sample programs -- Fix: correct program computation of the integral of x^2 (#438) +### The Rest of Your Life + - Fix -- Unitialized variable in first version of `integrate_x_sq.cc` + - Fix -- Remove unreferenced variables in several sample programs + - Fix -- Correct program computation of the integral of x^2 (#438) ---------------------------------------------------------------------------------------------------- # v3.0.1 (2020-03-31) -- Fix: Display rendered images as pixelated instead of smoothed (#179) -- Deleted: delete old README files specific to each book (#410) +### Common + - Delete -- Delete old README files specific to each book (#410) + - Fix -- Display rendered images as pixelated instead of smoothed (#179) -### _In One Weekend_ -- Fix: Remove duplicated text and reword on the camera up vector (#420) +### In One Weekend + - Fix -- Remove duplicated text and reword on the camera up vector (#420) ---------------------------------------------------------------------------------------------------- @@ -53,77 +591,78 @@ Following this release, we expect to switch to a much more incremental approach, patch-level (fix) changes and some minor-level (addition) changes. ### Common to All Project Source -- Change: Default floating-point type changed from `float` to `double` (#150) -- Change: Materials are now referenced with `std::shared_ptr` pointers -- Change: Complete elimination of bare pointers and `new`/`delete` -- Change: `hittable_list` uses `std::vector` plus `std::shared_ptr` pointers -- Change: Header cleanup across the source code (#218, #220) -- Change: Cleaned up standard C++ header use (#19) -- Change: Improved random number generator utilities -- Change: Replace MAXFLOAT with (portable) infinity (#195, #216) -- Change: A _lot_ of code cleanup, refactoring, renaming (#192) -- Change: Disable compile warnings for external `stb_image.h` on Windows -- Change: Replace pi with portable version (#207) -- Change: `ray_color()` function now has max depth passed in, rather than hard-coding it (#143) -- Change: Added `random_in_unit_sphere()`, `random_unit_vector()`, `random_in_hemisphere()` to - vec3.h. Fixed places where we were using one but should have been using another. (#145) -- Change: General rework of the `vec3` header (#153, #156, #215) -- Change: Clarify sphere intersection code, plus slight perf improvement (#113) -- Change: `ray::point_at_parameter()` renamed to `ray::at()` -- Change: Moved `ffmin()`, `ffmax()` from `aabb.h` to `rtweekend.h` -- Change: Move low-level utility functions to more appropriate headers -- Change: `squared_length()` renamed to `length_squared()` -- Change: Update `sphere::hit()` function. -- Change: Refraction variables renamed to match reflection variable names -- Change: Simplify lambertian scatter direction calculation -- New: CMake configuration & build -- New: Added progress output for main programs (#139) -- New: `src/common` directory for code shared across books -- New: Common project-wide header: `src/common/rtweekend.h` -- New: File constants.h with portable math constants (#151) -- New: `vec3::write_color` - provides a robust output method for color data (#93) -- New: `degrees_to_radians()` utility function (#217) -- New: `random_int()`, `random_double()`, and `vec3::random()` utility functions -- New: Added safety value when surface texture has null data -- New: Main programs now define and handle parameterized background color -- Fix: Diffuse PDF computation uses random point _on_ sphere, rather than _inside_ -- Fix: Improve color [0,1] -> [0,255] mapping + - Change -- Default floating-point type changed from `float` to `double` (#150) + - Change -- Materials are now referenced with `std::shared_ptr` pointers + - Change -- Complete elimination of bare pointers and `new`/`delete` + - Change -- `hittable_list` uses `std::vector` plus `std::shared_ptr` pointers + - Change -- Header cleanup across the source code (#218, #220) + - Change -- Cleaned up standard C++ header use (#19) + - Change -- Improved random number generator utilities + - Change -- Replace MAXFLOAT with (portable) infinity (#195, #216) + - Change -- A _lot_ of code cleanup, refactoring, renaming (#192) + - Change -- Disable compile warnings for external `stb_image.h` on Windows + - Change -- Replace pi with portable version (#207) + - Change -- `ray_color()` function now has max depth passed in, rather than hard-coding it (#143) + - Change -- Added `random_in_unit_sphere()`, `random_unit_vector()`, `random_in_hemisphere()` to + vec3.h. Fixed places where we were using one but should have been using another. + (#145) + - Change -- General rework of the `vec3` header (#153, #156, #215) + - Change -- Clarify sphere intersection code, plus slight perf improvement (#113) + - Change -- `ray::point_at_parameter()` renamed to `ray::at()` + - Change -- Moved `ffmin()`, `ffmax()` from `aabb.h` to `rtweekend.h` + - Change -- Move low-level utility functions to more appropriate headers + - Change -- `squared_length()` renamed to `length_squared()` + - Change -- Update `sphere::hit()` function. + - Change -- Refraction variables renamed to match reflection variable names + - Change -- Simplify lambertian scatter direction calculation + - Fix -- Diffuse PDF computation uses random point _on_ sphere, rather than _inside_ + - Fix -- Improve color [0,1] -> [0,255] mapping + - New -- CMake configuration & build + - New -- Added progress output for main programs (#139) + - New -- `src/common` directory for code shared across books + - New -- Common project-wide header: `src/common/rtweekend.h` + - New -- File constants.h with portable math constants (#151) + - New -- `vec3::write_color` - provides a robust output method for color data (#93) + - New -- `degrees_to_radians()` utility function (#217) + - New -- `random_int()`, `random_double()`, and `vec3::random()` utility functions + - New -- Added safety value when surface texture has null data + - New -- Main programs now define and handle parameterized background color ### Common to All Books -- Change: Code in source and in book reformatted to a consistent 96-column line length (#219) -- Change: Lots more highlighting of changed code in books to aid reading -- Change: Math typesetting fixes throughout the books (#13) -- Change: Books now use Markdeep's chapter indirection syntax -- Change: Updated several output images to match code updates -- Change: Books general styling improvements (#197) -- Change: Refactored acknowledgements. These are now moved to and duplicated in each book -- New: Added code listing captions, including source file name, for all books (#238) -- New: Added captions to all figures (#238) -- New: Local copy of `markdeep.min.js` for offline reading -- Fix: Fixed various minor problems in the text - -### _In One Weekend_ -- Change: Reworked Lambertian reflection text (#155) -- Change: Revised the figure for computing a random reflection vector (#142) -- New: Clarified text around the ideal Lambertian distribution (#155) -- New: Additional explanatory text to the dielectric chapter -- New: Image for hemispherical rendering -- New: Image for dealing with front and back faces (#326) -- Fix: Update `ray_color()` code blocks to match current source (#391) - -### _The Next Week_ -- Change: Added proper handling of front vs back face intersection (#270) -- New: "The Next Week" main program added swtich statement for different scenes -- New: "The Next Week" main program now defines all image/camera parameters for each scene -- Fix: Fixed bug in `noise_texture::value()` (#396) -- Fix: Correct first Perlin noise() function in "The Next Week". -- Fix: Fix OCR error in `texture::value()` function (#399) -- Fix: Remove premature declaration of `moving_sphere::bounding_box()` (#405) - -### _The Rest of Your Life_ -- Change: Improved naming of auxilliary programs in _The Rest of Your Life_ source -- Fix: Delete unused variable `p` in main() (#317) -- Deleted: Several unused source files from `src/TheRestOfYourLife` + - Change -- Code in source and in book reformatted to a consistent 96-column line length (#219) + - Change -- Lots more highlighting of changed code in books to aid reading + - Change -- Math typesetting fixes throughout the books (#13) + - Change -- Books now use Markdeep's chapter indirection syntax + - Change -- Updated several output images to match code updates + - Change -- Books general styling improvements (#197) + - Change -- Refactored acknowledgements. These are now moved to and duplicated in each book + - Fix -- Fixed various minor problems in the text + - New -- Added code listing captions, including source file name, for all books (#238) + - New -- Added captions to all figures (#238) + - New -- Local copy of `markdeep.min.js` for offline reading + +### In One Weekend + - Change -- Reworked Lambertian reflection text (#155) + - Change -- Revised the figure for computing a random reflection vector (#142) + - Fix -- Update `ray_color()` code blocks to match current source (#391) + - New -- Clarified text around the ideal Lambertian distribution (#155) + - New -- Additional explanatory text to the dielectric chapter + - New -- Image for hemispherical rendering + - New -- Image for dealing with front and back faces (#326) + +### The Next Week + - Change -- Added proper handling of front vs back face intersection (#270) + - Fix -- Fixed bug in `noise_texture::value()` (#396) + - Fix -- Correct first Perlin noise() function in "The Next Week". + - Fix -- Fix OCR error in `texture::value()` function (#399) + - Fix -- Remove premature declaration of `moving_sphere::bounding_box()` (#405) + - New -- "The Next Week" main program added swtich statement for different scenes + - New -- "The Next Week" main program now defines all image/camera parameters for each scene + +### The Rest of Your Life + - Delete -- Several unused source files from `src/TheRestOfYourLife` + - Change -- Improved naming of auxilliary programs in _The Rest of Your Life_ source + - Fix -- Delete unused variable `p` in main() (#317) ---------------------------------------------------------------------------------------------------- @@ -135,108 +674,109 @@ overhaul to the contents, particularly around source code blocks in the text, ma typesetting and source-code cleanup. ### Common -- Change: Moved existing _InOneWeekend_, _TheNextWeek_, _TheRestOfYourLife_ content to io repo -- Change: Rewrote vec3.h `cross` function for clarity -- New: General release to web -- New: Created single monolithic raytracing.github.io repo -- New: License change to CC0 in COPYING.txt -- New: CHANGELOG.md -- New: CONTRIBUTING.md -- New: COPYING.txt -- New: README.md -- New: raytracing.github.io links to all the three books -- New: CSS for all books -- New: CSS for the print variant of the books -- Fix: All instances of `hitable` have become `hittable` -- Fix: Replaced `drand48()` with portable `random_double` number generation -- Delete: Deprecated existing _InOneWeekend_, _TheNextWeek_, _TheRestOfYourLife_ repos - -### _In One Weekend_ -- Change: README files updated for top level, source, and books -- Change: Text, Chapter 0 Overview has become Chapter 1, all subsequent chapters incremented -- Change: Text, Syntax highlighting of source modifications -- Change: Text, Chapter 3, Reorder include files in code blocks to match src conventions -- Change: Text, Chapter 3, Consistent use of spaces in code blocks -- Change: Text, Chapter 3, Reordered `vec3` class functions to + - * / -- Change: Text, Chapter 4, Reorder include files in code blocks to match src conventions -- Change: Text, Chapter 6, Reorder include files in code blocks to match src conventions -- Change: Text, Chapter 6, Consistent use of spaces in code blocks -- Change: Text, Chapter 7, Consistent use of spaces in code blocks -- Change: Text, Chapter 9, Consistent use of spaces in code blocks -- Change: Text, Chapter 9, Put function signatures and `{` on the same line -- Change: Text, Chapter 10, Consistent use of spaces in code blocks -- Change: Text, Chapter 10, Put function signatures and `{` on the same line -- Change: Text, Chapter 11, Consistent use of spaces in code blocks -- Change: Text, Chapter 13, Put function signatures and `{` on the same line -- New: Markdeep page created for entire body of text -- New: Markdeep MathJax for formulae and equations for body of text -- New: raytracing.github.io/books/RayTracingInOneWeekend.html -- Fix: Text, Chapter 7, Add `#include "random.h"` in code blocks -- Fix: Text, Chapter 10, Added metal fuzziness parameter for initial dielectric -- Fix: Text, Chapter 13, Added metal fuzziness parameter -- Fix: Code, Removed extraneous `;` from `vec3::&operator[]` signature -- Delete: Code, `vec3 p = r.point_at_parameter(2.0);` in main.cc - -### _The Next Week_ -- Change: Text, Chapter 0 Overview has become Chapter 1, all subsequent chapters incremented -- Change: Text, Syntax highlighting of source modifications -- Change: Text, Chapter 2, Consistent use of spaces in code blocks -- Change: Text, Chapter 3, Consistent use of spaces in code blocks -- Change: Text, Chapter 4, Consistent use of spaces in code blocks -- Change: Text, Chapter 5, Consistent use of spaces in code blocks -- Change: Text, Chapter 5, added "texture" to "We can use that texture on some spheres" -- Change: Text, Chapter 7, Consistent use of spaces in code blocks -- Change: Text, Chapter 7, "This is yz and xz" changed to "This is xz and yz" -- Change: Text, Chapter 8, Changed "And the changes to Cornell is" to "... Cornell are" -- Change: Text, Chapter 9, Changed short `if` statements to two lines for Consistency -- Change: Text, Chapter 10, Consistent use of spaces in code blocks -- Change: Code and Text, Chapter 9, cleaned up implementation of `constant_medium::hit` -- Change: Code and Text, Chapter 9, Rewrote debug functionality in `constant_medium::hit` -- New: raytracing.github.io/books/RayTracingTheNextWeek.html -- New: README.md, source README.md -- New: Markdeep page created for entire body of text -- New: Markdeep MathJax created for formulae and equations for body of text -- New: Earth map picture for use in rendering -- Fix: Text, Chapter 2, The `lambertian` class definition now uses `vec3` instead of `texture` -- Fix: Text, Chapter 7, Changed `cornell_box` hittable array size to 5 -- Fix: Code and Text, Chapter 3, Changed `List[0]` to `List[i]` in `hittable_list::bounding_box()` -- Fix: Code and Text, Chapter 3, Replaced `fmax` and `fmin` with `ffmax` and `ffmin` -- Fix: Code, Add missing headers to `constant_medium.h` to fix g++ compiler error - -### _The Rest of Your Life_ -- Change: Text, Chapter 0 Overview has become Chapter 1, all subsequent chapters incremented -- Change: Text, Syntax highlighting of source modifications -- Change: Text, Chapter 2, Reorder include files in code blocks to match src conventions -- Change: Text, Chapter 3, Reorder include files in code blocks to match src conventions -- Change: Text, Chapter 6, Consistent use of spaces in code blocks -- Change: Text, Chapter 6, Consistent use of spaces in code blocks -- Change: Text, Chapter 8, Changed calculation of `a` axis to pseudocode -- Change: Text, Chapter 8, Consistent use of spaces in code blocks -- New: raytracing.github.io/books/RayTracingTheRestOfYourLife.html -- New: README.md, source README.md -- New: Markdeep page created for entire body of text -- New: Markdeep MathJax created for formulae and equations for body of text -- Fix: Text, Chapter order starting from chapter 2 -- Fix: Text, Renamed figures and images to match new chapter numbering -- Fix: Text, Chapter 4, Rewrote formula for "Color" to "Color = A * color(direction" -- Fix: Code and Text, Chapter 6, `material::scattering_pdf` now returns type float -- Fix: Code and Text, Chapter 6, removal of factor of 2 to `random_cosine_direction` calculation + - Delete -- Deprecated existing _InOneWeekend_, _TheNextWeek_, _TheRestOfYourLife_ repos + - Change -- Moved existing _InOneWeekend_, _TheNextWeek_, _TheRestOfYourLife_ content to io repo + - Change -- Rewrote vec3.h `cross` function for clarity + - Fix -- All instances of `hitable` have become `hittable` + - Fix -- Replaced `drand48()` with portable `random_double` number generation + - New -- General release to web + - New -- Created single monolithic raytracing.github.io repo + - New -- License change to CC0 in COPYING.txt + - New -- CHANGELOG.md + - New -- CONTRIBUTING.md + - New -- COPYING.txt + - New -- README.md + - New -- Raytracing.github.io links to all the three books + - New -- CSS for all books + - New -- CSS for the print variant of the books + +### In One Weekend + - Delete -- Code, `vec3 p = r.point_at_parameter(2.0);` in main.cc + - Change -- README files updated for top level, source, and books + - Change -- Text, Chapter 0 Overview has become Chapter 1, all subsequent chapters incremented + - Change -- Text, Syntax highlighting of source modifications + - Change -- Text, Chapter 3, Reorder include files in code blocks to match src conventions + - Change -- Text, Chapter 3, Consistent use of spaces in code blocks + - Change -- Text, Chapter 3, Reordered `vec3` class functions to + - * / + - Change -- Text, Chapter 4, Reorder include files in code blocks to match src conventions + - Change -- Text, Chapter 6, Reorder include files in code blocks to match src conventions + - Change -- Text, Chapter 6, Consistent use of spaces in code blocks + - Change -- Text, Chapter 7, Consistent use of spaces in code blocks + - Change -- Text, Chapter 9, Consistent use of spaces in code blocks + - Change -- Text, Chapter 9, Put function signatures and `{` on the same line + - Change -- Text, Chapter 10, Consistent use of spaces in code blocks + - Change -- Text, Chapter 10, Put function signatures and `{` on the same line + - Change -- Text, Chapter 11, Consistent use of spaces in code blocks + - Change -- Text, Chapter 13, Put function signatures and `{` on the same line + - Fix -- Text, Chapter 7, Add `#include "random.h"` in code blocks + - Fix -- Text, Chapter 10, Added metal fuzziness parameter for initial dielectric + - Fix -- Text, Chapter 13, Added metal fuzziness parameter + - Fix -- Code, Removed extraneous `;` from `vec3::&operator[]` signature + - New -- Markdeep page created for entire body of text + - New -- Markdeep MathJax for formulae and equations for body of text + - New -- Raytracing.github.io/books/RayTracingInOneWeekend.html + +### The Next Week + - Change -- Text, Chapter 0 Overview has become Chapter 1, all subsequent chapters incremented + - Change -- Text, Syntax highlighting of source modifications + - Change -- Text, Chapter 2, Consistent use of spaces in code blocks + - Change -- Text, Chapter 3, Consistent use of spaces in code blocks + - Change -- Text, Chapter 4, Consistent use of spaces in code blocks + - Change -- Text, Chapter 5, Consistent use of spaces in code blocks + - Change -- Text, Chapter 5, added "texture" to "We can use that texture on some spheres" + - Change -- Text, Chapter 7, Consistent use of spaces in code blocks + - Change -- Text, Chapter 7, "This is yz and xz" changed to "This is xz and yz" + - Change -- Text, Chapter 8, Changed "And the changes to Cornell is" to "... Cornell are" + - Change -- Text, Chapter 9, Changed short `if` statements to two lines for Consistency + - Change -- Text, Chapter 10, Consistent use of spaces in code blocks + - Change -- Code and Text, Chapter 9, cleaned up implementation of `constant_medium::hit` + - Change -- Code and Text, Chapter 9, Rewrote debug functionality in `constant_medium::hit` + - Fix -- Text, Chapter 2, The `lambertian` class definition now uses `vec3` instead of `texture` + - Fix -- Text, Chapter 7, Changed `cornell_box` hittable array size to 5 + - Fix -- Code and Text, Chapter 3, Changed `List[0]` to `List[i]` in + `hittable_list::bounding_box()`. + - Fix -- Code and Text, Chapter 3, Replaced `fmax` and `fmin` with `ffmax` and `ffmin` + - Fix -- Code, Add missing headers to `constant_medium.h` to fix g++ compiler error + - New -- Raytracing.github.io/books/RayTracingTheNextWeek.html + - New -- README.md, source README.md + - New -- Markdeep page created for entire body of text + - New -- Markdeep MathJax created for formulae and equations for body of text + - New -- Earth map picture for use in rendering + +### The Rest of Your Life + - Change -- Text, Chapter 0 Overview has become Chapter 1, all subsequent chapters incremented + - Change -- Text, Syntax highlighting of source modifications + - Change -- Text, Chapter 2, Reorder include files in code blocks to match src conventions + - Change -- Text, Chapter 3, Reorder include files in code blocks to match src conventions + - Change -- Text, Chapter 6, Consistent use of spaces in code blocks + - Change -- Text, Chapter 6, Consistent use of spaces in code blocks + - Change -- Text, Chapter 8, Changed calculation of `a` axis to pseudocode + - Change -- Text, Chapter 8, Consistent use of spaces in code blocks + - Fix -- Text, Chapter order starting from chapter 2 + - Fix -- Text, Renamed figures and images to match new chapter numbering + - Fix -- Text, Chapter 4, Rewrote formula for "Color" to "Color = A * color(direction" + - Fix -- Code and Text, Chapter 6, `material::scattering_pdf` now returns type float + - Fix -- Code and Text, Chapter 6, removal of factor of 2 to `random_cosine_direction` + calculation + - New -- Raytracing.github.io/books/RayTracingTheRestOfYourLife.html + - New -- README.md, source README.md + - New -- Markdeep page created for entire body of text + - New -- Markdeep MathJax created for formulae and equations for body of text ---------------------------------------------------------------------------------------------------- # v1.123.0 (2018-08-26) -- New: First GitHub release of _Ray Tracing: The Rest of Your Life_. + - New -- First GitHub release of _Ray Tracing: The Rest of Your Life_. ---------------------------------------------------------------------------------------------------- # v1.54.0 (2018-08-26) -- New: First GitHub release of _Ray Tracing in One Weekend_. + - New -- First GitHub release of _Ray Tracing in One Weekend_. ---------------------------------------------------------------------------------------------------- # v1.42.0 (2018-08-26) -- New: First GitHub release of _Ray Tracing: The Next Week_. - + - New -- First GitHub release of _Ray Tracing: The Next Week_. diff --git a/CMakeLists.txt b/CMakeLists.txt index e736d5bda..8cf3a015c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,80 +4,115 @@ # See README.md for guidance. #--------------------------------------------------------------------------------------------------- -cmake_minimum_required ( VERSION 3.1.0 ) +cmake_minimum_required ( VERSION 3.1.0...4.0.0 ) -project ( RTWeekend - VERSION 3.0.0 - LANGUAGES CXX -) +project ( RTWeekend LANGUAGES CXX ) -# Set to c++11 -set ( CMAKE_CXX_STANDARD 11 ) +# Set to C++11 +set ( CMAKE_CXX_STANDARD 11 ) +set ( CMAKE_CXX_STANDARD_REQUIRED ON ) +set ( CMAKE_CXX_EXTENSIONS OFF ) # Source -set ( COMMON_ALL - src/common/rtweekend.h - src/common/camera.h - src/common/ray.h - src/common/vec3.h + +set ( EXTERNAL + src/external/stb_image.h ) set ( SOURCE_ONE_WEEKEND - ${COMMON_ALL} + src/InOneWeekend/main.cc + src/InOneWeekend/camera.h + src/InOneWeekend/color.h src/InOneWeekend/hittable.h src/InOneWeekend/hittable_list.h + src/InOneWeekend/interval.h src/InOneWeekend/material.h + src/InOneWeekend/ray.h + src/InOneWeekend/rtweekend.h src/InOneWeekend/sphere.h - src/InOneWeekend/main.cc + src/InOneWeekend/vec3.h ) set ( SOURCE_NEXT_WEEK - ${COMMON_ALL} - src/common/aabb.h - src/common/external/stb_image.h - src/common/perlin.h - src/common/rtw_stb_image.h - src/common/texture.h - src/TheNextWeek/aarect.h - src/TheNextWeek/box.h + src/TheNextWeek/main.cc + src/TheNextWeek/aabb.h src/TheNextWeek/bvh.h + src/TheNextWeek/camera.h + src/TheNextWeek/color.h src/TheNextWeek/constant_medium.h src/TheNextWeek/hittable.h src/TheNextWeek/hittable_list.h + src/TheNextWeek/interval.h src/TheNextWeek/material.h - src/TheNextWeek/moving_sphere.h + src/TheNextWeek/perlin.h + src/TheNextWeek/quad.h + src/TheNextWeek/ray.h + src/TheNextWeek/rtw_stb_image.h + src/TheNextWeek/rtweekend.h src/TheNextWeek/sphere.h - src/TheNextWeek/main.cc + src/TheNextWeek/texture.h + src/TheNextWeek/vec3.h ) set ( SOURCE_REST_OF_YOUR_LIFE - ${COMMON_ALL} - src/common/aabb.h - src/common/external/stb_image.h - src/common/perlin.h - src/common/rtw_stb_image.h - src/common/texture.h - src/TheRestOfYourLife/aarect.h - src/TheRestOfYourLife/box.h - src/TheRestOfYourLife/bvh.h + src/TheRestOfYourLife/main.cc + src/TheRestOfYourLife/aabb.h + src/TheRestOfYourLife/camera.h + src/TheRestOfYourLife/color.h + src/TheRestOfYourLife/constant_medium.h src/TheRestOfYourLife/hittable.h src/TheRestOfYourLife/hittable_list.h + src/TheRestOfYourLife/interval.h src/TheRestOfYourLife/material.h src/TheRestOfYourLife/onb.h src/TheRestOfYourLife/pdf.h + src/TheRestOfYourLife/perlin.h + src/TheRestOfYourLife/quad.h + src/TheRestOfYourLife/ray.h + src/TheRestOfYourLife/rtw_stb_image.h + src/TheRestOfYourLife/rtweekend.h src/TheRestOfYourLife/sphere.h - src/TheRestOfYourLife/main.cc + src/TheRestOfYourLife/texture.h + src/TheRestOfYourLife/vec3.h ) -# Executables -add_executable(inOneWeekend ${SOURCE_ONE_WEEKEND}) -add_executable(theNextWeek ${SOURCE_NEXT_WEEK}) -add_executable(theRestOfYourLife ${SOURCE_REST_OF_YOUR_LIFE}) -add_executable(cos_cubed src/TheRestOfYourLife/cos_cubed.cc ${COMMON_ALL}) -add_executable(cos_density src/TheRestOfYourLife/cos_density.cc ${COMMON_ALL}) -add_executable(integrate_x_sq src/TheRestOfYourLife/integrate_x_sq.cc ${COMMON_ALL}) -add_executable(pi src/TheRestOfYourLife/pi.cc ${COMMON_ALL}) -add_executable(sphere_importance src/TheRestOfYourLife/sphere_importance.cc ${COMMON_ALL}) -add_executable(sphere_plot src/TheRestOfYourLife/sphere_plot.cc ${COMMON_ALL}) +include_directories(src) + +# Specific compiler flags below. We're not going to add options for all possible compilers, but if +# you're new to CMake (like we are), the following may be a helpful example if you're using a +# different compiler or want to set different compiler options. -include_directories(src/common) +message (STATUS "Compiler ID: " ${CMAKE_CXX_COMPILER_ID}) +message (STATUS "Release flags: " ${CMAKE_CXX_FLAGS_RELEASE}) +message (STATUS "Debug flags: " ${CMAKE_CXX_FLAGS_DEBUG}) + +if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + # /wd #### - Disable warning + # /we #### - treat warning as error + add_compile_options("/W4") # Enable level-4 warnings + add_compile_options("/we 4265") # Class has virtual functions, but its non-trivial destructor is not virtual + add_compile_options("/we 5204") # Class has virtual functions, but its trivial destructor is not virtual + add_compile_options("/wd 4100") # unreferenced formal parameter +elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + add_compile_options(-Wnon-virtual-dtor) # Class has virtual functions, but its destructor is not virtual + add_compile_options(-Wreorder) # Data member will be initialized after [other] data member + add_compile_options(-Wmaybe-uninitialized) # Variable improperly initialized + add_compile_options(-Wunused-variable) # Variable is defined but unused +elseif (CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wnon-virtual-dtor) # Class has virtual functions, but its destructor is not virtual + add_compile_options(-Wreorder) # Data member will be initialized after [other] data member + add_compile_options(-Wsometimes-uninitialized) # Variable improperly initialized + add_compile_options(-Wunused-variable) # Variable is defined but unused +endif() + +# Executables +add_executable(inOneWeekend ${EXTERNAL} ${SOURCE_ONE_WEEKEND}) +add_executable(theNextWeek ${EXTERNAL} ${SOURCE_NEXT_WEEK}) +add_executable(theRestOfYourLife ${EXTERNAL} ${SOURCE_REST_OF_YOUR_LIFE}) +add_executable(cos_cubed src/TheRestOfYourLife/cos_cubed.cc ) +add_executable(cos_density src/TheRestOfYourLife/cos_density.cc ) +add_executable(integrate_x_sq src/TheRestOfYourLife/integrate_x_sq.cc ) +add_executable(pi src/TheRestOfYourLife/pi.cc ) +add_executable(estimate_halfway src/TheRestOfYourLife/estimate_halfway.cc ) +add_executable(sphere_importance src/TheRestOfYourLife/sphere_importance.cc ) +add_executable(sphere_plot src/TheRestOfYourLife/sphere_plot.cc ) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e310780ae..a4266b206 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,26 +10,13 @@ https://raytracing.github.io/, so we can keep the content up-to-date with the la improvements. -Repository Organization ------------------------- -This repository has the following organization - -```bash -raytracing.github.io/ # Repository root - books/ # Book content - images/ # All figures, renderings and other images used throughout the project - src/ # Source code - common/ # Source code that is common to two or more books - InOneWeekend/ # The final form of source code for Ray Tracing in One Weekend - TheNextWeek/ # The final form of source code for Ray Tracing: The Next Week - TheRestOfYourLife/ # The final form of source code for Ray Tracing: The Rest of Your Life - style/ # CSS for books and web site -``` - -The latest official release can be found in the `master` branch. All ongoing development work (and -all of the latest changes) will be in the `dev-patch`, `dev-minor`, and `dev-major` branches. The -appropriate target branch for any pull requests you want to make will be determined in the -associated issue first (all PRs should have an associated issue). +Development Branches +--------------------- +We use `release` as our release branch. _Generally, changes should never go directly to the release +branch_. All ongoing development work (and all of the latest changes) will be in the `dev-patch`, +`dev-minor`, `dev-major`, or feature branches. The appropriate target branch for any pull requests +you want to make will be determined in the associated issue first (all pull requests should have an +associated issue). Issues @@ -37,35 +24,80 @@ Issues The easiest way to help out is to log any issues you find in the books. Unclear passages, errors of all kinds, even better ways to present something -- just go to the [issues page][]. -**Before creating a new issue**, please review existing issues to see if someone has already -submitted the same one. Odds are you're not the first to encounter something, so a little quick -research can save everyone some hassle. It's also a good idea to verify that problems still exist in -the `development` branch when creating new issues. +1. First ensure that the issue is still outstanding (check `dev-patch`, `dev-minor` or `dev-major` + as appropriate). Often the issue has already been addressed or no longer applies to the latest + in-development version. Admittedly, that's a bit of a hassle, but at least step two should help + you avoid duplicate issues. -When entering a new issue, please include all relevant information. For content issues, include the -book or books this applies to, and specific locations that should be reviewed. Similarly for code: -please include the file, function/class, and line number(s) if that applies. +2. **Before creating a new issue**, please review existing issues to see if someone has already + submitted the same one. Chances are you're not the first to encounter something, so a little + quick research can save everyone some hassle. If you have new information, please continue the + thread in the existing issue. + +3. When entering a new issue, please include all relevant information. For content issues, include + the book or books this applies to, and specific locations that should be reviewed. Similarly for + code: please include the file, function/class, and line number(s) if that applies. + +4. Finally, _please keep issues focused on a single problem or suggestion_. If discussion prompts + you to think of a related issue, create a separate issue for that topic and add a link back to + the original discussion or issue (just use the "#NNN" syntax for issue/discussion/pull-request + NNN -- GitHub will automatically make this a link). Pull Requests -------------- -To contribute a change to the project, please follow these steps: +To contribute a change to the project, *please follow these steps*: + + 1. [Create a new GitHub issue](https://github.com/RayTracing/raytracing.github.io/issues). - 1. [Create a GitHub issue](https://github.com/RayTracing/raytracing.github.io/issues). + 2. Let us know if you're willing to make the fix yourself. - 2. Participate in the discussion as needed. We'll ensure that the work doesn't conflict with or + 3. Participate in the discussion as needed. We'll ensure that the work doesn't conflict with or duplicate other work planned or in progress, and decide which development branch is correct for the release type and release schedule. - 3. Create your changes in a feature branch (or fork) from the assigned development branch - (probably `dev-patch`, `dev-minor`, `dev-major`, or `future`). + 4. Once you've received instructions to proceed with your change, create a new feature branch (or + fork) from the assigned development branch (usually `dev-patch`, `dev-minor`, or `dev-major`). + + 5. Follow the existing code style. + + 6. Ensure that the change is complete: + + - Update all relevant source code for all three books (`src/*`). Since the code is developed as + the books proceed, you may need to update many historical code listings as well, _and this + may require corresponding updates to the book text_. + + - Update all relevant code listings and text in all three books (`books/RayTracing*.html`). + Follow existing style for the Markdeep source (for example, text should be wrapped to 100 + characters). + + - Provide clear and full commit descriptions: title line (50 characters max), followed by a + blank line, and then a descriptive body with lines not exceeding 72 characters. If a commit + is expected to completely resolve an outstanding issue, add a line "Resolves #NNN" to the + bottom of your commit message, where NNN is the existing GitHub issue number. You may provide + multiple such lines if applicable. + + - Include a one-line summary change at the bottom of the current development section in the + changelog (`CHANGELOG.md`). Include a reference to the associated GitHub issue. + + - For an example of the above, see + [issue #1262](https://github.com/RayTracing/raytracing.github.io/issues/1262) and + [PR #1263](https://github.com/RayTracing/raytracing.github.io/pull/1263). + + 7. When ready, create your pull request (PR) and request a review from "RayTracing/reviewers". - 4. Follow existing code style. + 8. Congratulate yourself for having been part of the 1% of contributors who actually read and + followed these guidelines. - 5. When ready, create a pull request and request a review from "rt-contributors". +Note the code philosophy outlined in the first section of the first book. In short, the code is +intended to be clear for everyone, using simple C/C++ idioms as much as possible. We have chosen to +adopt _some_ modern conventions where we feel it makes the code more accessible to non-C++ +programmers. Please follow the existing coding style and simplicity when offering your suggested +changes. -New to GitHub? We'll walk you through the process above. Just mention that you'd like a little -guidance in the proposal issue. +If anything above sounds daunting, note that you'll get _**massive**_ credit for actually reading +the CONTRIBUTING.md and following the process above -- so we'd be delighted to answer any questions +and guide you through the process. Questions diff --git a/PRINTING.md b/PRINTING.md new file mode 100644 index 000000000..f29940b0d --- /dev/null +++ b/PRINTING.md @@ -0,0 +1,32 @@ +Printing These Books +==================================================================================================== + +These books have been formatted to be print friendly (using CSS media queries). That means that you +should be able to print them directly from your browser of choice (usually Ctrl-P on Windows and +Linux, or Cmd-P on Mac). + +We've taken some care to set up sensible page breaks in the printed versions, though you still may +find a few odd artifacts here and there. For example, the latest version of Markdeep can let +images/figures and their captions land on different pages. This issue has been reported, and there +may be a fix in the works for this. + + +Pre-Printed Books +------------------ +I've gone back and created PDFs for each book for versions since v2.0.0. You can find +these in the assets section of each release on the [GitHub releases page][releases]. We will include +PDFs for all books with all future releases. + + +Creating PDFs of These Books +----------------------------- +If you wish to create your own PDF of any of these books (like for a version currently in +development, or to test the results on your own changes), the easiest way is to use a save-to-PDF +print driver. When viewing a book in your browser, issue your browser's print command, and look +through the available destination printers. Hopefully you'll find a save-to-PDF printer already set +up and available. If not, you should be able to search for and find such a print driver for your +particular operating system. + + + +[releases]: https://github.com/RayTracing/raytracing.github.io/releases diff --git a/README.md b/README.md index 846772cd2..391e20762 100644 --- a/README.md +++ b/README.md @@ -9,55 +9,142 @@ Ray Tracing in One Weekend Book Series Getting the Books ------------------ The _Ray Tracing in One Weekend_ series of books are now available to the public for free directly -from the web: +from the web. + +### Version 4.0.1 - [Ray Tracing in One Weekend][web1] - [Ray Tracing: The Next Week][web2] - [Ray Tracing: The Rest of Your Life][web3] -These books have been formatted for both screen and print. For printed copies, or to create PDF -versions, use the print function in your browser. +These books have been formatted for both screen and print. For more information about printing your +own copies, or on getting PDFs of the books, see [PRINTING.md][] for more information. -News ------ -2020-March-23 — v3.0.0 is out! Following the major v2.0.0 release, we finally had the chance to dig -into some of the larger changes we'd been wanting to make for a long time. This is a large change -across the books and entire source code. The new source code now builds using CMake, for most -platforms and build systems. We've refactored a good deal of the project source, cleaning things up, -refactoring, adding new functionality where it made sense, and generally trying to simplify the code -with the goal of making it easier to understand and modify. Finally, this release includes a number -of changes to areas of the book that have given readers difficulties. Enjoy! +Contributing +------------- +If you'd like to contribute a PR _**please read our [contribution guidelines][CONTRIBUTING] +first**_. -Branches ---------- -The `master` branch contains the code at latest release. All ongoing development, with all of the -latest changes, can be found in the `dev-patch`, `dev-minor`, and `dev-major` branches. +Project Status +--------------- +If you'd like to check out the latest updates and watch our progress, we're on the `dev-patch`, +`dev-minor`, and `dev-major` branches. You can also browse our issues and milestones to see what +we're planning. +If you're interested in contributing, email us! You can find our contact info at the head of each +book. Or just start [a new discussion][discussions] or [issue][issues]. -Downloading The Source Code ----------------------------- + +GitHub Discussions +------------------ +Do you have general questions about raytracing code, issues with your own implmentation, or general +raytracing ideas you'd like to share? Check out our [GitHub discussions][discussions] forum! + + +Directory Structure +------------------- +The organization of this repository is meant to be simple and self-evident at a glance: + + - `books/` -- + This folder contains the three raytracing books (in HTML), and some supporting material. + + - `images/` -- + Contains all of the images and figures of the books. Can also be used to compare your + results. + + - `style/` -- + Contains the css for the books and the site. + + - `src/` -- + Contains the source. + + - `src/<book>/` -- + Contains the final source code for each book. + + +Source Code +----------- +### Intent +This repository is not meant to act as its own tutorial. The source is provided so you can compare +your work when progressing through the book. We strongly recommend reading and following along with +the book to understand the source. Ideally, you'll be developing your own implementation as you go, +in order to deeply understand how a raytracer works. + +### Downloading The Source Code The [GitHub home][] for this project contains all source and documentation associated with the _Ray -Tracing in One Weekend_ series of books. To clone or download the source code, see the green "Clone -or download" button in the upper right of the project home page. +Tracing in One Weekend_ book series. To clone or download the source code, see the green "Clone or +download" button in the upper right of the project home page. + +### Programming Language +This book is written in C++, and uses some modern features of C++11. The language and features were +chosen to be broadly understood by the largest collection of programmers. It is not meant to +represent ideal (or optimized) C++ code. + +### Implementations in Other Languages +The _Ray Tracing in One Weekend_ series has a long history of implementations in other programming +languages (see [Implementations in Other Languages][implementations]), and across different +operating systems. Feel free to add your own implementation to the list! + +### Branches +In general, ongoing development, with all of the latest changes, can be found in the `dev-patch`, +`dev-minor`, and `dev-major` branches, minor and major changes, depending on the change level and +release in progress. We try to keep CHANGELOG.md up to date, so you can easily browse what's new in +each development branch. We may from time to time use additional development branches, so stay up to +date by reviewing the [CONTRIBUTING][] page. + +The `release` branch contains the latest released (and live) assets. This is the branch from which +GitHub pages serves up https://raytracing.github.io/. Building and Running --------------------- -Copies of source are provided for you to check your work and compare against. If you wish to build -the provided source, the project uses CMake. At the root of the project directory, run the following -commands to build the debug version of every executable: +Copies of the source are provided for you to check your work and compare against. If you wish to +build the provided source, this project uses CMake. To build, go to the root of the project +directory and run the following commands to create the debug version of every executable: $ cmake -B build $ cmake --build build +You should run `cmake -B build` whenever you change your project `CMakeLists.txt` file (like when +adding a new source file). + You can specify the target with the `--target <program>` option, where the program may be `inOneWeekend`, `theNextWeek`, `theRestOfYourLife`, or any of the demonstration programs. By default (with no `--target` option), CMake will build all targets. -On Windows, you can build either `debug` (the default) or `release` (the optimized version). To -specify this, use the `--config <debug|release>` option. + $ cmake --build build --target inOneWeekend + +### Optimized Builds +CMake supports Release and Debug configurations. These require slightly different invocations +across Windows (MSVC) and Linux/macOS (using GCC or Clang). The following instructions will place +optimized binaries under `build/Release` and debug binaries (unoptimized and containing debug +symbols) under `build/Debug`: + +On Windows: + +```shell +$ cmake -B build +$ cmake --build build --config Release # Create release binaries in `build\Release` +$ cmake --build build --config Debug # Create debug binaries in `build\Debug` +``` + +On Linux / macOS: + +```shell +# Configure and build release binaries under `build/Release` +$ cmake -B build/Release -DCMAKE_BUILD_TYPE=Release +$ cmake --build build/Release + +# Configure and build debug binaries under `build/Debug` +$ cmake -B build/Debug -DCMAKE_BUILD_TYPE=Debug +$ cmake --build build/Debug +``` + +We recommend building and running the `Release` version (especially before the final render) for +the fastest results, unless you need the extra debug information provided by the (default) debug +build. ### CMake GUI on Windows You may choose to use the CMake GUI when building on windows. @@ -67,7 +154,7 @@ You may choose to use the CMake GUI when building on windows. `C:\Users\Peter\raytracing.github.io`. 3. Add the folder "build" within the location of the copied directory. For example, `C:\Users\Peter\raytracing.github.io\build`. -4. For "Where to build the binaries", set this to the newly-created build directory. +4. For "Where to build the binaries", set this to the newly-created "build" directory. 5. Click "Configure". 6. For "Specify the generator for this project", set this to your version of Visual Studio. 7. Click "Done". @@ -81,17 +168,13 @@ operating system to simply print the image to file. ### Running The Programs -On Linux or OSX, from the terminal, run like this: - - $ build/inOneWeekend > image.ppm - -On Windows, run like this: +You can run the programs by executing the binaries placed in the build directory: - build\debug\inOneWeekend > image.ppm + $ build\Debug\inOneWeekend > image.ppm -or, run the optimized version (if you've built with `--config release`): +or, run the optimized version (if you compiled with the release configuration): - build\release\inOneWeekend > image.ppm + $ build\Release\inOneWeekend > image.ppm The generated PPM file can be viewed directly as a regular computer image, if your operating system supports this image type. If your system doesn't handle PPM files, then you should be able to find @@ -100,20 +183,24 @@ PPM file viewers online. We like [ImageMagick][]. Corrections & Contributions ---------------------------- -If you spot errors, have suggested corrections, or would like to help out with the project, please -review the [CONTRIBUTING][] document for the most effective way to proceed. - - - -[book1]: books/RayTracingInOneWeekend.html -[book2]: books/RayTracingTheNextWeek.html -[book3]: books/RayTracingTheRestOfYourLife.html -[CONTRIBUTING]: ./CONTRIBUTING.md -[cover1]: images/RTOneWeekend-small.jpg -[cover2]: images/RTNextWeek-small.jpg -[cover3]: images/RTRestOfYourLife-small.jpg -[GitHub home]: https://github.com/RayTracing/raytracing.github.io/ -[ImageMagick]: https://imagemagick.org/ -[web1]: https://raytracing.github.io/books/RayTracingInOneWeekend.html -[web2]: https://raytracing.github.io/books/RayTracingTheNextWeek.html -[web3]: https://raytracing.github.io/books/RayTracingTheRestOfYourLife.html +If you spot errors, have suggested corrections, or would like to help out with the project, +_**please review the [CONTRIBUTING][] document for the most effective way to proceed.**_ + + + +[book1]: books/RayTracingInOneWeekend.html +[book2]: books/RayTracingTheNextWeek.html +[book3]: books/RayTracingTheRestOfYourLife.html +[CONTRIBUTING]: CONTRIBUTING.md +[cover1]: images/cover/CoverRTW1-small.jpg +[cover2]: images/cover/CoverRTW2-small.jpg +[cover3]: images/cover/CoverRTW3-small.jpg +[discussions]: https://github.com/RayTracing/raytracing.github.io/discussions/ +[GitHub home]: https://github.com/RayTracing/raytracing.github.io/ +[ImageMagick]: https://imagemagick.org/ +[implementations]: https://github.com/RayTracing/raytracing.github.io/wiki/Implementations +[issues]: https://github.com/RayTracing/raytracing.github.io/issues/ +[PRINTING.md]: PRINTING.md +[web1]: https://raytracing.github.io/books/RayTracingInOneWeekend.html +[web2]: https://raytracing.github.io/books/RayTracingTheNextWeek.html +[web3]: https://raytracing.github.io/books/RayTracingTheRestOfYourLife.html diff --git a/books/RayTracingInOneWeekend.html b/books/RayTracingInOneWeekend.html index d9777f436..3344a4223 100644 --- a/books/RayTracingInOneWeekend.html +++ b/books/RayTracingInOneWeekend.html @@ -1,21 +1,21 @@ +<!DOCTYPE html> <meta charset="utf-8"> +<link rel="icon" type="image/png" href="../favicon.png"> <!-- Markdeep: https://casual-effects.com/markdeep/ --> **Ray Tracing in One Weekend** - Peter Shirley - edited by Steve Hollasch and Trevor David Black + [Peter Shirley][], [Trevor David Black][], [Steve Hollasch][] <br> - Version 3.0.2, 2020-04-11 + Version 4.0.2, 2025-04-25 <br> - Copyright 2018-2020 Peter Shirley. All rights reserved. + Copyright 2018-2024 Peter Shirley. All rights reserved. Overview ==================================================================================================== - I’ve taught many graphics classes over the years. Often I do them in ray tracing, because you are forced to write all the code, but you can still get cool images with no API. I decided to adapt my course notes into a how-to, to get you to a cool program as quickly as possible. It will not be a @@ -32,26 +32,57 @@ in a weekend. If you take longer, don’t worry about it. I use C++ as the driving language, but you don’t need to. However, I suggest you do, because it’s fast, portable, and most production movie and video game renderers are written in C++. Note that I avoid most “modern features” of C++, but -inheritance and operator overloading are too useful for ray tracers to pass on. I do not provide the -code online, but the code is real and I show all of it except for a few straightforward operators in -the `vec3` class. I am a big believer in typing in code to learn it, but when code is available I -use it, so I only practice what I preach when the code is not available. So don’t ask! +inheritance and operator overloading are too useful for ray tracers to pass on. + +> I do not provide the code online, but the code is real and I show all of it except for a few +> straightforward operators in the `vec3` class. I am a big believer in typing in code to learn it, +> but when code is available I use it, so I only practice what I preach when the code is not +> available. So don’t ask! I have left that last part in because it is funny what a 180 I have done. Several readers ended up -with subtle errors that were helped when we compared code. So please do type in the code, but if you -want to look at mine it is at: +with subtle errors that were helped when we compared code. So please do type in the code, but you +can find the finished source for each book in the [RayTracing project on GitHub][repo]. + +A note on the implementing code for these books -- our philosophy for the included code prioritizes +the following goals: + + - The code should implement the concepts covered in the books. -https://github.com/RayTracing/raytracing.github.io/ + - We use C++, but as simple as possible. Our programming style is very C-like, but we take + advantage of modern features where it makes the code easier to use or understand. -I assume a little bit of familiarity with vectors (like dot product and vector addition). If you + - Our coding style continues the style established from the original books as much as possible, + for continuity. + + - Line length is kept to 96 characters per line, to keep lines consistent between the codebase and + code listings in the books. + +The code thus provides a baseline implementation, with tons of improvements left for the reader to +enjoy. There are endless ways one can optimize and modernize the code; we prioritize the simple +solution. + +We assume a little bit of familiarity with vectors (like dot product and vector addition). If you don’t know that, do a little review. If you need that review, or to learn it for the first time, -check out Marschner’s and my graphics text, Foley, Van Dam, _et al._, or McGuire’s graphics codex. +check out the online [_Graphics Codex_][gfx-codex] by Morgan McGuire, _Fundamentals of Computer +Graphics_ by Steve Marschner and Peter Shirley, or _Computer Graphics: Principles and Practice_ +by J.D. Foley and Andy Van Dam. + +See the [project README][readme] file for information about this project, the repository on GitHub, +directory structure, building & running, and how to make or reference corrections and contributions. -If you run into trouble, or do something cool you’d like to show somebody, send me some email at -ptrshrl@gmail.com. +See [our Further Reading wiki page][wiki-further] for additional project related resources. -I’ll be maintaining a site related to the book including further reading and links to resources at a -blog https://in1weekend.blogspot.com/ related to this book. +These books have been formatted to print well directly from your browser. We also include PDFs of +each book [with each release][releases], in the "Assets" section. + +If you want to communicate with us, feel free to send us an email at: + + - Peter Shirley, ptrshrl@gmail.com + - Steve Hollasch, steve@hollasch.net + - Trevor David Black, trevordblack@trevord.black + +Finally, if you run into problems with your implementation, have general questions, or would like to +share your own ideas or work, see [the GitHub Discussions forum][discussions] on the GitHub project. Thanks to everyone who lent a hand on this project. You can find them in the acknowledgments section at the end of this book. @@ -63,11 +94,13 @@ Output an Image ==================================================================================================== +The PPM Image Format +--------------------- Whenever you start a renderer, you need a way to see an image. The most straightforward way is to write it to a file. The catch is, there are so many formats. Many of those are complex. I always start with a plain text ppm file. Here’s a nice description from Wikipedia: - ![](../images/img.ppm-example.jpg) + ![Figure [ppm]: PPM Example](../images/fig-1.01-ppm.jpg) <div class='together'> Let’s make some C++ code to output such a thing: @@ -76,92 +109,141 @@ #include <iostream> int main() { - const int image_width = 200; - const int image_height = 100; + + // Image + + int image_width = 256; + int image_height = 256; + + // Render std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n"; - for (int j = image_height-1; j >= 0; --j) { - for (int i = 0; i < image_width; ++i) { - auto r = double(i) / image_width; - auto g = double(j) / image_height; - auto b = 0.2; - int ir = static_cast<int>(255.999 * r); - int ig = static_cast<int>(255.999 * g); - int ib = static_cast<int>(255.999 * b); + for (int j = 0; j < image_height; j++) { + for (int i = 0; i < image_width; i++) { + auto r = double(i) / (image_width-1); + auto g = double(j) / (image_height-1); + auto b = 0.0; + + int ir = int(255.999 * r); + int ig = int(255.999 * g); + int ib = int(255.999 * b); + std::cout << ir << ' ' << ig << ' ' << ib << '\n'; } } } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [main-initial]: <kbd>[main.cc]</kbd> Creating your first image] + </div> -There are some things to note in that code: +There are some things to note in this code: - 1. The pixels are written out in rows with pixels left to right. + 1. The pixels are written out in rows. - 2. The rows are written out from top to bottom. + 2. Every row of pixels is written out left to right. - 3. By convention, each of the red/green/blue components range from 0.0 to 1.0. We will relax that - later when we internally use high dynamic range, but before output we will tone map to the zero - to one range, so this code won’t change. + 3. These rows are written out from top to bottom. - 4. Red goes from black to fully on from left to right, and green goes from black at the bottom to - fully on at the top. Red and green together make yellow so we should expect the upper right - corner to be yellow. + 4. By convention, each of the red/green/blue components are represented internally by real-valued + variables that range from 0.0 to 1.0. These must be scaled to integer values between 0 and 255 + before we print them out. -<div class='together'> -Because the file is written to the program output, you'll need to redirect it to an image file. -Typically this is done from the command-line by using the `>` redirection operator, like so: + 5. Red goes from fully off (black) to fully on (bright red) from left to right, and green goes + from fully off at the top (black) to fully on at the bottom (bright green). Adding red and + green light together make yellow so we should expect the bottom right corner to be yellow. + + +Creating an Image File +----------------------- +Because the file is written to the standard output stream, you'll need to redirect it to an image +file. Typically this is done from the command-line by using the `>` redirection operator. + +On Windows, you'd get the debug build from CMake running this command: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - build\Release\inOneWeekend.exe > image.ppm + cmake -B build + cmake --build build ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This is how things would look on Windows. On Mac or Linux, it would look like this: +Then run your newly-built program like so: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - build/inOneWeekend > image.ppm + build\Debug\inOneWeekend.exe > image.ppm ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -</div> -<div class='together'> -Opening the output file (in `ToyViewer` on my Mac, but try it in your favorite viewer and Google -“ppm viewer” if your viewer doesn’t support it) shows this result: +Later, it will be better to run optimized builds for speed. In that case, you would build like this: - <div class="render"> + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + cmake --build build --config release + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - ![First PPM image](../images/img.first-ppm-image.png) +and would run the optimized program like this: - </div> + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + build\Release\inOneWeekend.exe > image.ppm + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -</div> +The examples above assume that you are building with CMake, using the same approach as the +`CMakeLists.txt` file in the included source. Use whatever build environment (and language) you're +most comfortable with. + +On Mac or Linux, release build, you would launch the program like this: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + build/inOneWeekend > image.ppm + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Complete building and running instructions can be found in the [project README][readme]. + +Opening the output file (in `ToyViewer` on my Mac, but try it in your favorite image viewer and +Google “ppm viewer” if your viewer doesn’t support it) shows this result: + + ![<span class='num'>Image 1:</span> First PPM image + ](../images/img-1.01-first-ppm-image.png class='pixel') -<div class='together'> Hooray! This is the graphics “hello world”. If your image doesn’t look like that, open the output file in a text editor and see what it looks like. It should start something like this: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ P3 - 200 100 + 256 256 255 - 0 253 51 - 1 253 51 - 2 253 51 - 3 253 51 - 5 253 51 - 6 253 51 - 7 253 51 - 8 253 51 + 0 0 0 + 1 0 0 + 2 0 0 + 3 0 0 + 4 0 0 + 5 0 0 + 6 0 0 + 7 0 0 + 8 0 0 + 9 0 0 + 10 0 0 + 11 0 0 + 12 0 0 + ... ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [first-img]: First image output] -</div> -If it doesn’t, then you probably just have some newlines or something similar that is confusing the -image reader. +If your PPM file doesn't look like this, then double-check your formatting code. If it _does_ look +like this but fails to render, then you may have line-ending differences or something similar that +is confusing your image viewer. To help debug this, you can find a file `test.ppm` in the `images` +directory of the Github project. This should help to ensure that your viewer can handle the PPM +format and to use as a comparison against your generated PPM file. + +Some readers have reported problems viewing their generated files on Windows. In this case, the +problem is often that the PPM is written out as UTF-16, often from PowerShell. If you run into this +problem, see [Discussion 1114](https://github.com/RayTracing/raytracing.github.io/discussions/1114) +for help with this issue. + +If everything displays correctly, then you're pretty much done with system and IDE issues -- +everything in the remainder of this series uses this same simple mechanism for generated rendered +images. -If you want to produce more image types than PPM, I am a fan of `stb_image.h` available on github. +If you want to produce other image formats, I am a fan of `stb_image.h`, a header-only image library +available on GitHub at <https://github.com/nothings/stb>. Adding a Progress Indicator @@ -172,203 +254,257 @@ <div class='together'> Our program outputs the image to the standard output stream (`std::cout`), so leave that alone and -instead write to the error output stream (`std::cerr`): +instead write to the logging output stream (`std::clog`): ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - for (int j = image_height-1; j >= 0; --j) { + for (int j = 0; j < image_height; ++j) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - std::cerr << "\rScanlines remaining: " << j << ' ' << std::flush; + std::clog << "\rScanlines remaining: " << (image_height - j) << ' ' << std::flush; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - for (int i = 0; i < image_width; ++i) { - auto r = double(i) / image_width; - auto g = double(j) / image_height; - auto b = 0.2; - int ir = static_cast<int>(255.999 * r); - int ig = static_cast<int>(255.999 * g); - int ib = static_cast<int>(255.999 * b); + for (int i = 0; i < image_width; i++) { + auto r = double(i) / (image_width-1); + auto g = double(j) / (image_height-1); + auto b = 0.0; + + int ir = int(255.999 * r); + int ig = int(255.999 * g); + int ib = int(255.999 * b); + std::cout << ir << ' ' << ig << ' ' << ib << '\n'; } } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - std::cerr << "\nDone.\n"; + std::clog << "\rDone. \n"; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [main-progress]: <kbd>[main.cc]</kbd> Main render loop with progress reporting] + </div> +Now when running, you'll see a running count of the number of scanlines remaining. Hopefully this +runs so fast that you don't even see it! Don't worry -- you'll have lots of time in the future to +watch a slowly updating progress line as we expand our ray tracer. The vec3 Class ==================================================================================================== - Almost all graphics programs have some class(es) for storing geometric vectors and colors. In many -systems these vectors are 4D (3D plus a homogeneous coordinate for geometry, and RGB plus an alpha -transparency channel for colors). For our purposes, three coordinates suffices. We’ll use the same -class `vec3` for colors, locations, directions, offsets, whatever. Some people don’t like this -because it doesn’t prevent you from doing something silly, like adding a color to a location. They -have a good point, but we’re going to always take the “less code” route when not obviously wrong. - -<div class='together'> -Here’s the top part of my `vec3` class: +systems these vectors are 4D (3D position plus a homogeneous coordinate for geometry, or RGB plus an +alpha transparency component for colors). For our purposes, three coordinates suffice. We’ll use the +same class `vec3` for colors, locations, directions, offsets, whatever. Some people don’t like this +because it doesn’t prevent you from doing something silly, like subtracting a position from a color. +They have a good point, but we’re going to always take the “less code” route when not obviously +wrong. In spite of this, we do declare two aliases for `vec3`: `point3` and `color`. Since these two +types are just aliases for `vec3`, you won't get warnings if you pass a `color` to a function +expecting a `point3`, and nothing is stopping you from adding a `point3` to a `color`, but it makes +the code a little bit easier to read and to understand. + +We define the `vec3` class in the top half of a new `vec3.h` header file, and define a set of useful +vector utility functions in the bottom half: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #ifndef VEC3_H + #define VEC3_H + + #include <cmath> #include <iostream> class vec3 { - public: - vec3() : e{0,0,0} {} - vec3(double e0, double e1, double e2) : e{e0, e1, e2} {} - - double x() const { return e[0]; } - double y() const { return e[1]; } - double z() const { return e[2]; } - - vec3 operator-() const { return vec3(-e[0], -e[1], -e[2]); } - double operator[](int i) const { return e[i]; } - double& operator[](int i) { return e[i]; } - - vec3& operator+=(const vec3 &v) { - e[0] += v.e[0]; - e[1] += v.e[1]; - e[2] += v.e[2]; - return *this; - } + public: + double e[3]; - vec3& operator*=(const double t) { - e[0] *= t; - e[1] *= t; - e[2] *= t; - return *this; - } + vec3() : e{0,0,0} {} + vec3(double e0, double e1, double e2) : e{e0, e1, e2} {} - vec3& operator/=(const double t) { - return *this *= 1/t; - } + double x() const { return e[0]; } + double y() const { return e[1]; } + double z() const { return e[2]; } - double length() const { - return sqrt(length_squared()); - } + vec3 operator-() const { return vec3(-e[0], -e[1], -e[2]); } + double operator[](int i) const { return e[i]; } + double& operator[](int i) { return e[i]; } - double length_squared() const { - return e[0]*e[0] + e[1]*e[1] + e[2]*e[2]; - } + vec3& operator+=(const vec3& v) { + e[0] += v.e[0]; + e[1] += v.e[1]; + e[2] += v.e[2]; + return *this; + } - void write_color(std::ostream &out) { - // Write the translated [0,255] value of each color component. - out << static_cast<int>(255.999 * e[0]) << ' ' - << static_cast<int>(255.999 * e[1]) << ' ' - << static_cast<int>(255.999 * e[2]) << '\n'; - } + vec3& operator*=(double t) { + e[0] *= t; + e[1] *= t; + e[2] *= t; + return *this; + } + + vec3& operator/=(double t) { + return *this *= 1/t; + } - public: - double e[3]; + double length() const { + return std::sqrt(length_squared()); + } + + double length_squared() const { + return e[0]*e[0] + e[1]*e[1] + e[2]*e[2]; + } }; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [vec3-class]: <kbd>[vec3.h]</kbd> `vec3` class] -</div> -We use `double` here, but some ray tracers use `float`. Either one is fine -- follow your own -tastes. The second part of the header file contains vector utility functions: + // point3 is just an alias for vec3, but useful for geometric clarity in the code. + using point3 = vec3; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - // vec3 Utility Functions - inline std::ostream& operator<<(std::ostream &out, const vec3 &v) { + // Vector Utility Functions + + inline std::ostream& operator<<(std::ostream& out, const vec3& v) { return out << v.e[0] << ' ' << v.e[1] << ' ' << v.e[2]; } - inline vec3 operator+(const vec3 &u, const vec3 &v) { + inline vec3 operator+(const vec3& u, const vec3& v) { return vec3(u.e[0] + v.e[0], u.e[1] + v.e[1], u.e[2] + v.e[2]); } - inline vec3 operator-(const vec3 &u, const vec3 &v) { + inline vec3 operator-(const vec3& u, const vec3& v) { return vec3(u.e[0] - v.e[0], u.e[1] - v.e[1], u.e[2] - v.e[2]); } - inline vec3 operator*(const vec3 &u, const vec3 &v) { + inline vec3 operator*(const vec3& u, const vec3& v) { return vec3(u.e[0] * v.e[0], u.e[1] * v.e[1], u.e[2] * v.e[2]); } - inline vec3 operator*(double t, const vec3 &v) { + inline vec3 operator*(double t, const vec3& v) { return vec3(t*v.e[0], t*v.e[1], t*v.e[2]); } - inline vec3 operator*(const vec3 &v, double t) { + inline vec3 operator*(const vec3& v, double t) { return t * v; } - inline vec3 operator/(vec3 v, double t) { + inline vec3 operator/(const vec3& v, double t) { return (1/t) * v; } - inline double dot(const vec3 &u, const vec3 &v) { + inline double dot(const vec3& u, const vec3& v) { return u.e[0] * v.e[0] + u.e[1] * v.e[1] + u.e[2] * v.e[2]; } - inline vec3 cross(const vec3 &u, const vec3 &v) { + inline vec3 cross(const vec3& u, const vec3& v) { return vec3(u.e[1] * v.e[2] - u.e[2] * v.e[1], u.e[2] * v.e[0] - u.e[0] * v.e[2], u.e[0] * v.e[1] - u.e[1] * v.e[0]); } - inline vec3 unit_vector(vec3 v) { + inline vec3 unit_vector(const vec3& v) { return v / v.length(); } + + #endif ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [vec3-utility]: <kbd>[vec3.h]</kbd> vec3 utility functions] + [Listing [vec3-class]: <kbd>[vec3.h]</kbd> vec3 definitions and helper functions] -<div class='together'> -Now we can change our main to use this: +We use `double` here, but some ray tracers use `float`. `double` has greater precision and range, +but is twice the size compared to `float`. This increase in size may be important if you're +programming in limited memory conditions (such as hardware shaders). Either one is fine -- follow +your own tastes. + + +Color Utility Functions +------------------------ +Using our new `vec3` class, we'll create a new `color.h` header file and define a utility function +that writes a single pixel's color out to the standard output stream. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #ifndef COLOR_H + #define COLOR_H + + #include "vec3.h" + + #include <iostream> + + using color = vec3; + + void write_color(std::ostream& out, const color& pixel_color) { + auto r = pixel_color.x(); + auto g = pixel_color.y(); + auto b = pixel_color.z(); + + // Translate the [0,1] component values to the byte range [0,255]. + int rbyte = int(255.999 * r); + int gbyte = int(255.999 * g); + int bbyte = int(255.999 * b); + + // Write out the pixel color components. + out << rbyte << ' ' << gbyte << ' ' << bbyte << '\n'; + } + + #endif + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [color]: <kbd>[color.h]</kbd> color utility functions] + +<div class='together'> +Now we can change our main to use both of these: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + #include "color.h" #include "vec3.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ #include <iostream> int main() { - const int image_width = 200; - const int image_height = 100; + + // Image + + int image_width = 256; + int image_height = 256; + + // Render std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n"; - for (int j = image_height-1; j >= 0; --j) { - std::cerr << "\rScanlines remaining: " << j << ' ' << std::flush; - for (int i = 0; i < image_width; ++i) { + for (int j = 0; j < image_height; j++) { + std::clog << "\rScanlines remaining: " << (image_height - j) << ' ' << std::flush; + for (int i = 0; i < image_width; i++) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - vec3 color(double(i)/image_width, double(j)/image_height, 0.2); - color.write_color(std::cout); + auto pixel_color = color(double(i)/(image_width-1), double(j)/(image_height-1), 0); + write_color(std::cout, pixel_color); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ } } - std::cerr << "\nDone.\n"; + std::clog << "\rDone. \n"; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [main-gradient]: <kbd>[main.cc]</kbd> Creating a color gradient image] + [Listing [ppm-2]: <kbd>[main.cc]</kbd> Final code for the first PPM image] + </div> +And you should get the exact same picture as before. + Rays, a Simple Camera, and Background ==================================================================================================== -<div class='together'> +The ray Class +-------------- The one thing that all ray tracers have is a ray class and a computation of what color is seen along -a ray. Let’s think of a ray as a function $\mathbf{p}(t) = \mathbf{a} + t \vec{\mathbf{b}}$. Here -$\mathbf{p}$ is a 3D position along a line in 3D. $\mathbf{a}$ is the ray origin, and -$\vec{\mathbf{b}}$ is the ray direction. The ray parameter $t$ is a real number (`double` in the -code). Plug in a different $t$ and $p(t)$ moves the point along the ray. Add in negative $t$ and you -can go anywhere on the 3D line. For positive $t$, you get only the parts in front of $\mathbf{a}$, -and this is what is often called a half-line or ray. - - ![Figure [lerp]: Linear interpolation](../images/fig.lerp.jpg) +a ray. Let’s think of a ray as a function $\mathbf{P}(t) = \mathbf{A} + t \mathbf{b}$. Here +$\mathbf{P}$ is a 3D position along a line in 3D. $\mathbf{A}$ is the ray origin and $\mathbf{b}$ is +the ray direction. The ray parameter $t$ is a real number (`double` in the code). Plug in a +different $t$ and $\mathbf{P}(t)$ moves the point along the ray. Add in negative $t$ values and you +can go anywhere on the 3D line. For positive $t$, you get only the parts in front of $\mathbf{A}$, +and this is what is often called a half-line or a ray. -</div> + ![Figure [lerp]: Linear interpolation](../images/fig-1.02-lerp.jpg) <div class='together'> -The function $p(t)$ in more verbose code form I call `ray::at(t)`: +We can represent the idea of a ray as a class, and represent the function $\mathbf{P}(t)$ as a +function that we'll call `ray::at(t)`: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ #ifndef RAY_H @@ -377,111 +513,259 @@ #include "vec3.h" class ray { - public: - ray() {} - ray(const vec3& origin, const vec3& direction) - : orig(origin), dir(direction) - {} + public: + ray() {} - vec3 origin() const { return orig; } - vec3 direction() const { return dir; } + ray(const point3& origin, const vec3& direction) : orig(origin), dir(direction) {} - vec3 at(double t) const { - return orig + t*dir; - } + const point3& origin() const { return orig; } + const vec3& direction() const { return dir; } + + point3 at(double t) const { + return orig + t*dir; + } - public: - vec3 orig; - vec3 dir; + private: + point3 orig; + vec3 dir; }; #endif ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [ray-initial]: <kbd>[ray.h]</kbd> The ray class] + </div> -Now we are ready to turn the corner and make a ray tracer. At the core, the ray tracer sends rays +(For those unfamiliar with C++, the functions `ray::origin()` and `ray::direction()` both return an +immutable reference to their members. Callers can either just use the reference directly, or make a +mutable copy depending on their needs.) + + +Sending Rays Into the Scene +---------------------------- +Now we are ready to turn the corner and make a ray tracer. At its core, a ray tracer sends rays through pixels and computes the color seen in the direction of those rays. The involved steps are -(1) calculate the ray from the eye to the pixel, (2) determine which objects the ray intersects, and -(3) compute a color for that intersection point. When first developing a ray tracer, I always do a -simple camera for getting the code up and running. I also make a simple `color(ray)` function that -returns the color of the background (a simple gradient). + + 1. Calculate the ray from the “eye” through the pixel, + 2. Determine which objects the ray intersects, and + 3. Compute a color for the closest intersection point. + +When first developing a ray tracer, I always do a simple camera for getting the code up and running. I’ve often gotten into trouble using square images for debugging because I transpose $x$ and $y$ too -often, so I’ll stick with a 200×100 image. I’ll put the “eye” (or camera center if you think of a -camera) at $(0,0,0)$. I will have the y-axis go up, and the x-axis to the right. In order to respect -the convention of a right handed coordinate system, into the screen is the negative z-axis. I will -traverse the screen from the lower left hand corner, and use two offset vectors along the screen -sides to move the ray endpoint across the screen. Note that I do not make the ray direction a unit -length vector because I think not doing that makes for simpler and slightly faster code. +often, so we’ll use a non-square image. A square image has a 1∶1 aspect ratio, because its +width is the same as its height. Since we want a non-square image, we'll choose 16∶9 because +it's so common. A 16∶9 aspect ratio means that the ratio of image width to image height is +16∶9. Put another way, given an image with a 16∶9 aspect ratio, + + $$\text{width} / \text{height} = 16 / 9 = 1.7778$$ - ![Figure [cam-geom]: Camera geometry](../images/fig.cam-geom.jpg) +For a practical example, an image 800 pixels wide by 400 pixels high has a 2∶1 aspect ratio. + +The image's aspect ratio can be determined from the ratio of its width to its height. However, since +we have a given aspect ratio in mind, it's easier to set the image's width and the aspect ratio, and +then using this to calculate for its height. This way, we can scale up or down the image by changing +the image width, and it won't throw off our desired aspect ratio. We do have to make sure that when +we solve for the image height the resulting height is at least 1. + +In addition to setting up the pixel dimensions for the rendered image, we also need to set up a +virtual _viewport_ through which to pass our scene rays. The viewport is a virtual rectangle in the +3D world that contains the grid of image pixel locations. If pixels are spaced the same distance +horizontally as they are vertically, the viewport that bounds them will have the same aspect ratio +as the rendered image. The distance between two adjacent pixels is called the pixel spacing, and +square pixels is the standard. + +To start things off, we'll choose an arbitrary viewport height of 2.0, and scale the viewport width +to give us the desired aspect ratio. Here's a snippet of what this code will look like: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + auto aspect_ratio = 16.0 / 9.0; + int image_width = 400; + + // Calculate the image height, and ensure that it's at least 1. + int image_height = int(image_width / aspect_ratio); + image_height = (image_height < 1) ? 1 : image_height; + + // Viewport widths less than one are ok since they are real valued. + auto viewport_height = 2.0; + auto viewport_width = viewport_height * (double(image_width)/image_height); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [image-setup]: Rendered image setup] + +If you're wondering why we don't just use `aspect_ratio` when computing `viewport_width`, it's +because the value set to `aspect_ratio` is the ideal ratio, it may not be the _actual_ ratio between +`image_width` and `image_height`. If `image_height` was allowed to be real valued--rather than just +an integer--then it would be fine to use `aspect_ratio`. But the _actual_ ratio between +`image_width` and `image_height` can vary based on two parts of the code. First, `image_height` is +rounded down to the nearest integer, which can increase the ratio. Second, we don't allow +`image_height` to be less than one, which can also change the actual aspect ratio. + +Note that `aspect_ratio` is an ideal ratio, which we approximate as best as possible with the +integer-based ratio of image width over image height. In order for our viewport proportions to +exactly match our image proportions, we use the calculated image aspect ratio to determine our final +viewport width. + +Next we will define the camera center: a point in 3D space from which all scene rays will originate +(this is also commonly referred to as the _eye point_). The vector from the camera center to the +viewport center will be orthogonal to the viewport. We'll initially set the distance between the +viewport and the camera center point to be one unit. This distance is often referred to as the +_focal length_. + +For simplicity we'll start with the camera center at $(0,0,0)$. We'll also have the y-axis go up, +the x-axis to the right, and the negative z-axis pointing in the viewing direction. (This is +commonly referred to as _right-handed coordinates_.) + + ![Figure [camera-geom]: Camera geometry](../images/fig-1.03-cam-geom.jpg) + +Now the inevitable tricky part. While our 3D space has the conventions above, this conflicts with +our image coordinates, where we want to have the zeroth pixel in the top-left and work our way down +to the last pixel at the bottom right. This means that our image coordinate Y-axis is inverted: Y +increases going down the image. + +As we scan our image, we will start at the upper left pixel (pixel $0,0$), scan left-to-right across +each row, and then scan row-by-row, top-to-bottom. To help navigate the pixel grid, we'll use a +vector from the left edge to the right edge ($\mathbf{V_u}$), and a vector from the upper edge to +the lower edge ($\mathbf{V_v}$). + +Our pixel grid will be inset from the viewport edges by half the pixel-to-pixel distance. This way, +our viewport area is evenly divided into width × height identical regions. Here's what our +viewport and pixel grid look like: + + ![Figure [pixel-grid]: Viewport and pixel grid](../images/fig-1.04-pixel-grid.jpg) + +In this figure, we have the viewport, the pixel grid for a 7×5 resolution image, the viewport +upper left corner $\mathbf{Q}$, the pixel $\mathbf{P_{0,0}}$ location, the viewport vector +$\mathbf{V_u}$ (`viewport_u`), the viewport vector $\mathbf{V_v}$ (`viewport_v`), and the pixel +delta vectors $\mathbf{\Delta u}$ and $\mathbf{\Delta v}$. <div class='together'> -Below in code, the ray `r` goes to approximately the pixel centers (I won’t worry about exactness -for now because we’ll add antialiasing later): +Drawing from all of this, here's the code that implements the camera. We'll stub in a function +`ray_color(const ray& r)` that returns the color for a given scene ray -- which we'll set to always +return black for now. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include "color.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight #include "ray.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include "vec3.h" #include <iostream> - vec3 ray_color(const ray& r) { - vec3 unit_direction = unit_vector(r.direction()); - auto t = 0.5*(unit_direction.y() + 1.0); - return (1.0-t)*vec3(1.0, 1.0, 1.0) + t*vec3(0.5, 0.7, 1.0); + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + color ray_color(const ray& r) { + return color(0,0,0); } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ int main() { - const int image_width = 200; - const int image_height = 100; - std::cout << "P3\n" << image_width << " " << image_height << "\n255\n"; + // Image + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - vec3 lower_left_corner(-2.0, -1.0, -1.0); - vec3 horizontal(4.0, 0.0, 0.0); - vec3 vertical(0.0, 2.0, 0.0); - vec3 origin(0.0, 0.0, 0.0); + auto aspect_ratio = 16.0 / 9.0; + int image_width = 400; + + // Calculate the image height, and ensure that it's at least 1. + int image_height = int(image_width / aspect_ratio); + image_height = (image_height < 1) ? 1 : image_height; + + // Camera + + auto focal_length = 1.0; + auto viewport_height = 2.0; + auto viewport_width = viewport_height * (double(image_width)/image_height); + auto camera_center = point3(0, 0, 0); + + // Calculate the vectors across the horizontal and down the vertical viewport edges. + auto viewport_u = vec3(viewport_width, 0, 0); + auto viewport_v = vec3(0, -viewport_height, 0); + + // Calculate the horizontal and vertical delta vectors from pixel to pixel. + auto pixel_delta_u = viewport_u / image_width; + auto pixel_delta_v = viewport_v / image_height; + + // Calculate the location of the upper left pixel. + auto viewport_upper_left = camera_center + - vec3(0, 0, focal_length) - viewport_u/2 - viewport_v/2; + auto pixel00_loc = viewport_upper_left + 0.5 * (pixel_delta_u + pixel_delta_v); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - for (int j = image_height-1; j >= 0; --j) { - std::cerr << "\rScanlines remaining: " << j << ' ' << std::flush; - for (int i = 0; i < image_width; ++i) { + + // Render + + std::cout << "P3\n" << image_width << " " << image_height << "\n255\n"; + + for (int j = 0; j < image_height; j++) { + std::clog << "\rScanlines remaining: " << (image_height - j) << ' ' << std::flush; + for (int i = 0; i < image_width; i++) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - auto u = double(i) / image_width; - auto v = double(j) / image_height; - ray r(origin, lower_left_corner + u*horizontal + v*vertical); - vec3 color = ray_color(r); + auto pixel_center = pixel00_loc + (i * pixel_delta_u) + (j * pixel_delta_v); + auto ray_direction = pixel_center - camera_center; + ray r(camera_center, ray_direction); + + color pixel_color = ray_color(r); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - color.write_color(std::cout); + write_color(std::cout, pixel_color); } } - std::cerr << "\nDone.\n"; + std::clog << "\rDone. \n"; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [main-blue-white-blend]: <kbd>[main.cc]</kbd> Rendering a blue-to-white gradient] + [Listing [creating-rays]: <kbd>[main.cc]</kbd> Creating scene rays] + </div> +Notice that in the code above, I didn't make `ray_direction` a unit vector, because I think not +doing that makes for simpler and slightly faster code. + +Now we'll fill in the `ray_color(ray)` function to implement a simple gradient. This function will +linearly blend white and blue depending on the height of the $y$ coordinate _after_ scaling the ray +direction to unit length (so $-1.0 < y < 1.0$). Because we're looking at the $y$ height after +normalizing the vector, you'll notice a horizontal gradient to the color in addition to the vertical +gradient. + +I'll use a standard graphics trick to linearly scale $0.0 ≤ a ≤ 1.0$. When $a = 1.0$, I want blue. +When $a = 0.0$, I want white. In between, I want a blend. This forms a “linear blend”, or “linear +interpolation”. This is commonly referred to as a _lerp_ between two values. A lerp is always of the +form + + $$ \mathit{blendedValue} = (1-a)\cdot\mathit{startValue} + a\cdot\mathit{endValue}, $$ + +with $a$ going from zero to one. + <div class='together'> -The `ray_color(ray)` function linearly blends white and blue depending on the height of the $y$ -coordinate _after_ scaling the ray direction to unit length (so $-1.0 < y < 1.0$). Because we're -looking at the $y$ height after normalizing the vector, you'll notice a horizontal gradient to the -color in addition to the vertical gradient. +Putting all this together, here's what we get: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include "color.h" + #include "ray.h" + #include "vec3.h" + + #include <iostream> -I then did a standard graphics trick of scaling that to $0.0 ≤ t ≤ 1.0$. When $t = 1.0$ I want blue. -When $t = 0.0$ I want white. In between, I want a blend. This forms a “linear blend”, or “linear -interpolation”, or “lerp” for short, between two things. A lerp is always of the form - $$ \text{blendedValue} = (1-t)\cdot\text{startValue} + t\cdot\text{endValue}, $$ + color ray_color(const ray& r) { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + vec3 unit_direction = unit_vector(r.direction()); + auto a = 0.5*(unit_direction.y() + 1.0); + return (1.0-a)*color(1.0, 1.0, 1.0) + a*color(0.5, 0.7, 1.0); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } -with $t$ going from zero to one. In our case this produces: + ... + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [main-blue-white-blend]: <kbd>[main.cc]</kbd> Rendering a blue-to-white gradient] - <div class="render"> +</div> - ![A blue-to-white gradient depending on ray Y coordinate](../images/img.blue-to-white.png) +<div class='together'> +In our case this produces: - </div> + ![<span class='num'>Image 2:</span> A blue-to-white gradient depending on ray Y coordinate + ](../images/img-1.02-blue-to-white.png class='pixel') </div> @@ -489,247 +773,314 @@ Adding a Sphere ==================================================================================================== - -<div class='together'> Let’s add a single object to our ray tracer. People often use spheres in ray tracers because -calculating whether a ray hits a sphere is pretty straightforward. Recall that the equation for a -sphere centered at the origin of radius $R$ is $x^2 + y^2 + z^2 = R^2$. Put another way, if a given -point $(x,y,z)$ is on the sphere, then $x^2 + y^2 + z^2 = R^2$. If the given point $(x,y,z)$ is -_inside_ the sphere, then $x^2 + y^2 + z^2 < R^2$, and if a given point $(x,y,z)$ is _outside_ the -sphere, then $x^2 + y^2 + z^2 > R^2$. +calculating whether a ray hits a sphere is relatively simple. -It gets uglier if the sphere center is at $(\mathbf{c}_x, \mathbf{c}_y, \mathbf{c}_z)$: - $$ (x-\mathbf{c}_x)^2 + (y-\mathbf{c}_y)^2 + (z-\mathbf{c}_z)^2 = R^2 $$ -</div> +Ray-Sphere Intersection +------------------------ +The equation for a sphere of radius $r$ that is centered at the origin is an important mathematical +equation: -<div class='together'> -In graphics, you almost always want your formulas to be in terms of vectors so all the x/y/z stuff -is under the hood in the `vec3` class. You might note that the vector from center -$\mathbf{c} = (\mathbf{c}_x,\mathbf{c}_y,\mathbf{c}_z)$ to point $\mathbf{P} = (x,y,z)$ is -$(\mathbf{p} - \mathbf{c})$, and therefore + $$ x^2 + y^2 + z^2 = r^2 $$ + +You can also think of this as saying that if a given point $(x,y,z)$ is on the surface of the +sphere, then $x^2 + y^2 + z^2 = r^2$. If a given point $(x,y,z)$ is _inside_ the sphere, then +$x^2 + y^2 + z^2 < r^2$, and if a given point $(x,y,z)$ is _outside_ the sphere, then +$x^2 + y^2 + z^2 > r^2$. + +If we want to allow the sphere center to be at an arbitrary point $(C_x, C_y, C_z)$, then the +equation becomes a lot less nice: - $$ (\mathbf{p} - \mathbf{c}) \cdot (\mathbf{p} - \mathbf{c}) - = (x-\mathbf{c}_x)^2 + (y-\mathbf{c}_y)^2 + (z-\mathbf{c}_z)^2 + $$ (C_x - x)^2 + (C_y - y)^2 + (C_z - z)^2 = r^2 $$ + +In graphics, you almost always want your formulas to be in terms of vectors so that all the +$x$/$y$/$z$ stuff can be simply represented using a `vec3` class. You might note that the vector +from point $\mathbf{P} = (x,y,z)$ to center $\mathbf{C} = (C_x, C_y, C_z)$ is +$(\mathbf{C} - \mathbf{P})$. + +If we use the definition of the dot product: + + $$ (\mathbf{C} - \mathbf{P}) \cdot (\mathbf{C} - \mathbf{P}) + = (C_x - x)^2 + (C_y - y)^2 + (C_z - z)^2 $$ -</div> -<div class='together'> -So the equation of the sphere in vector form is: +Then we can rewrite the equation of the sphere in vector form as: - $$ (\mathbf{p} - \mathbf{c}) \cdot (\mathbf{p} - \mathbf{c}) = R^2 $$ -</div> + $$ (\mathbf{C} - \mathbf{P}) \cdot (\mathbf{C} - \mathbf{P}) = r^2 $$ -<div class='together'> -We can read this as “any point $\mathbf{p}$ that satisfies this equation is on the sphere”. We want -to know if our ray $p(t) = \mathbf{a} + t\vec{\mathbf{b}}$ ever hits the sphere anywhere. If it does -hit the sphere, there is some $t$ for which $p(t)$ satisfies the sphere equation. So we are looking -for any $t$ where this is true: +We can read this as “any point $\mathbf{P}$ that satisfies this equation is on the sphere”. We want +to know if our ray $\mathbf{P}(t) = \mathbf{Q} + t\mathbf{d}$ ever hits the sphere anywhere. If it +does hit the sphere, there is some $t$ for which $\mathbf{P}(t)$ satisfies the sphere equation. So +we are looking for any $t$ where this is true: - $$ (p(t) - \mathbf{c})\cdot(p(t) - \mathbf{c}) = R^2 $$ + $$ (\mathbf{C} - \mathbf{P}(t)) \cdot (\mathbf{C} - \mathbf{P}(t)) = r^2 $$ -or expanding the full form of the ray $p(t)$: +which can be found by replacing $\mathbf{P}(t)$ with its expanded form: - $$ (\mathbf{a} + t \vec{\mathbf{b}} - \mathbf{c}) - \cdot (\mathbf{a} + t \vec{\mathbf{b}} - \mathbf{c}) = R^2 $$ -</div> + $$ (\mathbf{C} - (\mathbf{Q} + t \mathbf{d})) + \cdot (\mathbf{C} - (\mathbf{Q} + t \mathbf{d})) = r^2 $$ -<div class='together'> -The rules of vector algebra are all that we would want here. If we expand that equation and move all -the terms to the left hand side we get: +We have three vectors on the left dotted by three vectors on the right. If we solved for the full +dot product we would get nine vectors. You can definitely go through and write everything out, but +we don't need to work that hard. If you remember, we want to solve for $t$, so we'll separate the +terms based on whether there is a $t$ or not: - $$ t^2 \vec{\mathbf{b}}\cdot\vec{\mathbf{b}} - + 2t \vec{\mathbf{b}} \cdot \vec{(\mathbf{a}-\mathbf{c})} - + \vec{(\mathbf{a}-\mathbf{c})} \cdot \vec{(\mathbf{a}-\mathbf{c})} - R^2 = 0 + $$ (-t \mathbf{d} + (\mathbf{C} - \mathbf{Q})) \cdot (-t \mathbf{d} + (\mathbf{C} - \mathbf{Q})) + = r^2 $$ -</div> -<div class='together'> -The vectors and $R$ in that equation are all constant and known. The unknown is $t$, and the -equation is a quadratic, like you probably saw in your high school math class. You can solve for $t$ -and there is a square root part that is either positive (meaning two real solutions), negative -(meaning no real solutions), or zero (meaning one real solution). In graphics, the algebra almost -always relates very directly to the geometry. What we have is: +And now we follow the rules of vector algebra to distribute the dot product: + + $$ t^2 \mathbf{d} \cdot \mathbf{d} + - 2t \mathbf{d} \cdot (\mathbf{C} - \mathbf{Q}) + + (\mathbf{C} - \mathbf{Q}) \cdot (\mathbf{C} - \mathbf{Q}) = r^2 + $$ - ![Figure [ray-sphere]: Ray-sphere intersection results](../images/fig.ray-sphere.jpg) +Move the square of the radius over to the left hand side: -</div> + $$ t^2 \mathbf{d} \cdot \mathbf{d} + - 2t \mathbf{d} \cdot (\mathbf{C} - \mathbf{Q}) + + (\mathbf{C} - \mathbf{Q}) \cdot (\mathbf{C} - \mathbf{Q}) - r^2 = 0 + $$ -<div class='together'> -If we take that math and hard-code it into our program, we can test it by coloring red any pixel -that hits a small sphere we place at -1 on the z-axis: +It's hard to make out what exactly this equation is, but the vectors and $r$ in that equation are +all constant and known. Furthermore, the only vectors that we have are reduced to scalars by dot +product. The only unknown is $t$, and we have a $t^2$, which means that this equation is quadratic. +You can solve for a quadratic equation $ax^2 + bx + c = 0$ by using the quadratic formula: + + $$ \frac{-b \pm \sqrt{b^2 - 4ac}}{2a} $$ + +So solving for $t$ in the ray-sphere intersection equation gives us these values for $a$, $b$, and +$c$: + + $$ a = \mathbf{d} \cdot \mathbf{d} $$ + $$ b = -2 \mathbf{d} \cdot (\mathbf{C} - \mathbf{Q}) $$ + $$ c = (\mathbf{C} - \mathbf{Q}) \cdot (\mathbf{C} - \mathbf{Q}) - r^2 $$ + +Using all of the above you can solve for $t$, but there is a square root part that can be either +positive (meaning two real solutions), negative (meaning no real solutions), or zero (meaning one +real solution). In graphics, the algebra almost always relates very directly to the geometry. What +we have is: + + ![Figure [ray-sphere]: Ray-sphere intersection results](../images/fig-1.05-ray-sphere.jpg) + + +Creating Our First Raytraced Image +----------------------------------- +If we take that math and hard-code it into our program, we can test our code by placing a small +sphere at -1 on the z-axis and then coloring red any pixel that intersects it. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - bool hit_sphere(const vec3& center, double radius, const ray& r) { - vec3 oc = r.origin() - center; + bool hit_sphere(const point3& center, double radius, const ray& r) { + vec3 oc = center - r.origin(); auto a = dot(r.direction(), r.direction()); - auto b = 2.0 * dot(oc, r.direction()); + auto b = -2.0 * dot(r.direction(), oc); auto c = dot(oc, oc) - radius*radius; auto discriminant = b*b - 4*a*c; - return (discriminant > 0); + return (discriminant >= 0); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + color ray_color(const ray& r) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - vec3 ray_color(const ray& r) { - if (hit_sphere(vec3(0,0,-1), 0.5, r)) - return vec3(1, 0, 0); + if (hit_sphere(point3(0,0,-1), 0.5, r)) + return color(1, 0, 0); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + vec3 unit_direction = unit_vector(r.direction()); - auto t = 0.5*(unit_direction.y() + 1.0); - return (1.0-t)*vec3(1.0, 1.0, 1.0) + t*vec3(0.5, 0.7, 1.0); + auto a = 0.5*(unit_direction.y() + 1.0); + return (1.0-a)*color(1.0, 1.0, 1.0) + a*color(0.5, 0.7, 1.0); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [main-red-sphere]: <kbd>[main.cc]</kbd> Rendering a red sphere] -</div> <div class='together'> What we get is this: - <div class="render"> - - ![A simple red sphere](../images/img.red-sphere.png) - - </div> + ![<span class='num'>Image 3:</span> A simple red sphere + ](../images/img-1.03-red-sphere.png class='pixel') </div> -Now this lacks all sorts of things -- like shading and reflection rays and more than one object -- -but we are closer to halfway done than we are to our start! One thing to be aware of is that we -tested whether the ray hits the sphere at all, but $t < 0$ solutions work fine. If you change your -sphere center to $z = +1$ you will get exactly the same picture because you see the things behind -you. This is not a feature! We’ll fix those issues next. +Now this lacks all sorts of things -- like shading, reflection rays, and more than one object -- but +we are closer to halfway done than we are to our start! One thing to be aware of is that we are +testing to see if a ray intersects with the sphere by solving the quadratic equation and seeing if a +solution exists, but solutions with negative values of $t$ work just fine. If you change your sphere +center to $z = +1$ you will get exactly the same picture because this solution doesn't distinguish +between objects _in front of the camera_ and objects _behind the camera_. This is not a feature! +We’ll fix those issues next. Surface Normals and Multiple Objects ==================================================================================================== +Shading with Surface Normals +----------------------------- First, let’s get ourselves a surface normal so we can shade. This is a vector that is perpendicular -to the surface at the point of intersection. There are two design decisions to make for normals. -The first is whether these normals are unit length. That is convenient for shading so I will say -yes, but I won’t enforce that in the code. This could allow subtle bugs, so be aware this is -personal preference as are most design decisions like that. For a sphere, the outward normal is in -the direction of the hit point minus the center: +to the surface at the point of intersection. - ![Figure [surf-normal]: Sphere surface-normal geometry](../images/fig.sphere-normal.jpg) +We have a key design decision to make for normal vectors in our code: whether normal vectors will +have an arbitrary length, or will be normalized to unit length. -<div class='together'> -On the earth, this implies that the vector from the earth’s center to you points straight up. Let’s +It is tempting to skip the expensive square root operation involved in normalizing the vector, in +case it's not needed. In practice, however, there are three important observations. First, if a +unit-length normal vector is _ever_ required, then you might as well do it up front once, instead of +over and over again "just in case" for every location where unit-length is required. Second, we _do_ +require unit-length normal vectors in several places. Third, if you require normal vectors to be +unit length, then you can often efficiently generate that vector with an understanding of the +specific geometry class, in its constructor, or in the `hit()` function. For example, sphere normals +can be made unit length simply by dividing by the sphere radius, avoiding the square root entirely. + +Given all of this, we will adopt the policy that all normal vectors will be of unit length. + +For a sphere, the outward normal is in the direction of the hit point minus the center: + + ![Figure [sphere-normal]: Sphere surface-normal geometry](../images/fig-1.06-sphere-normal.jpg) + +On the earth, this means that the vector from the earth’s center to you points straight up. Let’s throw that into the code now, and shade it. We don’t have any lights or anything yet, so let’s just visualize the normals with a color map. A common trick used for visualizing normals (because it’s -easy and somewhat intuitive to assume $\vec{\mathbf{N}}$ is a unit length vector -- so each -component is between -1 and 1) is to map each component to the interval from 0 to 1, and then map -x/y/z to r/g/b. For the normal, we need the hit point, not just whether we hit or not. Let’s assume -the closest hit point (smallest $t$). These changes in the code let us compute and visualize -$\vec{\mathbf{N}}$: +easy and somewhat intuitive to assume $\mathbf{n}$ is a unit length vector -- so each component is +between -1 and 1) is to map each component to the interval from 0 to 1, and then map $(x, y, z)$ to +$(\mathit{red}, \mathit{green}, \mathit{blue})$. For the normal, we need the hit point, not just +whether we hit or not (which is all we're calculating at the moment). We only have one sphere in the +scene, and it's directly in front of the camera, so we won't worry about negative values of $t$ yet. +We'll just assume the closest hit point (smallest $t$) is the one that we want. These changes in the +code let us compute and visualize $\mathbf{n}$: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - double hit_sphere(const vec3& center, double radius, const ray& r) { + double hit_sphere(const point3& center, double radius, const ray& r) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - vec3 oc = r.origin() - center; + vec3 oc = center - r.origin(); auto a = dot(r.direction(), r.direction()); - auto b = 2.0 * dot(oc, r.direction()); + auto b = -2.0 * dot(r.direction(), oc); auto c = dot(oc, oc) - radius*radius; auto discriminant = b*b - 4*a*c; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight if (discriminant < 0) { return -1.0; } else { - return (-b - sqrt(discriminant) ) / (2.0*a); + return (-b - std::sqrt(discriminant) ) / (2.0*a); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ } - vec3 ray_color(const ray& r) { + color ray_color(const ray& r) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - auto t = hit_sphere(vec3(0,0,-1), 0.5, r); + auto t = hit_sphere(point3(0,0,-1), 0.5, r); if (t > 0.0) { vec3 N = unit_vector(r.at(t) - vec3(0,0,-1)); - return 0.5*vec3(N.x()+1, N.y()+1, N.z()+1); + return 0.5*color(N.x()+1, N.y()+1, N.z()+1); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + vec3 unit_direction = unit_vector(r.direction()); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - t = 0.5*(unit_direction.y() + 1.0); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - return (1.0-t)*vec3(1.0, 1.0, 1.0) + t*vec3(0.5, 0.7, 1.0); + auto a = 0.5*(unit_direction.y() + 1.0); + return (1.0-a)*color(1.0, 1.0, 1.0) + a*color(0.5, 0.7, 1.0); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [render-surface-normal]: <kbd>[main.cc]</kbd> Rendering surface normals on a sphere] -</div> <div class='together'> And that yields this picture: - <div class="render"> - - ![A sphere colored according to its normal vectors](../images/img.normals-sphere.png) - - </div> + ![<span class='num'>Image 4:</span> A sphere colored according to its normal vectors + ](../images/img-1.04-normals-sphere.png class='pixel') </div> -Let’s revisit the ray-sphere equation: + +Simplifying the Ray-Sphere Intersection Code +--------------------------------------------- +Let’s revisit the ray-sphere function: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - vec3 oc = r.origin() - center; - auto a = dot(r.direction(), r.direction()); - auto b = 2.0 * dot(oc, r.direction()); - auto c = dot(oc, oc) - radius*radius; - auto discriminant = b*b - 4*a*c; + double hit_sphere(const point3& center, double radius, const ray& r) { + vec3 oc = center - r.origin(); + auto a = dot(r.direction(), r.direction()); + auto b = -2.0 * dot(r.direction(), oc); + auto c = dot(oc, oc) - radius*radius; + auto discriminant = b*b - 4*a*c; + + if (discriminant < 0) { + return -1.0; + } else { + return (-b - std::sqrt(discriminant) ) / (2.0*a); + } + } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [ray-sphere-before]: <kbd>[main.cc]</kbd> Ray-sphere intersection code (before)] First, recall that a vector dotted with itself is equal to the squared length of that vector. -Second, notice how the equation for `b` has a factor of two in it. Consider what happens to the -quadratic equation if $b = 2h$: +Second, notice how the equation for `b` has a factor of negative two in it. Consider what happens to +the quadratic equation if $b = -2h$: $$ \frac{-b \pm \sqrt{b^2 - 4ac}}{2a} $$ - $$ = \frac{-2h \pm \sqrt{(2h)^2 - 4ac}}{2a} $$ + $$ = \frac{-(-2h) \pm \sqrt{(-2h)^2 - 4ac}}{2a} $$ + + $$ = \frac{2h \pm 2\sqrt{h^2 - ac}}{2a} $$ - $$ = \frac{-2h \pm 2\sqrt{h^2 - ac}}{2a} $$ + $$ = \frac{h \pm \sqrt{h^2 - ac}}{a} $$ - $$ = \frac{-h \pm \sqrt{h^2 - ac}}{a} $$ +This simplifies nicely, so we'll use it. So solving for $h$: + $$ b = -2 \mathbf{d} \cdot (\mathbf{C} - \mathbf{Q}) $$ + $$ b = -2h $$ + $$ h = \frac{b}{-2} = \mathbf{d} \cdot (\mathbf{C} - \mathbf{Q}) $$ + +<div class='together'> Using these observations, we can now simplify the sphere-intersection code to this: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - vec3 oc = r.origin() - center; - auto a = r.direction().length_squared(); - auto half_b = dot(oc, r.direction()); - auto c = oc.length_squared() - radius*radius; - auto discriminant = half_b*half_b - a*c; + double hit_sphere(const point3& center, double radius, const ray& r) { + vec3 oc = center - r.origin(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + auto a = r.direction().length_squared(); + auto h = dot(r.direction(), oc); + auto c = oc.length_squared() - radius*radius; + auto discriminant = h*h - a*c; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - if (discriminant < 0) { - return -1.0; - } else { - return (-half_b - sqrt(discriminant) ) / a; + if (discriminant < 0) { + return -1.0; + } else { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + return (h - std::sqrt(discriminant)) / a; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [ray-sphere-after]: <kbd>[main.cc]</kbd> Ray-sphere intersection code (after)] +</div> -Now, how about several spheres? While it is tempting to have an array of spheres, a very clean -solution is the make an “abstract class” for anything a ray might hit, and make both a sphere and a -list of spheres just something you can hit. What that class should be called is something of a -quandary -- calling it an “object” would be good if not for “object oriented” programming. “Surface” -is often used, with the weakness being maybe we will want volumes. “hittable” emphasizes the member -function that unites them. I don’t love any of these, but I will go with “hittable”. -<div class='together'> -This `hittable` abstract class will have a hit function that takes in a ray. Most ray tracers have -found it convenient to add a valid interval for hits $t_{min}$ to $t_{max}$, so the hit only -“counts” if $t_{min} < t < t_{max}$. For the initial rays this is positive $t$, but as we will see, -it can help some details in the code to have an interval $t_{min}$ to $t_{max}$. One design question -is whether to do things like compute the normal if we hit something. We might end up hitting -something closer as we do our search, and we will only need the normal of the closest thing. I will -go with the simple solution and compute a bundle of stuff I will store in some structure. Here’s -the abstract class: +An Abstraction for Hittable Objects +------------------------------------ +Now, how about more than one sphere? While it is tempting to have an array of spheres, a very clean +solution is to make an “abstract class” for anything a ray might hit, and make both a sphere and a +list of spheres just something that can be hit. What that class should be called is something of a +quandary -- calling it an “object” would be good if not for “object oriented” programming. “Surface” +is often used, with the weakness being maybe we will want volumes (fog, clouds, stuff like that). +“hittable” emphasizes the member function that unites them. I don’t love any of these, but we'll go +with “hittable”. + +This `hittable` abstract class will have a `hit` function that takes in a ray. Most ray tracers have +found it convenient to add a valid interval for hits $t_{\mathit{min}}$ to $t_{\mathit{max}}$, so +the hit only “counts” if $t_{\mathit{min}} < t < t_{\mathit{max}}$. For the initial rays this is +positive $t$, but as we will see, it can simplify our code to have an interval $t_{\mathit{min}}$ to +$t_{\mathit{max}}$. One design question is whether to do things like compute the normal if we hit +something. We might end up hitting something closer as we do our search, and we will only need the +normal of the closest thing. I will go with the simple solution and compute a bundle of stuff I will +store in some structure. Here’s the abstract class: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ #ifndef HITTABLE_H @@ -737,20 +1088,23 @@ #include "ray.h" - struct hit_record { - vec3 p; + class hit_record { + public: + point3 p; vec3 normal; + double t; }; class hittable { - public: - virtual bool hit(const ray& r, double t_min, double t_max, hit_record& rec) const = 0; + public: + virtual ~hittable() = default; + + virtual bool hit(const ray& r, double ray_tmin, double ray_tmax, hit_record& rec) const = 0; }; #endif ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [hittable-initial]: <kbd>[hittable.h]</kbd> The hittable class] -</div> <div class='together'> And here’s the sphere: @@ -762,53 +1116,54 @@ #include "hittable.h" #include "vec3.h" - class sphere: public hittable { - public: - sphere() {} - sphere(vec3 cen, double r) : center(cen), radius(r) {}; - - virtual bool hit(const ray& r, double tmin, double tmax, hit_record& rec) const; + class sphere : public hittable { + public: + sphere(const point3& center, double radius) : center(center), radius(std::fmax(0,radius)) {} + + bool hit(const ray& r, double ray_tmin, double ray_tmax, hit_record& rec) const override { + vec3 oc = center - r.origin(); + auto a = r.direction().length_squared(); + auto h = dot(r.direction(), oc); + auto c = oc.length_squared() - radius*radius; + + auto discriminant = h*h - a*c; + if (discriminant < 0) + return false; + + auto sqrtd = std::sqrt(discriminant); + + // Find the nearest root that lies in the acceptable range. + auto root = (h - sqrtd) / a; + if (root <= ray_tmin || ray_tmax <= root) { + root = (h + sqrtd) / a; + if (root <= ray_tmin || ray_tmax <= root) + return false; + } - public: - vec3 center; - double radius; - }; + rec.t = root; + rec.p = r.at(rec.t); + rec.normal = (rec.p - center) / radius; - bool sphere::hit(const ray& r, double t_min, double t_max, hit_record& rec) const { - vec3 oc = r.origin() - center; - auto a = r.direction().length_squared(); - auto half_b = dot(oc, r.direction()); - auto c = oc.length_squared() - radius*radius; - auto discriminant = half_b*half_b - a*c; - - if (discriminant > 0) { - auto root = sqrt(discriminant); - auto temp = (-half_b - root)/a; - if (temp < t_max && temp > t_min) { - rec.t = temp; - rec.p = r.at(rec.t); - rec.normal = (rec.p - center) / radius; - return true; - } - temp = (-half_b + root) / a; - if (temp < t_max && temp > t_min) { - rec.t = temp; - rec.p = r.at(rec.t); - rec.normal = (rec.p - center) / radius; - return true; - } + return true; } - return false; - } + private: + point3 center; + double radius; + }; #endif ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [sphere-initial]: <kbd>[sphere.h]</kbd> The sphere class] +(Note here that we use the C++ standard function `std::fmax()`, which returns the maximum of the two +floating-point arguments. Similarly, we will later use `std::fmin()`, which returns the minimum of +the two floating-point arguments.) </div> -<div class='together'> + +Front Faces Versus Back Faces +------------------------------ The second design decision for normals is whether they should always point out. At present, the normal found will always be in the direction of the center to the intersection point (the normal points out). If the ray intersects the sphere from the outside, the normal points against the ray. @@ -817,21 +1172,19 @@ the sphere, the normal will point outward, but if the ray is inside the sphere, the normal will point inward. - ![Figure [normal-directions]: Possible directions for sphere surface-normal geometry](../images/fig.normal-possibilities.jpg) - -</div> + ![Figure [normal-sides]: Possible directions for sphere surface-normal geometry + ](../images/fig-1.07-normal-sides.jpg) -We need to choose one of these possibilities because we will eventually want to determine which -side of the surface that the ray is coming from. This is important for objects that are rendered +We need to choose one of these possibilities because we will eventually want to determine which side +of the surface that the ray is coming from. This is important for objects that are rendered differently on each side, like the text on a two-sided sheet of paper, or for objects that have an inside and an outside, like glass balls. -If we decide to have the normals always point out, then we will need to determine which side the -ray is on when we color it. We can figure this out by comparing the ray with the normal. If the ray -and the normal face in the same direction, the ray is inside the object, if the ray and the normal -face in the opposide direction, then the ray is outside the object. This can be determined by -taking the dot product of the two vectors, where if their dot is positive, the ray is inside the -sphere. +If we decide to have the normals always point out, then we will need to determine which side the ray +is on when we color it. We can figure this out by comparing the ray with the normal. If the ray and +the normal face in the same direction, the ray is inside the object, if the ray and the normal face +in the opposite direction, then the ray is outside the object. This can be determined by taking the +dot product of the two vectors, where if their dot is positive, the ray is inside the sphere. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ if (dot(ray_direction, outward_normal) > 0.0) { @@ -842,9 +1195,9 @@ ... } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [ray-normal-comparison]: <kbd>[sphere.h]</kbd> Comparing the ray and the normal] - + [Listing [ray-normal-comparison]: Comparing the ray and the normal] +<div class='together'> If we decide to have the normals always point against the ray, we won't be able to use the dot product to determine which side of the surface the ray is on. Instead, we would need to store that information: @@ -855,14 +1208,15 @@ // ray is inside the sphere normal = -outward_normal; front_face = false; - } - else { + } else { // ray is outside the sphere normal = outward_normal; front_face = true; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [normals-point-against]: <kbd>[sphere.h]</kbd> Remembering the side of the surface] + [Listing [normals-point-against]: Remembering the side of the surface] + +</div> We can set things up so that normals always point “outward” from the surface, or always point against the incident ray. This decision is determined by whether you want to determine the side of @@ -871,137 +1225,122 @@ at geometry time. This is simply a matter of preference, and you'll see both implementations in the literature. -We add the `front_face` bool to the `hit_record` struct. I know that we’ll also want motion blur at -some point, so I’ll also add a time input variable. +We add the `front_face` bool to the `hit_record` class. We'll also add a function to solve this +calculation for us: `set_face_normal()`. For convenience we will assume that the vector passed to +the new `set_face_normal()` function is of unit length. We could always normalize the parameter +explicitly, but it's more efficient if the geometry code does this, as it's usually easier when you +know more about the specific geometry. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - #ifndef HITTABLE_H - #define HITTABLE_H - - #include "ray.h" - - struct hit_record { - vec3 p; + class hit_record { + public: + point3 p; vec3 normal; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight double t; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight bool front_face; - inline void set_face_normal(const ray& r, const vec3& outward_normal) { + void set_face_normal(const ray& r, const vec3& outward_normal) { + // Sets the hit record normal vector. + // NOTE: the parameter `outward_normal` is assumed to have unit length. + front_face = dot(r.direction(), outward_normal) < 0; - normal = front_face ? outward_normal :-outward_normal; + normal = front_face ? outward_normal : -outward_normal; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ }; - - class hittable { - public: - virtual bool hit(const ray& r, double t_min, double t_max, hit_record& rec) const = 0; - }; - - #endif ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [hittable-time-side]: <kbd>[hittable.h]</kbd> The hittable class with time and side] + [Listing [front-face-tracking]: <kbd>[hittable.h]</kbd> Adding front-face tracking to hit_record] <div class='together'> And then we add the surface side determination to the class: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - bool sphere::hit(const ray& r, double t_min, double t_max, hit_record& rec) const { - vec3 oc = r.origin() - center; - auto a = r.direction().length_squared(); - auto half_b = dot(oc, r.direction()); - auto c = oc.length_squared() - radius*radius; - auto discriminant = half_b*half_b - a*c; + class sphere : public hittable { + public: + ... + bool hit(const ray& r, double ray_tmin, double ray_tmax, hit_record& rec) const { + ... - if (discriminant > 0) { - auto root = sqrt(discriminant); - auto temp = (-half_b - root)/a; - if (temp < t_max && temp > t_min) { - rec.t = temp; - rec.p = r.at(rec.t); + rec.t = root; + rec.p = r.at(rec.t); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - vec3 outward_normal = (rec.p - center) / radius; - rec.set_face_normal(r, outward_normal); + vec3 outward_normal = (rec.p - center) / radius; + rec.set_face_normal(r, outward_normal); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - return true; - } - temp = (-half_b + root) / a; - if (temp < t_max && temp > t_min) { - rec.t = temp; - rec.p = r.at(rec.t); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - vec3 outward_normal = (rec.p - center) / radius; - rec.set_face_normal(r, outward_normal); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - return true; - } + + return true; } - return false; - } + ... + }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [sphere-final]: <kbd>[sphere.h]</kbd> The `sphere` class with normal determination] + [Listing [sphere-final]: <kbd>[sphere.h]</kbd> The sphere class with normal determination] </div> -<div class='together'> -We add a list of objects: + +A List of Hittable Objects +--------------------------- +We have a generic object called a `hittable` that the ray can intersect with. We now add a class +that stores a list of `hittable`s: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ #ifndef HITTABLE_LIST_H #define HITTABLE_LIST_H #include "hittable.h" + #include <memory> #include <vector> - using std::shared_ptr; using std::make_shared; + using std::shared_ptr; - class hittable_list: public hittable { - public: - hittable_list() {} - hittable_list(shared_ptr<hittable> object) { add(object); } + class hittable_list : public hittable { + public: + std::vector<shared_ptr<hittable>> objects; - void clear() { objects.clear(); } - void add(shared_ptr<hittable> object) { objects.push_back(object); } + hittable_list() {} + hittable_list(shared_ptr<hittable> object) { add(object); } - virtual bool hit(const ray& r, double tmin, double tmax, hit_record& rec) const; + void clear() { objects.clear(); } - public: - std::vector<shared_ptr<hittable>> objects; - }; + void add(shared_ptr<hittable> object) { + objects.push_back(object); + } - bool hittable_list::hit(const ray& r, double t_min, double t_max, hit_record& rec) const { - hit_record temp_rec; - bool hit_anything = false; - auto closest_so_far = t_max; + bool hit(const ray& r, double ray_tmin, double ray_tmax, hit_record& rec) const override { + hit_record temp_rec; + bool hit_anything = false; + auto closest_so_far = ray_tmax; - for (const auto& object : objects) { - if (object->hit(r, t_min, closest_so_far, temp_rec)) { - hit_anything = true; - closest_so_far = temp_rec.t; - rec = temp_rec; + for (const auto& object : objects) { + if (object->hit(r, ray_tmin, closest_so_far, temp_rec)) { + hit_anything = true; + closest_so_far = temp_rec.t; + rec = temp_rec; + } } - } - return hit_anything; - } + return hit_anything; + } + }; #endif ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [hittable-list-initial]: <kbd>[hittable_list.h]</kbd> The hittable_list class] -</div> -## Some New C++ Features -The `hittable_list` class code uses two C++ features that may trip you up if you're not normally a -C++ programmer: `vector` and `shared_ptr`. +Some New C++ Features +---------------------- +The `hittable_list` class code uses some C++ features that may trip you up if you're not normally a +C++ programmer: `vector`, `shared_ptr`, and `make_shared`. -`shared_ptr<type>` is a pointer to some allocated type, with reference-counting semantics. -Every time you assign its value to another shared pointer (usually with a simple assignment), the +`shared_ptr<type>` is a pointer to some allocated type, with reference-counting semantics. Every +time you assign its value to another shared pointer (usually with a simple assignment), the reference count is incremented. As shared pointers go out of scope (like at the end of a block or -function), the reference count is decremented. Once the count goes to zero, the object is deleted. +function), the reference count is decremented. Once the count goes to zero, the object is safely +deleted. <div class='together'> Typically, a shared pointer is first initialized with a newly-allocated object, something like this: @@ -1009,28 +1348,31 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ shared_ptr<double> double_ptr = make_shared<double>(0.37); shared_ptr<vec3> vec3_ptr = make_shared<vec3>(1.414214, 2.718281, 1.618034); - shared_ptr<sphere> sphere_ptr = make_shared<sphere>(vec3(0,0,0), 1.0); + shared_ptr<sphere> sphere_ptr = make_shared<sphere>(point3(0,0,0), 1.0); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [shared-ptr]: An example allocation using `shared_ptr`] + [Listing [shared-ptr]: An example allocation using shared_ptr] + +</div> `make_shared<thing>(thing_constructor_params ...)` allocates a new instance of type `thing`, using the constructor parameters. It returns a `shared_ptr<thing>`. +<div class='together'> Since the type can be automatically deduced by the return type of `make_shared<type>(...)`, the above lines can be more simply expressed using C++'s `auto` type specifier: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ auto double_ptr = make_shared<double>(0.37); auto vec3_ptr = make_shared<vec3>(1.414214, 2.718281, 1.618034); - auto sphere_ptr = make_shared<sphere>(vec3(0,0,0), 1.0); + auto sphere_ptr = make_shared<sphere>(point3(0,0,0), 1.0); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [shared-ptr-auto]: An example allocation using `shared_ptr` with `auto` type] + [Listing [shared-ptr-auto]: An example allocation using shared_ptr with auto type] </div> We'll use shared pointers in our code, because it allows multiple geometries to share a common -instance (for example, a bunch of spheres that all use the same texture map material), and because -it makes memory management automatic and easier to reason about. +instance (for example, a bunch of spheres that all use the same color material), and because it +makes memory management automatic and easier to reason about. `std::shared_ptr` is included with the `<memory>` header. @@ -1042,32 +1384,31 @@ `std::vector` is included with the `<vector>` header. Finally, the `using` statements in listing [hittable-list-initial] tell the compiler that we'll be -getting `shared_ptr` and `make_shared` from the `std` library, so we don't need to prefex these with +getting `shared_ptr` and `make_shared` from the `std` library, so we don't need to prefix these with `std::` every time we reference them. -## Common Constants and Utility Functions -<div class='together'> +Common Constants and Utility Functions +--------------------------------------- We need some math constants that we conveniently define in their own header file. For now we only need infinity, but we will also throw our own definition of pi in there, which we will need later. -There is no standard portable definition of pi, so we just define our own constant for it. We'll -throw common useful constants and future utility functions in `rtweekend.h`, our general main header -file. +We'll also throw common useful constants and future utility functions in here. This new header, +`rtweekend.h`, will be our general main header file. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ #ifndef RTWEEKEND_H #define RTWEEKEND_H #include <cmath> - #include <cstdlib> + #include <iostream> #include <limits> #include <memory> - // Usings + // C++ Std Usings - using std::shared_ptr; using std::make_shared; + using std::shared_ptr; // Constants @@ -1077,668 +1418,1389 @@ // Utility Functions inline double degrees_to_radians(double degrees) { - return degrees * pi / 180; + return degrees * pi / 180.0; } - inline double ffmin(double a, double b) { return a <= b ? a : b; } - inline double ffmax(double a, double b) { return a >= b ? a : b; } - // Common Headers + #include "color.h" #include "ray.h" #include "vec3.h" #endif ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [rtweekend-initial]: <kbd>[rtweekend.h]</kbd> The rtweekend.h common header] -</div> + +Program files will include `rtweekend.h` first, so all other header files (where the bulk of our +code will reside) can implicitly assume that `rtweekend.h` has already been included. Header files +still need to explicitly include any other necessary header files. We'll make some updates with +these assumptions in mind. + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete + #include <iostream> + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [assume-rtw-color]: <kbd>[color.h]</kbd> Assume rtweekend.h inclusion for color.h] + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete + #include "ray.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [assume-rtw-hittable]: <kbd>[hittable.h]</kbd> Assume rtweekend.h inclusion for hittable.h] + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete + #include <memory> + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include <vector> + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete + using std::make_shared; + using std::shared_ptr; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [assume-rtw-hittable-list]: <kbd>[hittable_list.h]</kbd> Assume rtweekend.h inclusion for hittable_list.h] + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete + #include "vec3.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [assume-rtw-sphere]: <kbd>[sphere.h]</kbd> Assume rtweekend.h inclusion for sphere.h] + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete + #include <cmath> + #include <iostream> + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [assume-rtw-vec3]: <kbd>[vec3.h]</kbd> Assume rtweekend.h inclusion for vec3.h] <div class='together'> -And the new main: +And now the new main: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight #include "rtweekend.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete + #include "color.h" + #include "ray.h" + #include "vec3.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + #include "hittable.h" #include "hittable_list.h" #include "sphere.h" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete #include <iostream> + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete + double hit_sphere(const point3& center, double radius, const ray& r) { + ... + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - vec3 ray_color(const ray& r, const hittable& world) { + color ray_color(const ray& r, const hittable& world) { hit_record rec; if (world.hit(r, 0, infinity, rec)) { - return 0.5 * (rec.normal + vec3(1,1,1)); + return 0.5 * (rec.normal + color(1,1,1)); } - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + vec3 unit_direction = unit_vector(r.direction()); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - auto t = 0.5*(unit_direction.y() + 1.0); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - return (1.0-t)*vec3(1.0, 1.0, 1.0) + t*vec3(0.5, 0.7, 1.0); + auto a = 0.5*(unit_direction.y() + 1.0); + return (1.0-a)*color(1.0, 1.0, 1.0) + a*color(0.5, 0.7, 1.0); } int main() { - const int image_width = 200; - const int image_height = 100; - std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n"; + // Image - vec3 lower_left_corner(-2.0, -1.0, -1.0); - vec3 horizontal(4.0, 0.0, 0.0); - vec3 vertical(0.0, 2.0, 0.0); - vec3 origin(0.0, 0.0, 0.0); + auto aspect_ratio = 16.0 / 9.0; + int image_width = 400; + + // Calculate the image height, and ensure that it's at least 1. + int image_height = int(image_width / aspect_ratio); + image_height = (image_height < 1) ? 1 : image_height; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + // World + hittable_list world; - world.add(make_shared<sphere>(vec3(0,0,-1), 0.5)); - world.add(make_shared<sphere>(vec3(0,-100.5,-1), 100)); + + world.add(make_shared<sphere>(point3(0,0,-1), 0.5)); + world.add(make_shared<sphere>(point3(0,-100.5,-1), 100)); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - for (int j = image_height-1; j >= 0; --j) { - std::cerr << "\rScanlines remaining: " << j << ' ' << std::flush; - for (int i = 0; i < image_width; ++i) { - auto u = double(i) / image_width; - auto v = double(j) / image_height; - ray r(origin, lower_left_corner + u*horizontal + v*vertical); + // Camera + + auto focal_length = 1.0; + auto viewport_height = 2.0; + auto viewport_width = viewport_height * (double(image_width)/image_height); + auto camera_center = point3(0, 0, 0); + + // Calculate the vectors across the horizontal and down the vertical viewport edges. + auto viewport_u = vec3(viewport_width, 0, 0); + auto viewport_v = vec3(0, -viewport_height, 0); + + // Calculate the horizontal and vertical delta vectors from pixel to pixel. + auto pixel_delta_u = viewport_u / image_width; + auto pixel_delta_v = viewport_v / image_height; + + // Calculate the location of the upper left pixel. + auto viewport_upper_left = camera_center + - vec3(0, 0, focal_length) - viewport_u/2 - viewport_v/2; + auto pixel00_loc = viewport_upper_left + 0.5 * (pixel_delta_u + pixel_delta_v); + + // Render + + std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n"; + + for (int j = 0; j < image_height; j++) { + std::clog << "\rScanlines remaining: " << (image_height - j) << ' ' << std::flush; + for (int i = 0; i < image_width; i++) { + auto pixel_center = pixel00_loc + (i * pixel_delta_u) + (j * pixel_delta_v); + auto ray_direction = pixel_center - camera_center; + ray r(camera_center, ray_direction); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - vec3 color = ray_color(r, world); + color pixel_color = ray_color(r, world); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - - color.write_color(std::cout); + write_color(std::cout, pixel_color); } } - std::cerr << "\nDone.\n"; + std::clog << "\rDone. \n"; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [main-with-rtweekend-h]: <kbd>[main.cc]</kbd> desc] + [Listing [main-with-rtweekend-h]: <kbd>[main.cc]</kbd> The new main with hittables] + </div> -<div class='together'> -This yields a picture that is really just a visualization of where the spheres are along with their -surface normal. This is often a great way to look at your model for flaws and characteristics. +This yields a picture that is really just a visualization of where the spheres are located along +with their surface normal. This is often a great way to view any flaws or specific characteristics +of a geometric model. - <div class="render"> + ![<span class='num'>Image 5:</span> Resulting render of normals-colored sphere with ground + ](../images/img-1.05-normals-sphere-ground.png class='pixel') - ![Resulting render of normals-colored sphere with ground](../images/img.normals-sphere-ground.png) - </div> +An Interval Class +------------------ +Before we continue, we'll implement an interval class to manage real-valued intervals with a minimum +and a maximum. We'll end up using this class quite often as we proceed. -</div> + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #ifndef INTERVAL_H + #define INTERVAL_H + class interval { + public: + double min, max; + interval() : min(+infinity), max(-infinity) {} // Default interval is empty -Antialiasing -==================================================================================================== + interval(double min, double max) : min(min), max(max) {} -When a real camera takes a picture, there are usually no jaggies along edges because the edge pixels -are a blend of some foreground and some background. We can get the same effect by averaging a bunch -of samples inside each pixel. We will not bother with stratification. This is controversial, but is -usual for my programs. For some ray tracers it is critical, but the kind of general one we are -writing doesn’t benefit very much from it and it makes the code uglier. We abstract the camera class -a bit so we can make a cooler camera later. + double size() const { + return max - min; + } -One thing we need is a random number generator that returns real random numbers. We need a function -that returns a canonical random number which by convention returns random real in the range -$0 ≤ r < 1$. The “less than” before the 1 is important as we will sometimes take advantage of that. + bool contains(double x) const { + return min <= x && x <= max; + } -<div class='together'> -A simple approach to this is to use the `rand()` function that can be found in `<cstdlib>`. This -function returns a random integer in the range 0 and `RAND_MAX`. Hence we can get a real random -number as desired with the following code snippet, added to `rtweekend.h`: + bool surrounds(double x) const { + return min < x && x < max; + } - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - #include <cstdlib> - ... + static const interval empty, universe; + }; - inline double random_double() { - // Returns a random real in [0,1). - return rand() / (RAND_MAX + 1.0); - } + const interval interval::empty = interval(+infinity, -infinity); + const interval interval::universe = interval(-infinity, +infinity); - inline double random_double(double min, double max) { - // Returns a random real in [min,max). - return min + (max-min)*random_double(); - } + #endif ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [random-double]: <kbd>[rtweekend.h]</kbd> random_double() functions] -</div> + [Listing [interval-initial]: <kbd>[interval.h]</kbd> Introducing the new interval class] -<div class='together'> -C++ did not traditionally have a standard random number generator, but newer versions of C++ have -addressed this issue with the `<random>` header (if imperfectly according to some experts). -If you want to use this, you can obtain a random number with the conditions we need as follows: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - #include <functional> - #include <random> + // Common Headers - inline double random_double() { - static std::uniform_real_distribution<double> distribution(0.0, 1.0); - static std::mt19937 generator; - static std::function<double()> rand_generator = - std::bind(distribution, generator); - return rand_generator(); - } + #include "color.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + #include "interval.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include "ray.h" + #include "vec3.h" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [random-double-alt]: <kbd>[file]</kbd> random_double(), alternate implemenation] -</div> + [Listing [interval-rtweekend]: <kbd>[rtweekend.h]</kbd> Including the new interval class] -<div class='together'> -For a given pixel we have several samples within that pixel and send rays through each of the -samples. The colors of these rays are then averaged: - - ![Figure [pixel-samples]: Pixel samples](../images/fig.pixel-samples.jpg) - -</div> - -<div class='together'> -Putting that all together yields a camera class encapsulating our simple axis-aligned camera from -before: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - #ifndef CAMERA_H - #define CAMERA_H - - #include "rtweekend.h" - - class camera { - public: - camera() { - lower_left_corner = vec3(-2.0, -1.0, -1.0); - horizontal = vec3(4.0, 0.0, 0.0); - vertical = vec3(0.0, 2.0, 0.0); - origin = vec3(0.0, 0.0, 0.0); - } - - ray get_ray(double u, double v) { - return ray(origin, lower_left_corner + u*horizontal + v*vertical - origin); - } - - public: - vec3 origin; - vec3 lower_left_corner; - vec3 horizontal; - vec3 vertical; + class hittable { + public: + ... + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + virtual bool hit(const ray& r, interval ray_t, hit_record& rec) const = 0; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ }; - #endif ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [camera-initial]: <kbd>[camera.h]</kbd> The camera class] -</div> + [Listing [hittable-with-interval]: <kbd>[hittable.h]</kbd> hittable::hit() using interval] -To handle the multi-sampled color computation, we update the `vec3::write_color()` function. Rather -than adding in a fractional contribution each time we accumulate more light to the color, just add -the full color each iteration, and then perform a single divide at the end (by the number of -samples) when writing out the color. In addition, we'll add a handy utility function to the -`rtweekend.h` utility header: `clamp(x,min,max)`, which clamps the value `x` to the range [min,max]: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - inline double clamp(double x, double min, double max) { - if (x < min) return min; - if (x > max) return max; - return x; - } - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [clamp]: <kbd>[rtweekend.h]</kbd> The clamp() utility function] + class hittable_list : public hittable { + public: + ... + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + bool hit(const ray& r, interval ray_t, hit_record& rec) const override { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + hit_record temp_rec; + bool hit_anything = false; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + auto closest_so_far = ray_t.max; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + for (const auto& object : objects) { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + if (object->hit(r, interval(ray_t.min, closest_so_far), temp_rec)) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - void write_color(std::ostream &out, int samples_per_pixel) { - // Divide the color total by the number of samples. - auto scale = 1.0 / samples_per_pixel; - auto r = scale * e[0]; - auto g = scale * e[1]; - auto b = scale * e[2]; + hit_anything = true; + closest_so_far = temp_rec.t; + rec = temp_rec; + } + } - // Write the translated [0,255] value of each color component. - out << static_cast<int>(256 * clamp(r, 0.0, 0.999)) << ' ' - << static_cast<int>(256 * clamp(g, 0.0, 0.999)) << ' ' - << static_cast<int>(256 * clamp(b, 0.0, 0.999)) << '\n'; - } + return hit_anything; + } + ... + }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [write-color-clamped]: <kbd>[vec3.h]</kbd> The write_color() function] + [Listing [hittable-list-with-interval]: <kbd>[hittable_list.h]</kbd> hittable_list::hit() using interval] -<div class='together'> -Main is also changed: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - int main() { - const int image_width = 200; - const int image_height = 100; + class sphere : public hittable { + public: + ... ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - const int samples_per_pixel = 100; + bool hit(const ray& r, interval ray_t, hit_record& rec) const override { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + ... - std::cout << "P3\n" << image_width << " " << image_height << "\n255\n"; - - hittable_list world; - world.add(make_shared<sphere>(vec3(0,0,-1), 0.5)); - world.add(make_shared<sphere>(vec3(0,-100.5,-1), 100)); - + // Find the nearest root that lies in the acceptable range. + auto root = (h - sqrtd) / a; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - camera cam; + if (!ray_t.surrounds(root)) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - for (int j = image_height-1; j >= 0; --j) { - std::cerr << "\rScanlines remaining: " << j << ' ' << std::flush; - for (int i = 0; i < image_width; ++i) { + root = (h + sqrtd) / a; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - vec3 color(0, 0, 0); - for (int s = 0; s < samples_per_pixel; ++s) { - auto u = (i + random_double()) / image_width; - auto v = (j + random_double()) / image_height; - ray r = cam.get_ray(u, v); - color += ray_color(r, world); - } - color.write_color(std::cout, samples_per_pixel); + if (!ray_t.surrounds(root)) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + return false; } + ... + } + ... + }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [sphere-with-interval]: <kbd>[sphere.h]</kbd> sphere using interval] + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + color ray_color(const ray& r, const hittable& world) { + hit_record rec; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + if (world.hit(r, interval(0, infinity), rec)) { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + return 0.5 * (rec.normal + color(1,1,1)); } - std::cerr << "\nDone.\n"; + vec3 unit_direction = unit_vector(r.direction()); + auto a = 0.5*(unit_direction.y() + 1.0); + return (1.0-a)*color(1.0, 1.0, 1.0) + a*color(0.5, 0.7, 1.0); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [main-multi-sample]: <kbd>[main.cc]</kbd> Rendering with multi-sampled pixels] -</div> + [Listing [main-with-interval]: <kbd>[main.cc]</kbd> The new main using interval] -<div class='together'> -Zooming into the image that is produced, the big change is in edge pixels that are part background -and part foreground: - <div class="render"> - ![Close-up of antialiased pixels](../images/img.antialias.png) +Moving Camera Code Into Its Own Class +==================================================================================================== +Before continuing, now is a good time to consolidate our camera and scene-render code into a single +new class: the `camera` class. The camera class will be responsible for two important jobs: - </div> + 1. Construct and dispatch rays into the world. + 2. Use the results of these rays to construct the rendered image. -</div> +In this refactoring, we'll collect the `ray_color()` function, along with the image, camera, and +render sections of our main program. The new camera class will contain two public methods +`initialize()` and `render()`, plus two private helper methods `get_ray()` and `ray_color()`. +Ultimately, the camera will follow the simplest usage pattern that we could think of: it will be +default constructed no arguments, then the owning code will modify the camera's public variables +through simple assignment, and finally everything is initialized by a call to the `initialize()` +function. This pattern is chosen instead of the owner calling a constructor with a ton of parameters +or by defining and calling a bunch of setter methods. Instead, the owning code only needs to set +what it explicitly cares about. Finally, we could either have the owning code call `initialize()`, +or just have the camera call this function automatically at the start of `render()`. We'll use the +second approach. +After main creates a camera and sets default values, it will call the `render()` method. The +`render()` method will prepare the camera for rendering and then execute the render loop. -Diffuse Materials -==================================================================================================== +<div class='together'> +Here's the skeleton of our new `camera` class: -Now that we have objects and multiple rays per pixel, we can make some realistic looking materials. -We’ll start with diffuse (matte) materials. One question is whether we can mix and match shapes and -materials (so we assign a sphere a material) or if it’s put together so the geometry and material -are tightly bound (that could be useful for procedural objects where the geometry and material are -linked). We’ll go with separate -- which is usual in most renderers -- but do be aware of the -limitation. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #ifndef CAMERA_H + #define CAMERA_H -<div class='together'> -Diffuse objects that don’t emit light merely take on the color of their surroundings, but they -modulate that with their own intrinsic color. Light that reflects off a diffuse surface has its -direction randomized. So, if we send three rays into a crack between two diffuse surfaces they will -each have different random behavior: + #include "hittable.h" - ![Figure [light-bounce]: Light ray bounces](../images/fig.light-bounce.jpg) + class camera { + public: + /* Public Camera Parameters Here */ -</div> + void render(const hittable& world) { + ... + } -They also might be absorbed rather than reflected. The darker the surface, the more likely -absorption is. (That’s why it is dark!) Really any algorithm that randomizes direction will produce -surfaces that look matte. One of the simplest ways to do this turns out to be exactly correct for -ideal diffuse surfaces. (I used to do it as a lazy hack that approximates mathematically ideal -Lambertian.) + private: + /* Private Camera Variables Here */ -(Reader Vassillen Chizhov proved that the lazy hack is indeed just a lazy hack and is inaccurate. -The correct representation of ideal Lambertian isn't much more work, and is presented at the end of -the chapter.) + void initialize() { + ... + } -<div class='together'> -There are two unit radius spheres tangent to the hit point $p$ of a surface. These two spheres have -a center of $(p + \vec{N})$ and $(p - \vec{N})$, where $\vec{N}$ is the normal of the surface. The -sphere with a center at $(p - \vec{N})$ is considered _inside_ the surface, whereas the sphere with -center $(p + \vec{N})$ is considered _outside_ the surface. Select the tangent unit radius sphere -that is on the same side of the surface as the ray origin. Pick a random point $s$ inside this unit -radius sphere, and send a ray from the hit point $p$ to the random point $s$ (this is the vector -$(s-p)$): + color ray_color(const ray& r, const hittable& world) const { + ... + } + }; - ![Figure [rand-vector]: Generating a random diffuse bounce ray](../images/fig.rand-vector.jpg) + #endif + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [camera-skeleton]: <kbd>[camera.h]</kbd> The camera class skeleton] </div> <div class='together'> -We need a way to pick a random point in a unit radius sphere. We’ll use what is usually the easiest -algorithm: a rejection method. First, pick a random point in the unit cube where x, y, and z all -range from -1 to +1. Reject this point and try again if the point is outside the sphere. +To begin with, let's fill in the `ray_color()` function from `main.cc`: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - class vec3 { - public: + class camera { + ... + + private: ... - inline static vec3 random() { - return vec3(random_double(), random_double(), random_double()); - } - inline static vec3 random(double min, double max) { - return vec3(random_double(min,max), random_double(min,max), random_double(min,max)); - } - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [vec-rand-util]: <kbd>[vec3.h]</kbd> `vec3` random utility functions] + color ray_color(const ray& r, const hittable& world) const { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + hit_record rec; + + if (world.hit(r, interval(0, infinity), rec)) { + return 0.5 * (rec.normal + color(1,1,1)); + } + + vec3 unit_direction = unit_vector(r.direction()); + auto a = 0.5*(unit_direction.y() + 1.0); + return (1.0-a)*color(1.0, 1.0, 1.0) + a*color(0.5, 0.7, 1.0); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - vec3 random_in_unit_sphere() { - while (true) { - auto p = vec3::random(-1,1); - if (p.length_squared() >= 1) continue; - return p; } - } + }; + + #endif ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [random-in-unit-sphere]: <kbd>[vec3.h]</kbd> The random_in_unit_sphere() function] + [Listing [camera-ray-color]: <kbd>[camera.h]</kbd> The camera::ray_color function] + </div> <div class='together'> -Then update the `ray_color()` function to use the new random direction generator: +Now we move almost everything from the `main()` function into our new camera class. The only thing +remaining in the `main()` function is the world construction. Here's the camera class with newly +migrated code: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - vec3 ray_color(const ray& r, const hittable& world) { - hit_record rec; - - if (world.hit(r, 0, infinity, rec)) { + class camera { + public: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - vec3 target = rec.p + rec.normal + random_in_unit_sphere(); - return 0.5 * ray_color(ray(rec.p, target - rec.p), world); + double aspect_ratio = 1.0; // Ratio of image width over height + int image_width = 100; // Rendered image width in pixel count + + void render(const hittable& world) { + initialize(); + + std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n"; + + for (int j = 0; j < image_height; j++) { + std::clog << "\rScanlines remaining: " << (image_height - j) << ' ' << std::flush; + for (int i = 0; i < image_width; i++) { + auto pixel_center = pixel00_loc + (i * pixel_delta_u) + (j * pixel_delta_v); + auto ray_direction = pixel_center - center; + ray r(center, ray_direction); + + color pixel_color = ray_color(r, world); + write_color(std::cout, pixel_color); + } + } + + std::clog << "\rDone. \n"; + } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + private: + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + int image_height; // Rendered image height + point3 center; // Camera center + point3 pixel00_loc; // Location of pixel 0, 0 + vec3 pixel_delta_u; // Offset to pixel to the right + vec3 pixel_delta_v; // Offset to pixel below + + void initialize() { + image_height = int(image_width / aspect_ratio); + image_height = (image_height < 1) ? 1 : image_height; + + center = point3(0, 0, 0); + + // Determine viewport dimensions. + auto focal_length = 1.0; + auto viewport_height = 2.0; + auto viewport_width = viewport_height * (double(image_width)/image_height); + + // Calculate the vectors across the horizontal and down the vertical viewport edges. + auto viewport_u = vec3(viewport_width, 0, 0); + auto viewport_v = vec3(0, -viewport_height, 0); + + // Calculate the horizontal and vertical delta vectors from pixel to pixel. + pixel_delta_u = viewport_u / image_width; + pixel_delta_v = viewport_v / image_height; + + // Calculate the location of the upper left pixel. + auto viewport_upper_left = + center - vec3(0, 0, focal_length) - viewport_u/2 - viewport_v/2; + pixel00_loc = viewport_upper_left + 0.5 * (pixel_delta_u + pixel_delta_v); } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - vec3 unit_direction = unit_vector(r.direction()); - auto t = 0.5*(unit_direction.y() + 1.0); - return (1.0-t)*vec3(1.0, 1.0, 1.0) + t*vec3(0.5, 0.7, 1.0); - } + color ray_color(const ray& r, const hittable& world) const { + ... + } + }; + + #endif ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [ray-color-random-unit]: <kbd>[main.cc]</kbd> ray_color() using a random ray direction] + [Listing [camera-working]: <kbd>[camera.h]</kbd> The working camera class] + </div> <div class='together'> -There's one potential problem lurking here. Notice that the `ray_color` function is recursive. When -will it stop recursing? When it fails to hit anything. In some cases, however, that may be a long -time — long enough to blow the stack. To guard against that, let's limit the maximum recursion -depth, returning no light contribution at the maximum depth: +And here's the much reduced main: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include "rtweekend.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - vec3 ray_color(const ray& r, const hittable& world, int depth) { - hit_record rec; + #include "camera.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include "hittable.h" + #include "hittable_list.h" + #include "sphere.h" - // If we've exceeded the ray bounce limit, no more light is gathered. - if (depth <= 0) - return vec3(0,0,0); - if (world.hit(r, 0, infinity, rec)) { - vec3 target = rec.p + rec.normal + random_in_unit_sphere(); - return 0.5 * ray_color(ray(rec.p, target - rec.p), world, depth-1); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete + color ray_color(const ray& r, const hittable& world) { + ... + } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - } - vec3 unit_direction = unit_vector(r.direction()); - auto t = 0.5*(unit_direction.y() + 1.0); - return (1.0-t)*vec3(1.0, 1.0, 1.0) + t*vec3(0.5, 0.7, 1.0); + int main() { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + hittable_list world; + + world.add(make_shared<sphere>(point3(0,0,-1), 0.5)); + world.add(make_shared<sphere>(point3(0,-100.5,-1), 100)); + + camera cam; + + cam.aspect_ratio = 16.0 / 9.0; + cam.image_width = 400; + + cam.render(world); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [main-with-new-camera]: <kbd>[main.cc]</kbd> The new main, using the new camera] + +</div> + +Running this newly refactored program should give us the same rendered image as before. + + + +Antialiasing +==================================================================================================== +If you zoom into the rendered images so far, you might notice the harsh "stair step" nature of edges +in our rendered images. This stair-stepping is commonly referred to as "aliasing", or "jaggies". +When a real camera takes a picture, there are usually no jaggies along edges, because the edge +pixels are a blend of some foreground and some background. Consider that unlike our rendered images, +a true image of the world is continuous. Put another way, the world (and any true image of it) has +effectively infinite resolution. We can get the same effect by averaging a bunch of samples for each +pixel. + +With a single ray through the center of each pixel, we are performing what is commonly called _point +sampling_. The problem with point sampling can be illustrated by rendering a small checkerboard far +away. If this checkerboard consists of an 8×8 grid of black and white tiles, but only four +rays hit it, then all four rays might intersect only white tiles, or only black, or some odd +combination. In the real world, when we perceive a checkerboard far away with our eyes, we perceive +it as a gray color, instead of sharp points of black and white. That's because our eyes are +naturally doing what we want our ray tracer to do: integrate the (continuous function of) light +falling on a particular (discrete) region of our rendered image. + +Clearly we don't gain anything by just resampling the same ray through the pixel center multiple +times -- we'd just get the same result each time. Instead, we want to sample the light falling +_around_ the pixel, and then integrate those samples to approximate the true continuous result. So, +how do we integrate the light falling around the pixel? + +We'll adopt the simplest model: sampling the square region centered at the pixel that extends +halfway to each of the four neighboring pixels. This is not the optimal approach, but it is the most +straight-forward. (See [_A Pixel is Not a Little Square_][square-pixels] for a deeper dive into this +topic.) + + ![Figure [pixel-samples]: Pixel samples](../images/fig-1.08-pixel-samples.jpg) + + +Some Random Number Utilities +----------------------------- +We're going to need a random number generator that returns real random numbers. This function should +return a canonical random number, which by convention falls in the range $0 ≤ n < 1$. The “less +than” before the 1 is important, as we will sometimes take advantage of that. + +A simple approach to this is to use the `std::rand()` function that can be found in `<cstdlib>`, +which returns a random integer in the range 0 and `RAND_MAX`. Hence we can get a real random number +as desired with the following code snippet, added to `rtweekend.h`: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include <cmath> + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + #include <cstdlib> + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include <iostream> + #include <limits> + #include <memory> + ... + + // Utility Functions + + inline double degrees_to_radians(double degrees) { + return degrees * pi / 180.0; } + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + inline double random_double() { + // Returns a random real in [0,1). + return std::rand() / (RAND_MAX + 1.0); + } + + inline double random_double(double min, double max) { + // Returns a random real in [min,max). + return min + (max-min)*random_double(); + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [random-double]: <kbd>[rtweekend.h]</kbd> random_double() functions] + +C++ did not traditionally have a standard random number generator, but newer versions of C++ have +addressed this issue with the `<random>` header (if imperfectly according to some experts). If you +want to use this, you can obtain a random number with the conditions we need as follows: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ ... - int main() { - const int image_width = 200; - const int image_height = 100; - const int samples_per_pixel = 100; + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - const int max_depth = 50; + #include <random> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + ... + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + inline double random_double() { + static std::uniform_real_distribution<double> distribution(0.0, 1.0); + static std::mt19937 generator; + return distribution(generator); + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + inline double random_double(double min, double max) { + // Returns a random real in [min,max). + return min + (max-min)*random_double(); + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + + ... + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [random-double-alt]: <kbd>[rtweekend.h]</kbd> random_double(), alternate implementation] + + +Generating Pixels with Multiple Samples +---------------------------------------- +For a single pixel composed of multiple samples, we'll select samples from the area surrounding the +pixel and average the resulting light (color) values together. + +First we'll update the `write_color()` function to account for the number of samples we use: we need +to find the average across all of the samples that we take. To do this, we'll add the full color +from each iteration, and then finish with a single division (by the number of samples) at the end, +before writing out the color. To ensure that the color components of the final result remain within +the proper $[0,1]$ bounds, we'll add and use a small helper function: `interval::clamp(x)`. + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class interval { + public: ... - for (int j = image_height-1; j >= 0; --j) { - std::cerr << "\rScanlines remaining: " << j << ' ' << std::flush; - for (int i = 0; i < image_width; ++i) { - vec3 color(0, 0, 0); - for (int s = 0; s < samples_per_pixel; ++s) { - auto u = (i + random_double()) / image_width; - auto v = (j + random_double()) / image_height; - ray r = cam.get_ray(u, v); + + bool surrounds(double x) const { + return min < x && x < max; + } + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - color += ray_color(r, world, max_depth); + double clamp(double x) const { + if (x < min) return min; + if (x > max) return max; + return x; + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + ... + }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [clamp]: <kbd>[interval.h]</kbd> The interval::clamp() utility function] + +Here's the updated `write_color()` function that incorporates the interval clamping function: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + #include "interval.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include "vec3.h" + + using color = vec3; + + void write_color(std::ostream& out, const color& pixel_color) { + auto r = pixel_color.x(); + auto g = pixel_color.y(); + auto b = pixel_color.z(); + + // Translate the [0,1] component values to the byte range [0,255]. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + static const interval intensity(0.000, 0.999); + int rbyte = int(256 * intensity.clamp(r)); + int gbyte = int(256 * intensity.clamp(g)); + int bbyte = int(256 * intensity.clamp(b)); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + // Write out the pixel color components. + out << rbyte << ' ' << gbyte << ' ' << bbyte << '\n'; + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [write-color-clamped]: <kbd>[color.h]</kbd> The multi-sample write_color() function] + +Now let's update the camera class to define and use a new `camera::get_ray(i,j)` function, which +will generate different samples for each pixel. This function will use a new helper function +`sample_square()` that generates a random sample point within the unit square centered at the +origin. We then transform the random sample from this ideal square back to the particular pixel +we're currently sampling. + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class camera { + public: + double aspect_ratio = 1.0; // Ratio of image width over height + int image_width = 100; // Rendered image width in pixel count + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + int samples_per_pixel = 10; // Count of random samples for each pixel + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + void render(const hittable& world) { + initialize(); + + std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n"; + + for (int j = 0; j < image_height; j++) { + std::clog << "\rScanlines remaining: " << (image_height - j) << ' ' << std::flush; + for (int i = 0; i < image_width; i++) { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + color pixel_color(0,0,0); + for (int sample = 0; sample < samples_per_pixel; sample++) { + ray r = get_ray(i, j); + pixel_color += ray_color(r, world); + } + write_color(std::cout, pixel_samples_scale * pixel_color); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ } - color.write_color(std::cout, samples_per_pixel); } + + std::clog << "\rDone. \n"; } + ... + private: + int image_height; // Rendered image height + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + double pixel_samples_scale; // Color scale factor for a sum of pixel samples + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + point3 center; // Camera center + point3 pixel00_loc; // Location of pixel 0, 0 + vec3 pixel_delta_u; // Offset to pixel to the right + vec3 pixel_delta_v; // Offset to pixel below - std::cerr << "\nDone.\n"; - } + void initialize() { + image_height = int(image_width / aspect_ratio); + image_height = (image_height < 1) ? 1 : image_height; + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + pixel_samples_scale = 1.0 / samples_per_pixel; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + center = point3(0, 0, 0); + ... + } + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + ray get_ray(int i, int j) const { + // Construct a camera ray originating from the origin and directed at randomly sampled + // point around the pixel location i, j. + + auto offset = sample_square(); + auto pixel_sample = pixel00_loc + + ((i + offset.x()) * pixel_delta_u) + + ((j + offset.y()) * pixel_delta_v); + + auto ray_origin = center; + auto ray_direction = pixel_sample - ray_origin; + + return ray(ray_origin, ray_direction); + } + + vec3 sample_square() const { + // Returns the vector to a random point in the [-.5,-.5]-[+.5,+.5] unit square. + return vec3(random_double() - 0.5, random_double() - 0.5, 0); + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + color ray_color(const ray& r, const hittable& world) const { + ... + } + }; + + #endif ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [ray-color-depth]: <kbd>[main.cc]</kbd> ray_color() with depth limiting] + [Listing [camera-spp]: <kbd>[camera.h]</kbd> Camera with samples-per-pixel parameter] + </div> +(In addition to the new `sample_square()` function above, you'll also find the function +`sample_disk()` in the Github source code. This is included in case you'd like to experiment with +non-square pixels, but we won't be using it in this book. `sample_disk()` depends on the function +`random_in_unit_disk()` which is defined later on.) + <div class='together'> -This gives us: +Main is updated to set the new camera parameter. + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + int main() { + ... - <div class="render"> + camera cam; - ![First render of a diffuse sphere](../images/img.first-diffuse.jpg) + cam.aspect_ratio = 16.0 / 9.0; + cam.image_width = 400; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + cam.samples_per_pixel = 100; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - </div> + cam.render(world); + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [main-spp]: <kbd>[main.cc]</kbd> Setting the new samples-per-pixel parameter] </div> <div class='together'> -Note the shadowing under the sphere. This picture is very dark, but our spheres only absorb half the -energy on each bounce, so they are 50% reflectors. If you can’t see the shadow, don’t worry, we will -fix that now. These spheres should look pretty light (in real life, a light grey). The reason for -this is that almost all image viewers assume that the image is “gamma corrected”, meaning the 0 to 1 -values have some transform before being stored as a byte. There are many good reasons for that, but -for our purposes we just need to be aware of it. To a first approximation, we can use “gamma 2” -which means raising the color to the power $1/gamma$, or in our simple case ½, which is just -square-root: - - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - void write_color(std::ostream &out, int samples_per_pixel) { - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - // Divide the color total by the number of samples and gamma-correct - // for a gamma value of 2.0. - auto scale = 1.0 / samples_per_pixel; - auto r = sqrt(scale * e[0]); - auto g = sqrt(scale * e[1]); - auto b = sqrt(scale * e[2]); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - - // Write the translated [0,255] value of each color component. - out << static_cast<int>(256 * clamp(r, 0.0, 0.999)) << ' ' - << static_cast<int>(256 * clamp(g, 0.0, 0.999)) << ' ' - << static_cast<int>(256 * clamp(b, 0.0, 0.999)) << '\n'; - } - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [write-color-gamma]: <kbd>[vec3.h]</kbd> write_color(), with gamma correction] +Zooming into the image that is produced, we can see the difference in edge pixels. + + ![<span class='num'>Image 6:</span> Before and after antialiasing + ](../images/img-1.06-antialias-before-after.png class='pixel') </div> + + +Diffuse Materials +==================================================================================================== +Now that we have objects and multiple rays per pixel, we can make some realistic looking materials. +We’ll start with diffuse materials (also called _matte_). One question is whether we mix and match +geometry and materials (so that we can assign a material to multiple spheres, or vice versa) or if +geometry and materials are tightly bound (which could be useful for procedural objects where the +geometry and material are linked). We’ll go with separate -- which is usual in most renderers -- but +do be aware that there are alternative approaches. + +A Simple Diffuse Material +-------------------------- +Diffuse objects that don’t emit their own light merely take on the color of their surroundings, but +they do modulate that with their own intrinsic color. Light that reflects off a diffuse surface has +its direction randomized, so, if we send three rays into a crack between two diffuse surfaces they +will each have different random behavior: + + ![Figure [light-bounce]: Light ray bounces](../images/fig-1.09-light-bounce.jpg) + +They might also be absorbed rather than reflected. The darker the surface, the more likely the ray +is absorbed (that’s why it's dark!). Really any algorithm that randomizes direction will produce +surfaces that look matte. Let's start with the most intuitive: a surface that randomly bounces a ray +equally in all directions. For this material, a ray that hits the surface has an equal probability +of bouncing in any direction away from the surface. + + ![Figure [random-vec-hor]: Equal reflection above the horizon + ](../images/fig-1.10-random-vec-horizon.jpg) + +This very intuitive material is the simplest kind of diffuse and -- indeed -- many of the first +raytracing papers used this diffuse method (before adopting a more accurate method that we'll be +implementing a little bit later). We don't currently have a way to randomly reflect a ray, so we'll +need to add a few functions to our vector utility header. The first thing we need is the ability to +generate arbitrary random vectors: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class vec3 { + public: + ... + + double length_squared() const { + return e[0]*e[0] + e[1]*e[1] + e[2]*e[2]; + } + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + static vec3 random() { + return vec3(random_double(), random_double(), random_double()); + } + + static vec3 random(double min, double max) { + return vec3(random_double(min,max), random_double(min,max), random_double(min,max)); + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [vec-rand-util]: <kbd>[vec3.h]</kbd> vec3 random utility functions] + +Then we need to figure out how to manipulate a random vector so that we only get results that are on +the surface of a hemisphere. There are analytical methods of doing this, but they are actually +surprisingly complicated to understand, and quite a bit complicated to implement. Instead, we'll use +what is typically the easiest algorithm: A rejection method. A rejection method works by repeatedly +generating random samples until we produce a sample that meets the desired criteria. In other words, +keep rejecting bad samples until you find a good one. + +There are many equally valid ways of generating a random vector on a hemisphere using the rejection +method, but for our purposes we will go with the simplest, which is: + +1. Generate a random vector inside the unit sphere +2. Normalize this vector to extend it to the sphere surface +3. Invert the normalized vector if it falls onto the wrong hemisphere + <div class='together'> -That yields light grey, as we desire: +First, we will use a rejection method to generate the random vector inside the unit sphere (that is, +a sphere of radius 1). Pick a random point inside the cube enclosing the unit sphere (that is, where +$x$, $y$, and $z$ are all in the range $[-1,+1]$). If this point lies outside the unit +sphere, then generate a new one until we find one that lies inside or on the unit sphere. - <div class="render"> + ![Figure [sphere-vec]: Two vectors were rejected before finding a good one (pre-normalization) + ](../images/fig-1.11-sphere-vec.jpg) - ![Diffuse sphere, with gamma correction](../images/img.gamma-correct.jpg) + ![Figure [sphere-vec]: The accepted random vector is normalized to produce a unit vector + ](../images/fig-1.12-sphere-unit-vec.jpg) - </div> +Here's our first draft of the function: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + ... + + inline vec3 unit_vector(const vec3& v) { + return v / v.length(); + } + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + inline vec3 random_unit_vector() { + while (true) { + auto p = vec3::random(-1,1); + auto lensq = p.length_squared(); + if (lensq <= 1) + return p / sqrt(lensq); + } + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [random-unit-vector-1]: <kbd>[vec3.h]</kbd> The random_unit_vector() function, version one] </div> -<div class='together'> -There’s also a subtle bug in there. Some of the reflected rays hit the object they are reflecting -off of not at exactly $t=0$, but instead at $t=-0.0000001$ or $t=0.00000001$ or whatever floating -point approximation the sphere intersector gives us. So we need to ignore hits very near zero: +Sadly, we have a small floating-point abstraction leak to deal with. Since floating-point numbers +have finite precision, a very small value can underflow to zero when squared. So if all three +coordinates are small enough (that is, very near the center of the sphere), the norm of the vector +will be zero, and thus normalizing will yield the bogus vector $[\pm\infty, \pm\infty, \pm\infty]$. +To fix this, we'll also reject points that lie inside this "black hole" around the center. With +double precision (64-bit floats), we can safely support values greater than $10^{-160}$. + +Here's our more robust function: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - if (world.hit(r, 0.001, infinity, rec)) { + inline vec3 random_unit_vector() { + while (true) { + auto p = vec3::random(-1,1); + auto lensq = p.length_squared(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + if (1e-160 < lensq && lensq <= 1) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + return p / sqrt(lensq); + } + } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [reflect-tolerance]: <kbd>[main.cc]</kbd> Calculating reflected ray origins with tolerance] + [Listing [random-unit-vector-2]: <kbd>[vec3.h]</kbd> The random_unit_vector() function, version two] + +<div class='together'> +Now that we have a random unit vector, we can determine if it is on the correct hemisphere by +comparing against the surface normal: + + ![Figure [normal-hor]: The normal vector tells us which hemisphere we need + ](../images/fig-1.13-surface-normal.jpg) -This gets rid of the shadow acne problem. Yes it is really called that. </div> <div class='together'> -The rejection method presented here produces random points in the unit ball offset along the surface -normal. This corresponds to picking directions on the hemisphere with high probability close to the -normal, and a lower probability of scattering rays at grazing angles. The distribution present -scales by the $\cos^3 (\phi)$ where $\phi$ is the angle from the normal. This is useful since light -arriving at shallow angles spreads over a larger area, and thus has a lower contribution to the -final color. - -However, we are interested in a Lambertian distribution, which has a distribution of $\cos (\phi)$. -True Lambertian has the probability higher for ray scattering close to the normal, but the -distribution is more uniform. This is achieved by picking points on the surface of the unit sphere, -offset along the surface normal. Picking points on the sphere can be achieved by picking points in -the unit ball, and then normalizing those. - - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - vec3 random_unit_vector() { - auto a = random_double(0, 2*pi); - auto z = random_double(-1, 1); - auto r = sqrt(1 - z*z); - return vec3(r*cos(a), r*sin(a), z); +We can take the dot product of the surface normal and our random vector to determine if it's in the +correct hemisphere. If the dot product is positive, then the vector is in the correct hemisphere. If +the dot product is negative, then we need to invert the vector. + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + ... + + inline vec3 random_unit_vector() { + while (true) { + auto p = vec3::random(-1,1); + auto lensq = p.length_squared(); + if (1e-160 < lensq && lensq <= 1) + return p / sqrt(lensq); + } } - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [random-unit-vector]: <kbd>[vec3.h]</kbd> The random_unit_vector() function] - ![Figure [rand-unit-vector]: Generating a random unit vector](../images/fig.rand-unitvector.png) + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + inline vec3 random_on_hemisphere(const vec3& normal) { + vec3 on_unit_sphere = random_unit_vector(); + if (dot(on_unit_sphere, normal) > 0.0) // In the same hemisphere as the normal + return on_unit_sphere; + else + return -on_unit_sphere; + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [random-in-hemisphere]: <kbd>[vec3.h]</kbd> The random_on_hemisphere() function] </div> +If a ray bounces off of a material and keeps 100% of its color, then we say that the material is +_white_. If a ray bounces off of a material and keeps 0% of its color, then we say that the material +is black. As a first demonstration of our new diffuse material we'll set the `ray_color` function to +return 50% of the color from a bounce. We should expect to get a nice gray color. + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class camera { + ... + private: + ... + color ray_color(const ray& r, const hittable& world) const { + hit_record rec; + + if (world.hit(r, interval(0, infinity), rec)) { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + vec3 direction = random_on_hemisphere(rec.normal); + return 0.5 * ray_color(ray(rec.p, direction), world); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } + + vec3 unit_direction = unit_vector(r.direction()); + auto a = 0.5*(unit_direction.y() + 1.0); + return (1.0-a)*color(1.0, 1.0, 1.0) + a*color(0.5, 0.7, 1.0); + } + }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [ray-color-random-unit]: <kbd>[camera.h]</kbd> ray_color() using a random ray direction] <div class='together'> -This `random_unit_vector()` is a drop-in replacement for the existing `random_in_unit_sphere()` -function. +... Indeed we do get rather nice gray spheres: + + ![<span class='num'>Image 7:</span> First render of a diffuse sphere + ](../images/img-1.07-first-diffuse.png class='pixel') + +</div> + + +Limiting the Number of Child Rays +---------------------------------- +There's one potential problem lurking here. Notice that the `ray_color` function is recursive. When +will it stop recursing? When it fails to hit anything. In some cases, however, that may be a long +time -- long enough to blow the stack. To guard against that, let's limit the maximum recursion +depth, returning no light contribution at the maximum depth: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - vec3 ray_color(const ray& r, const hittable& world, int depth) { - hit_record rec; + class camera { + public: + double aspect_ratio = 1.0; // Ratio of image width over height + int image_width = 100; // Rendered image width in pixel count + int samples_per_pixel = 10; // Count of random samples for each pixel + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + int max_depth = 10; // Maximum number of ray bounces into scene + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + void render(const hittable& world) { + initialize(); - // If we've exceeded the ray bounce limit, no more light is gathered. - if (depth <= 0) - return vec3(0,0,0); + std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n"; - if (world.hit(r, 0.001, infinity, rec)) { + for (int j = 0; j < image_height; j++) { + std::clog << "\rScanlines remaining: " << (image_height - j) << ' ' << std::flush; + for (int i = 0; i < image_width; i++) { + color pixel_color(0,0,0); + for (int sample = 0; sample < samples_per_pixel; sample++) { + ray r = get_ray(i, j); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - vec3 target = rec.p + rec.normal + random_unit_vector(); + pixel_color += ray_color(r, max_depth, world); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - return 0.5 * ray_color(ray(rec.p, target - rec.p), world, depth-1); + } + write_color(std::cout, pixel_samples_scale * pixel_color); + } + } + + std::clog << "\rDone. \n"; } + ... + private: + ... + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + color ray_color(const ray& r, int depth, const hittable& world) const { + // If we've exceeded the ray bounce limit, no more light is gathered. + if (depth <= 0) + return color(0,0,0); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - vec3 unit_direction = unit_vector(r.direction()); - auto t = 0.5*(unit_direction.y() + 1.0); - return (1.0-t)*vec3(1.0, 1.0, 1.0) + t*vec3(0.5, 0.7, 1.0); + hit_record rec; + + if (world.hit(r, interval(0, infinity), rec)) { + vec3 direction = random_on_hemisphere(rec.normal); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + return 0.5 * ray_color(ray(rec.p, direction), depth-1, world); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } + + vec3 unit_direction = unit_vector(r.direction()); + auto a = 0.5*(unit_direction.y() + 1.0); + return (1.0-a)*color(1.0, 1.0, 1.0) + a*color(0.5, 0.7, 1.0); + } + }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [ray-color-depth]: <kbd>[camera.h]</kbd> camera::ray_color() with depth limiting] + +<div class='together'> +Update the main() function to use this new depth limit: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + int main() { + ... + + camera cam; + + cam.aspect_ratio = 16.0 / 9.0; + cam.image_width = 400; + cam.samples_per_pixel = 100; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + cam.max_depth = 50; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + cam.render(world); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [ray-color-unit-sphere]: <kbd>[main.cc]</kbd> ray_color() with replacement diffuse] + [Listing [main-ray-depth]: <kbd>[main.cc]</kbd> Using the new ray depth limiting] + </div> <div class='together'> -After rendering we get a similar image: +For this very simple scene we should get basically the same result: - <div class="render"> + ![<span class='num'>Image 8:</span> Second render of a diffuse sphere with limited bounces + ](../images/img-1.08-second-diffuse.png class='pixel') - ![Correct rendering of Lambertian spheres](../images/img.correct-lambertian.png) +</div> - </div> -It's hard to tell the difference between these two diffuse methods, given that our scene of two -spheres is so simple, but you should be able to notice two important visual differences: +Fixing Shadow Acne +------------------- +There’s also a subtle bug that we need to address. A ray will attempt to accurately calculate the +intersection point when it intersects with a surface. Unfortunately for us, this calculation is +susceptible to floating point rounding errors which can cause the intersection point to be ever so +slightly off. This means that the origin of the next ray, the ray that is randomly scattered off of +the surface, is unlikely to be perfectly flush with the surface. It might be just above the surface. +It might be just below the surface. If the ray's origin is just below the surface then it could +intersect with that surface again. Which means that it will find the nearest surface at +$t=0.00000001$ or whatever floating point approximation the hit function gives us. The simplest hack +to address this is just to ignore hits that are very close to the calculated intersection point: - 1. The shadows are less pronounced after the change - 2. Both spheres are lighter in appearance after the change + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class camera { + ... + private: + ... + color ray_color(const ray& r, int depth, const hittable& world) const { + // If we've exceeded the ray bounce limit, no more light is gathered. + if (depth <= 0) + return color(0,0,0); -Both of these changes are due to the more uniform scattering of the light rays, fewer rays are -scattering toward the normal. This means that for diffuse objects, they will appear _lighter_ -because more light bounces toward the camera. For the shadows, less light bounces straight-up, so -the parts of the larger sphere directly underneath the smaller sphere are brighter. -</div> + hit_record rec; + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + if (world.hit(r, interval(0.001, infinity), rec)) { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + vec3 direction = random_on_hemisphere(rec.normal); + return 0.5 * ray_color(ray(rec.p, direction), depth-1, world); + } + + vec3 unit_direction = unit_vector(r.direction()); + auto a = 0.5*(unit_direction.y() + 1.0); + return (1.0-a)*color(1.0, 1.0, 1.0) + a*color(0.5, 0.7, 1.0); + } + }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [reflect-tolerance]: <kbd>[camera.h]</kbd> Calculating reflected ray origins with tolerance] <div class='together'> -The initial hack presented in this book lasted a long time before it was proven to be an incorrect -approximation of ideal Lambertian diffuse. A big reason that the error persisted for so long is -that it can be difficult to: +This gets rid of the shadow acne problem. Yes it is really called that. Here's the result: - 1. Mathematically prove that the probability distribution is incorrect - 2. Intuitively explain why a $\cos (\phi)$ distribution is desirable (and what it would look like) + ![<span class='num'>Image 9:</span> Diffuse sphere with no shadow acne + ](../images/img-1.09-no-acne.png class='pixel') -Not a lot of common, everyday objects are perfectly diffuse, so our visual intuition of how these -objects behave under light can be poorly formed. </div> + +True Lambertian Reflection +--------------------------- +Scattering reflected rays evenly about the hemisphere produces a nice soft diffuse model, but we can +definitely do better. A more accurate representation of real diffuse objects is the _Lambertian_ +distribution. This distribution scatters reflected rays in a manner that is proportional to $\cos +(\phi)$, where $\phi$ is the angle between the reflected ray and the surface normal. This means that +a reflected ray is most likely to scatter in a direction near the surface normal, and less likely to +scatter in directions away from the normal. This non-uniform Lambertian distribution does a better +job of modeling material reflection in the real world than our previous uniform scattering. + +We can create this distribution by adding a random unit vector to the normal vector. At the point of +intersection on a surface there is the hit point, $\mathbf{p}$, and there is the normal of the +surface, $\mathbf{n}$. At the point of intersection, this surface has exactly two sides, so there +can only be two unique unit spheres tangent to any intersection point (one unique sphere for each +side of the surface). These two unit spheres will be displaced from the surface by the length of +their radius, which is exactly one for a unit sphere. + +One sphere will be displaced in the direction of the surface's normal ($\mathbf{n}$) and one sphere +will be displaced in the opposite direction ($\mathbf{-n}$). This leaves us with two spheres of unit +size that will only be _just_ touching the surface at the intersection point. From this, one of the +spheres will have its center at $(\mathbf{P} + \mathbf{n})$ and the other sphere will have its +center at $(\mathbf{P} - \mathbf{n})$. The sphere with a center at $(\mathbf{P} - \mathbf{n})$ is +considered _inside_ the surface, whereas the sphere with center $(\mathbf{P} + \mathbf{n})$ is +considered _outside_ the surface. + +We want to select the tangent unit sphere that is on the same side of the surface as the ray origin. +Pick a random point $\mathbf{S}$ on this unit radius sphere and send a ray from the hit point +$\mathbf{P}$ to the random point $\mathbf{S}$ (this is the vector $(\mathbf{S}-\mathbf{P})$): + + ![Figure [rand-unitvec]: Randomly generating a vector according to Lambertian distribution + ](../images/fig-1.14-rand-unitvec.jpg) + <div class='together'> -In the interest of learning, we are including an intuitive and easy to understand diffuse method. -For the two methods above we had a random vector, first of random length and then of unit length, -offset from the hit point by the normal. It may not be immediately obvious why the vectors should be -displaced by the normal. +The change is actually fairly minimal: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class camera { + ... + color ray_color(const ray& r, int depth, const hittable& world) const { + // If we've exceeded the ray bounce limit, no more light is gathered. + if (depth <= 0) + return color(0,0,0); -A more intuitive approach is to have a uniform scatter direction for all angles away from the hit -point, with no dependence on the angle from the normal. Many of the first raytracing papers used -this diffuse method (before adopting Lambertian diffuse). + hit_record rec; + if (world.hit(r, interval(0.001, infinity), rec)) { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + vec3 direction = rec.normal + random_unit_vector(); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - vec3 random_in_hemisphere(const vec3& normal) { - vec3 in_unit_sphere = random_in_unit_sphere(); - if (dot(in_unit_sphere, normal) > 0.0) // In the same hemisphere as the normal - return in_unit_sphere; - else - return -in_unit_sphere; - } + return 0.5 * ray_color(ray(rec.p, direction), depth-1, world); + } + + vec3 unit_direction = unit_vector(r.direction()); + auto a = 0.5*(unit_direction.y() + 1.0); + return (1.0-a)*color(1.0, 1.0, 1.0) + a*color(0.5, 0.7, 1.0); + } + }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [random-in-hemisphere]: <kbd>[vec3.h]</kbd> The random_in_hemisphere(normal) function] + [Listing [ray-color-unit-sphere]: <kbd>[camera.h]</kbd> ray_color() with replacement diffuse] </div> <div class='together'> -Plugging the new formula into the `ray_color()` function: +After rendering we get a similar image: + + ![<span class='num'>Image 10:</span> Correct rendering of Lambertian spheres + ](../images/img-1.10-correct-lambertian.png class='pixel') + +</div> + +It's hard to tell the difference between these two diffuse methods, given that our scene of two +spheres is so simple, but you should be able to notice two important visual differences: + + 1. The shadows are more pronounced after the change + 2. Both spheres are tinted blue from the sky after the change + +Both of these changes are due to the less uniform scattering of the light rays--more rays are +scattering toward the normal. This means that for diffuse objects, they will appear _darker_ because +less light bounces toward the camera. For the shadows, more light bounces straight-up, so the area +underneath the sphere is darker. + +Not a lot of common, everyday objects are perfectly diffuse, so our visual intuition of how these +objects behave under light can be poorly formed. As scenes become more complicated over the course +of the book, you are encouraged to switch between the different diffuse renderers presented here. +Most scenes of interest will contain a large amount of diffuse materials. You can gain valuable +insight by understanding the effect of different diffuse methods on the lighting of a scene. + + +Using Gamma Correction for Accurate Color Intensity +---------------------------------------------------- +Note the shadowing under the sphere. The picture is very dark, but our spheres only absorb half the +energy of each bounce, so they are 50% reflectors. The spheres should look pretty bright (in real +life, a light grey) but they appear to be rather dark. We can see this more clearly if we walk +through the full brightness gamut for our diffuse material. We start by setting the reflectance of +the `ray_color` function from `0.5` (50%) to `0.1` (10%): ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - vec3 ray_color(const ray& r, const hittable& world, int depth) { - hit_record rec; + class camera { + ... + color ray_color(const ray& r, int depth, const hittable& world) const { + // If we've exceeded the ray bounce limit, no more light is gathered. + if (depth <= 0) + return color(0,0,0); - // If we've exceeded the ray bounce limit, no more light is gathered. - if (depth <= 0) - return vec3(0,0,0); + hit_record rec; - if (world.hit(r, 0.001, infinity, rec)) { + if (world.hit(r, interval(0.001, infinity), rec)) { + vec3 direction = rec.normal + random_unit_vector(); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - vec3 target = rec.p + random_in_hemisphere(rec.normal); + return 0.1 * ray_color(ray(rec.p, direction), depth-1, world); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - return 0.5 * ray_color(ray(rec.p, target - rec.p), world, depth-1); + } + + vec3 unit_direction = unit_vector(r.direction()); + auto a = 0.5*(unit_direction.y() + 1.0); + return (1.0-a)*color(1.0, 1.0, 1.0) + a*color(0.5, 0.7, 1.0); } + }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [ray-color-gamut]: <kbd>[camera.h]</kbd> ray_color() with 10% reflectance] + +We render out at this new 10% reflectance. We then set reflectance to 30% and render again. We +repeat for 50%, 70%, and finally 90%. You can overlay these images from left to right in the photo +editor of your choice and you should get a very nice visual representation of the increasing +brightness of your chosen gamut. This is the one that we've been working with so far: + +![<span class='num'>Image 11:</span> The gamut of our renderer so far +](../images/img-1.11-linear-gamut.png class='pixel') + +If you look closely, or if you use a color picker, you should notice that the 50% reflectance render +(the one in the middle) is far too dark to be half-way between white and black (middle-gray). +Indeed, the 70% reflector is closer to middle-gray. The reason for this is that almost all computer +programs assume that an image is “gamma corrected” before being written into an image file. This +means that the 0 to 1 values have some transform applied before being stored as a byte. Images with +data that are written without being transformed are said to be in _linear space_, whereas images +that are transformed are said to be in _gamma space_. It is likely that the image viewer you are +using is expecting an image in gamma space, but we are giving it an image in linear space. This is +the reason why our image appears inaccurately dark. + +There are many good reasons for why images should be stored in gamma space, but for our purposes we +just need to be aware of it. We are going to transform our data into gamma space so that our image +viewer can more accurately display our image. As a simple approximation, we can use “gamma 2” as our +transform, which is the power that you use when going from gamma space to linear space. We need to +go from linear space to gamma space, which means taking the inverse of "gamma 2", which means an +exponent of $1/\mathit{gamma}$, which is just the square-root. We'll also want to ensure that we +robustly handle negative inputs. - vec3 unit_direction = unit_vector(r.direction()); - auto t = 0.5*(unit_direction.y() + 1.0); - return (1.0-t)*vec3(1.0, 1.0, 1.0) + t*vec3(0.5, 0.7, 1.0); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + inline double linear_to_gamma(double linear_component) + { + if (linear_component > 0) + return std::sqrt(linear_component); + + return 0; } - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [ray-color-hemisphere]: <kbd>[main.cc]</kbd> ray_color() with hemispherical scattering] + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ -Gives us the following image: + void write_color(std::ostream& out, const color& pixel_color) { + auto r = pixel_color.x(); + auto g = pixel_color.y(); + auto b = pixel_color.z(); - <div class="render"> - ![Rendering of diffuse spheres with hemispherical scattering](../images/img.rand-hemispherical.png) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + // Apply a linear to gamma transform for gamma 2 + r = linear_to_gamma(r); + g = linear_to_gamma(g); + b = linear_to_gamma(b); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - </div> + // Translate the [0,1] component values to the byte range [0,255]. + static const interval intensity(0.000, 0.999); + int rbyte = int(256 * intensity.clamp(r)); + int gbyte = int(256 * intensity.clamp(g)); + int bbyte = int(256 * intensity.clamp(b)); -</div> + // Write out the pixel color components. + out << rbyte << ' ' << gbyte << ' ' << bbyte << '\n'; + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [write-color-gamma]: <kbd>[color.h]</kbd> write_color(), with gamma correction] <div class='together'> -Scenes will become more complicated over the course of the book. You are encouraged to -switch between the different diffuse renderers presented here. Most scenes of interest will contain -a disproportionate amount of diffuse materials. You can gain valuable insight by understanding the -effect of different diffuse methods on the lighting of the scene. +Using this gamma correction, we now get a much more consistent ramp from darkness to lightness: + + ![<span class='num'>Image 12:</span> The gamut of our renderer, gamma-corrected + ](../images/img-1.12-gamma-gamut.png class='pixel') + </div> + Metal ==================================================================================================== +An Abstract Class for Materials +-------------------------------- If we want different objects to have different materials, we have a design decision. We could have a -universal material with lots of parameters and different material types just zero out some of those -parameters. This is not a bad approach. Or we could have an abstract material class that -encapsulates behavior. I am a fan of the latter approach. For our program the material needs to do -two things: +universal material type with lots of parameters so any individual material type could just ignore +the parameters that don't affect it. This is not a bad approach. Or we could have an abstract +material class that encapsulates unique behavior. I am a fan of the latter approach. For our program +the material needs to do two things: 1. Produce a scattered ray (or say it absorbed the incident ray). 2. If scattered, say how much the ray should be attenuated. @@ -1747,470 +2809,626 @@ This suggests the abstract class: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #ifndef MATERIAL_H + #define MATERIAL_H + + #include "hittable.h" + class material { - public: - virtual bool scatter( - const ray& r_in, const hit_record& rec, vec3& attenuation, ray& scattered - ) const = 0; + public: + virtual ~material() = default; + + virtual bool scatter( + const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered + ) const { + return false; + } }; + + #endif ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [material-initial]: <kbd>[material.h]</kbd> The material class] -</div> -<div class='together'> -The `hit_record` is to avoid a bunch of arguments so we can stuff whatever info we want in there. -You can use arguments instead; it’s a matter of taste. Hittables and materials need to know each -other so there is some circularity of the references. In C++ you just need to alert the compiler -that the pointer is to a class, which the “class material” in the hittable class below does: +</div> - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - #ifndef HITTABLE_H - #define HITTABLE_H - #include "rtweekend.h" - #include "ray.h" +A Data Structure to Describe Ray-Object Intersections +------------------------------------------------------ +The `hit_record` is to avoid a bunch of arguments so we can stuff whatever info we want in there. +You can use arguments instead of an encapsulated type, it’s just a matter of taste. Hittables and +materials need to be able to reference the other's type in code so there is some circularity of the +references. In C++ we add the line `class material;` to tell the compiler that `material` is a class +that will be defined later. Since we're just specifying a pointer to the class, the compiler doesn't +need to know the details of the class, solving the circular reference issue. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight class material; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - struct hit_record { - vec3 p; + class hit_record { + public: + point3 p; vec3 normal; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - shared_ptr<material> mat_ptr; + shared_ptr<material> mat; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ double t; bool front_face; - - inline void set_face_normal(const ray& r, const vec3& outward_normal) { + void set_face_normal(const ray& r, const vec3& outward_normal) { front_face = dot(r.direction(), outward_normal) < 0; - normal = front_face ? outward_normal :-outward_normal; + normal = front_face ? outward_normal : -outward_normal; } }; - - class hittable { - public: - virtual bool hit(const ray& r, double t_min, double t_max, hit_record& rec) const = 0; - }; - - #endif ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [hit-with-material]: <kbd>[hittable.h]</kbd> Hit record with added material pointer] -</div> -What we have set up here is that material will tell us how rays interact with the surface. -`hit_record` is just a way to stuff a bunch of arguments into a struct so we can send them as a +`hit_record` is just a way to stuff a bunch of arguments into a class so we can send them as a group. When a ray hits a surface (a particular sphere for example), the material pointer in the `hit_record` will be set to point at the material pointer the sphere was given when it was set up in -`main()` when we start. When the `color()` routine gets the `hit_record` it can call member +`main()` when we start. When the `ray_color()` routine gets the `hit_record` it can call member functions of the material pointer to find out what ray, if any, is scattered. <div class='together'> -To achieve this, we must have a reference to the material for our sphere class to returned -within `hit_record`. See the highlighted lines below: +To achieve this, `hit_record` needs to be told the material that is assigned to the sphere. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - class sphere: public hittable { - public: - sphere() {} + class sphere : public hittable { + public: + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + sphere(const point3& center, double radius) : center(center), radius(std::fmax(0,radius)) { + // TODO: Initialize the material pointer `mat`. + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + bool hit(const ray& r, interval ray_t, hit_record& rec) const override { + ... + + rec.t = root; + rec.p = r.at(rec.t); + vec3 outward_normal = (rec.p - center) / radius; + rec.set_face_normal(r, outward_normal); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - sphere(vec3 cen, double r, shared_ptr<material> m) - : center(cen), radius(r), mat_ptr(m) {}; + rec.mat = mat; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - virtual bool hit(const ray& r, double tmin, double tmax, hit_record& rec) const; + return true; + } - public: - vec3 center; - double radius; + private: + point3 center; + double radius; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - shared_ptr<material> mat_ptr; + shared_ptr<material> mat; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [sphere-material]: <kbd>[sphere.h]</kbd> Ray-sphere intersection with added material information] - bool sphere::hit(const ray& r, double t_min, double t_max, hit_record& rec) const { - vec3 oc = r.origin() - center; - auto a = r.direction().length_squared(); - auto half_b = dot(oc, r.direction()); - auto c = oc.length_squared() - radius*radius; - auto discriminant = half_b*half_b - a*c; +</div> + + +Modeling Light Scatter and Reflectance +--------------------------------------- +Here and throughout these books we will use the term _albedo_ (Latin for "whiteness"). Albedo is a +precise technical term in some disciplines, but in all cases it is used to define some form of +_fractional reflectance_. Albedo will vary with material color and (as we will later implement for +glass materials) can also vary with incident viewing direction (the direction of the incoming ray). + +Lambertian (diffuse) reflectance can either always scatter and attenuate light according to its +reflectance $R$, or it can sometimes scatter (with probability $1-R$) with no attenuation (where a +ray that isn't scattered is just absorbed into the material). It could also be a mixture of both +those strategies. We will choose to always scatter, so implementing Lambertian materials becomes a +simple task: - if (discriminant > 0) { - auto root = sqrt(discriminant); - auto temp = (-half_b - root)/a; - if (temp < t_max && temp > t_min) { - rec.t = temp; - rec.p = r.at(rec.t); - vec3 outward_normal = (rec.p - center) / radius; - rec.set_face_normal(r, outward_normal); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - rec.mat_ptr = mat_ptr; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - return true; - } - temp = (-half_b + root) / a; - if (temp < t_max && temp > t_min) { - rec.t = temp; - rec.p = r.at(rec.t); - vec3 outward_normal = (rec.p - center) / radius; - rec.set_face_normal(r, outward_normal); + class material { + ... + }; + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - rec.mat_ptr = mat_ptr; + class lambertian : public material { + public: + lambertian(const color& albedo) : albedo(albedo) {} + + bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) + const override { + auto scatter_direction = rec.normal + random_unit_vector(); + scattered = ray(rec.p, scatter_direction); + attenuation = albedo; + return true; + } + + private: + color albedo; + }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [lambertian-initial]: <kbd>[material.h]</kbd> The new lambertian material class] + +Note the third option: we could scatter with some fixed probability $p$ and have attenuation be +$\mathit{albedo}/p$. Your choice. + +If you read the code above carefully, you'll notice a small chance of mischief. If the random unit +vector we generate is exactly opposite the normal vector, the two will sum to zero, which will +result in a zero scatter direction vector. This leads to bad scenarios later on (infinities and +NaNs), so we need to intercept the condition before we pass it on. + +In service of this, we'll create a new vector method -- `vec3::near_zero()` -- that returns true if +the vector is very close to zero in all dimensions. + +The following changes will use the C++ standard library function `std::fabs`, which returns the +absolute value of its input. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - return true; - } + class vec3 { + ... + + double length_squared() const { + return e[0]*e[0] + e[1]*e[1] + e[2]*e[2]; } - return false; - } + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + bool near_zero() const { + // Return true if the vector is close to zero in all dimensions. + auto s = 1e-8; + return (std::fabs(e[0]) < s) && (std::fabs(e[1]) < s) && (std::fabs(e[2]) < s); + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + ... + }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [sphere-material]: <kbd>[sphere.h]</kbd> Ray-sphere intersection with added material information] -</div> + [Listing [vec3-near-zero]: <kbd>[vec3.h]</kbd> The vec3::near_zero() method] -<div class='together'> -For the Lambertian (diffuse) case we already have, it can either scatter always and attenuate by its -reflectance $R$, or it can scatter with no attenuation but absorb the fraction $1-R$ of the rays, or -it could be a mixture of those strategies. For Lambertian materials we get this simple class: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class lambertian : public material { - public: - lambertian(const vec3& a) : albedo(a) {} - - virtual bool scatter( - const ray& r_in, const hit_record& rec, vec3& attenuation, ray& scattered - ) const { - vec3 scatter_direction = rec.normal + random_unit_vector(); - scattered = ray(rec.p, scatter_direction); - attenuation = albedo; - return true; - } + public: + lambertian(const color& albedo) : albedo(albedo) {} + + bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) + const override { + auto scatter_direction = rec.normal + random_unit_vector(); + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + // Catch degenerate scatter direction + if (scatter_direction.near_zero()) + scatter_direction = rec.normal; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + scattered = ray(rec.p, scatter_direction); + attenuation = albedo; + return true; + } - public: - vec3 albedo; + private: + color albedo; }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [lambertian-initial]: <kbd>[material.h]</kbd> The lambertian material class] -</div> + [Listing [lambertian-catch-zero]: <kbd>[material.h]</kbd> Lambertian scatter, bullet-proof] -Note we could just as well only scatter with some probability $p$ and have attenuation be -$albedo/p$. Your choice. -<div class='together'> -For smooth metals the ray won’t be randomly scattered. The key math is: how does a ray get +Mirrored Light Reflection +-------------------------- +For polished metals the ray won’t be randomly scattered. The key question is: How does a ray get reflected from a metal mirror? Vector math is our friend here: - ![Figure [reflection]: Ray reflection](../images/fig.ray-reflect.jpg) + ![Figure [reflection]: Ray reflection](../images/fig-1.15-reflection.jpg) -</div> +The reflected ray direction in red is just $\mathbf{v} + 2\mathbf{b}$. In our design, $\mathbf{n}$ +is a unit vector (length one), but $\mathbf{v}$ may not be. To get the vector $\mathbf{b}$, we scale +the normal vector by the length of the projection of $\mathbf{v}$ onto $\mathbf{n}$, which is given +by the dot product $\mathbf{v} \cdot \mathbf{n}$. (If $\mathbf{n}$ were not a unit vector, we would +also need to divide this dot product by the length of $\mathbf{n}$.) Finally, because $\mathbf{v}$ +points _into_ the surface, and we want $\mathbf{b}$ to point _out_ of the surface, we need to negate +this projection length. -<div class='together'> -The reflected ray direction in red is just $\vec{\mathbf{V}} + 2\vec{\mathbf{B}}$. In our design, -$\vec{\mathbf{N}}$ is a unit vector, but $\vec{\mathbf{V}}$ may not be. The length of -$\vec{\mathbf{B}}$ should be $\vec{\mathbf{V}} \cdot \vec{\mathbf{N}}$. Because $\vec{\mathbf{V}}$ -points in, we will need a minus sign, yielding: +Putting everything together, we get the following computation of the reflected vector: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - vec3 reflect(const vec3& v, const vec3& n) { + ... + + inline vec3 random_on_hemisphere(const vec3& normal) { + ... + } + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + inline vec3 reflect(const vec3& v, const vec3& n) { return v - 2*dot(v,n)*n; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [vec3-reflect]: <kbd>[vec3.h]</kbd> vec3 reflection function] -</div> <div class='together'> The metal material just reflects rays using that formula: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + ... + + class lambertian : public material { + ... + }; + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight class metal : public material { - public: - metal(const vec3& a) : albedo(a) {} - - virtual bool scatter( - const ray& r_in, const hit_record& rec, vec3& attenuation, ray& scattered - ) const { - vec3 reflected = reflect(unit_vector(r_in.direction()), rec.normal); - scattered = ray(rec.p, reflected); - attenuation = albedo; - return (dot(scattered.direction(), rec.normal) > 0); - } + public: + metal(const color& albedo) : albedo(albedo) {} + + bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) + const override { + vec3 reflected = reflect(r_in.direction(), rec.normal); + scattered = ray(rec.p, reflected); + attenuation = albedo; + return true; + } - public: - vec3 albedo; + private: + color albedo; }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [metal-material]: <kbd>[material.h]</kbd> Metal material with reflectance function] + </div> <div class='together'> -We need to modify the color function to use this: +We need to modify the `ray_color()` function for all of our changes: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - vec3 ray_color(const ray& r, const hittable& world, int depth) { - hit_record rec; + #include "hittable.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + #include "material.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + ... + + class camera { + ... + private: + ... + color ray_color(const ray& r, int depth, const hittable& world) const { + // If we've exceeded the ray bounce limit, no more light is gathered. + if (depth <= 0) + return color(0,0,0); - // If we've exceeded the ray bounce limit, no more light is gathered. - if (depth <= 0) - return vec3(0,0,0); + hit_record rec; - if (world.hit(r, 0.001, infinity, rec)) { + if (world.hit(r, interval(0.001, infinity), rec)) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - ray scattered; - vec3 attenuation; - if (rec.mat_ptr->scatter(r, rec, attenuation, scattered)) - return attenuation * ray_color(scattered, world, depth-1); - return vec3(0,0,0); + ray scattered; + color attenuation; + if (rec.mat->scatter(r, rec, attenuation, scattered)) + return attenuation * ray_color(scattered, depth-1, world); + return color(0,0,0); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - } + } - vec3 unit_direction = unit_vector(r.direction()); - auto t = 0.5*(unit_direction.y() + 1.0); - return (1.0-t)*vec3(1.0, 1.0, 1.0) + t*vec3(0.5, 0.7, 1.0); - } + vec3 unit_direction = unit_vector(r.direction()); + auto a = 0.5*(unit_direction.y() + 1.0); + return (1.0-a)*color(1.0, 1.0, 1.0) + a*color(0.5, 0.7, 1.0); + } + }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [ray-color-scatter]: <kbd>[main.cc]</kbd> Ray color with scattered reflectance] + [Listing [ray-color-scatter]: <kbd>[camera.h]</kbd> Ray color with scattered reflectance] + </div> -<div class='together'> -Now let’s add some metal spheres to our scene: +Now we'll update the `sphere` constructor to initialize the material pointer `mat`: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - int main() { - const int image_width = 200; - const int image_height = 100; - const int samples_per_pixel = 100; - const int max_depth = 50; + class sphere : public hittable { + public: + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + sphere(const point3& center, double radius, shared_ptr<material> mat) + : center(center), radius(std::fmax(0,radius)), mat(mat) {} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - std::cout << "P3\n" << image_width << " " << image_height << "\n255\n"; + ... + }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [sphere-ctor-material]: <kbd>[sphere.h]</kbd> Initializing sphere with a material] + + +A Scene with Metal Spheres +--------------------------- +Now let’s add some metal spheres to our scene: + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include "rtweekend.h" + #include "camera.h" + #include "hittable.h" + #include "hittable_list.h" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - hittable_list world; + #include "material.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include "sphere.h" - world.add(make_shared<sphere>( - vec3(0,0,-1), 0.5, make_shared<lambertian>(vec3(0.7, 0.3, 0.3)))); + int main() { + hittable_list world; - world.add(make_shared<sphere>( - vec3(0,-100.5,-1), 100, make_shared<lambertian>(vec3(0.8, 0.8, 0.0)))); - world.add(make_shared<sphere>(vec3(1,0,-1), 0.5, make_shared<metal>(vec3(0.8, 0.6, 0.2)))); - world.add(make_shared<sphere>(vec3(-1,0,-1), 0.5, make_shared<metal>(vec3(0.8, 0.8, 0.8)))); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + auto material_ground = make_shared<lambertian>(color(0.8, 0.8, 0.0)); + auto material_center = make_shared<lambertian>(color(0.1, 0.2, 0.5)); + auto material_left = make_shared<metal>(color(0.8, 0.8, 0.8)); + auto material_right = make_shared<metal>(color(0.8, 0.6, 0.2)); + + world.add(make_shared<sphere>(point3( 0.0, -100.5, -1.0), 100.0, material_ground)); + world.add(make_shared<sphere>(point3( 0.0, 0.0, -1.2), 0.5, material_center)); + world.add(make_shared<sphere>(point3(-1.0, 0.0, -1.0), 0.5, material_left)); + world.add(make_shared<sphere>(point3( 1.0, 0.0, -1.0), 0.5, material_right)); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ camera cam; - for (int j = image_height-1; j >= 0; --j) { - std::cerr << "\rScanlines remaining: " << j << ' ' << std::flush; - for (int i = 0; i < image_width; ++i) { - vec3 color(0, 0, 0); - for (int s = 0; s < samples_per_pixel; ++s) { - auto u = (i + random_double()) / image_width; - auto v = (j + random_double()) / image_height; - ray r = cam.get_ray(u, v); - color += ray_color(r, world, max_depth); - } - color.write_color(std::cout, samples_per_pixel); - } - } - std::cerr << "\nDone.\n"; + cam.aspect_ratio = 16.0 / 9.0; + cam.image_width = 400; + cam.samples_per_pixel = 100; + cam.max_depth = 50; + + cam.render(world); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [scene-with-metal]: <kbd>[main.cc]</kbd> Scene with metal spheres] -</div> <div class='together'> Which gives: - <div class="render"> - - ![Shiny metal](../images/img.metal-shiny.png) - - </div> + ![<span class='num'>Image 13:</span> Shiny metal + ](../images/img-1.13-metal-shiny.png class='pixel') </div> -<div class='together'> -We can also randomize the reflected direction by using a small sphere and choosing a new endpoint -for the ray: - ![Figure [reflect-fuzzy]: Generating fuzzed reflection rays](../images/fig.reflect-fuzzy.jpg) +Fuzzy Reflection +----------------- +We can also randomize the reflected direction by using a small sphere and choosing a new endpoint +for the ray. We'll use a random point from the surface of a sphere centered on the original +endpoint, scaled by the fuzz factor. -</div> + ![Figure [reflect-fuzzy]: Generating fuzzed reflection rays](../images/fig-1.16-reflect-fuzzy.jpg) -<div class='together'> -The bigger the sphere, the fuzzier the reflections will be. This suggests adding a fuzziness +The bigger the fuzz sphere, the fuzzier the reflections will be. This suggests adding a fuzziness parameter that is just the radius of the sphere (so zero is no perturbation). The catch is that for -big spheres or grazing rays, we may scatter below the surface. We can just have the surface -absorb those. +big spheres or grazing rays, we may scatter below the surface. We can just have the surface absorb +those. + +Also note that in order for the fuzz sphere to make sense, it needs to be consistently scaled +compared to the reflection vector, which can vary in length arbitrarily. To address this, we need to +normalize the reflected ray. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class metal : public material { - public: + public: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - metal(const vec3& a, double f) : albedo(a), fuzz(f < 1 ? f : 1) {} + metal(const color& albedo, double fuzz) : albedo(albedo), fuzz(fuzz < 1 ? fuzz : 1) {} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - virtual bool scatter( - const ray& r_in, const hit_record& rec, vec3& attenuation, ray& scattered - ) const { - vec3 reflected = reflect(unit_vector(r_in.direction()), rec.normal); + bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) + const override { + vec3 reflected = reflect(r_in.direction(), rec.normal); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - scattered = ray(rec.p, reflected + fuzz*random_in_unit_sphere()); + reflected = unit_vector(reflected) + (fuzz * random_unit_vector()); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - attenuation = albedo; - return (dot(scattered.direction(), rec.normal) > 0); - } + scattered = ray(rec.p, reflected); + attenuation = albedo; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + return (dot(scattered.direction(), rec.normal) > 0); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } - public: - vec3 albedo; + private: + color albedo; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - double fuzz; + double fuzz; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [metal-fuzz]: <kbd>[material.h]</kbd> Metal spheres with fuzziness] - -</div> + [Listing [metal-fuzz]: <kbd>[material.h]</kbd> Metal material fuzziness] <div class='together'> We can try that out by adding fuzziness 0.3 and 1.0 to the metals: - <div class="render"> - - ![Fuzzed metal](../images/img.metal-fuzz.png) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + int main() { + ... + auto material_ground = make_shared<lambertian>(color(0.8, 0.8, 0.0)); + auto material_center = make_shared<lambertian>(color(0.1, 0.2, 0.5)); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + auto material_left = make_shared<metal>(color(0.8, 0.8, 0.8), 0.3); + auto material_right = make_shared<metal>(color(0.8, 0.6, 0.2), 1.0); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + ... + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [metal-fuzz-spheres]: <kbd>[main.cc]</kbd> Metal spheres with fuzziness] - </div> + ![<span class='num'>Image 14:</span> Fuzzed metal + ](../images/img-1.14-metal-fuzz.png class='pixel') </div> - Dielectrics ==================================================================================================== - -Clear materials such as water, glass, and diamonds are dielectrics. When a light ray hits them, it +Clear materials such as water, glass, and diamond are dielectrics. When a light ray hits them, it splits into a reflected ray and a refracted (transmitted) ray. We’ll handle that by randomly -choosing between reflection or refraction, and only generating one scattered ray per interaction. +choosing between reflection and refraction, only generating one scattered ray per interaction. -<div class='together'> -The hardest part to debug is the refracted ray. I usually first just have all the light refract if -there is a refraction ray at all. For this project, I tried to put two glass balls in our scene, and -I got this (I have not told you how to do this right or wrong yet, but soon!): +As a quick review of terms, a _reflected_ ray hits a surface and then "bounces" off in a new +direction. - <div class="render"> +A _refracted_ ray bends as it transitions from a material's surroundings into the material itself +(as with glass or water). This is why a pencil looks bent when partially inserted in water. - ![Glass first](../images/img.glass-first.png) +The amount that a refracted ray bends is determined by the material's _refractive index_. Generally, +this is a single value that describes how much light bends when entering a material from a vacuum. +Glass has a refractive index of something like 1.5–1.7, diamond is around 2.4, and air has a +small refractive index of 1.000293. - </div> +When a transparent material is embedded in a different transparent material, you can describe the +refraction with a relative refraction index: the refractive index of the object's material divided +by the refractive index of the surrounding material. For example, if you want to render a glass ball +under water, then the glass ball would have an effective refractive index of 1.125. This is given by +the refractive index of glass (1.5) divided by the refractive index of water (1.333). -</div> +You can find the refractive index of most common materials with a quick internet search. + + +Refraction +----------- +The hardest part to debug is the refracted ray. I usually first just have all the light refract if +there is a refraction ray at all. For this project, I tried to put two glass balls in our scene, and +I got this (I have not told you how to do this right or wrong yet, but soon!): + + ![<span class='num'>Image 15:</span> Glass first + ](../images/img-1.15-glass-first.png class='pixel') Is that right? Glass balls look odd in real life. But no, it isn’t right. The world should be flipped upside down and no weird black stuff. I just printed out the ray straight through the middle of the image and it was clearly wrong. That often does the job. -<div class='together'> + +Snell's Law +------------ The refraction is described by Snell’s law: $$ \eta \cdot \sin\theta = \eta' \cdot \sin\theta' $$ Where $\theta$ and $\theta'$ are the angles from the normal, and $\eta$ and $\eta'$ (pronounced -"eta" and "eta prime") are the refractive indices (typically air = 1.0, glass = 1.3–1.7, diamond = -2.4). The geometry is: +"eta" and "eta prime") are the refractive indices. The geometry is: - ![Figure [ray-refract]: Refracted ray geometry](../images/fig.ray-refract.jpg) - -</div> + ![Figure [refraction]: Ray refraction](../images/fig-1.17-refraction.jpg) -<div class='together'> In order to determine the direction of the refracted ray, we have to solve for $\sin\theta'$: $$ \sin\theta' = \frac{\eta}{\eta'} \cdot \sin\theta $$ On the refracted side of the surface there is a refracted ray $\mathbf{R'}$ and a normal -$\mathbf{N'}$, and there exists an angle, $\theta'$, between them. We can split $\mathbf{R'}$ into -the parts of the ray that are parallel to $\mathbf{N'}$ and perpendicular to $\mathbf{N'}$: +$\mathbf{n'}$, and there exists an angle, $\theta'$, between them. We can split $\mathbf{R'}$ into +the parts of the ray that are perpendicular to $\mathbf{n'}$ and parallel to $\mathbf{n'}$: - $$ \mathbf{R'} = \mathbf{R'}_{\parallel} + \mathbf{R'}_{\bot} $$ + $$ \mathbf{R'} = \mathbf{R'}_{\bot} + \mathbf{R'}_{\parallel} $$ -If we solve for $\mathbf{R'}_{\parallel}$ and $\mathbf{R'}_{\bot}$ we get: +If we solve for $\mathbf{R'}_{\bot}$ and $\mathbf{R'}_{\parallel}$ we get: - $$ \mathbf{R'}_{\parallel} = \frac{\eta}{\eta'} (\mathbf{R} + \cos\theta \mathbf{N}) $$ - $$ \mathbf{R'}_{\bot} = -\sqrt{1 - |\mathbf{R'}_{\parallel}|^2} \mathbf{N} $$ + $$ \mathbf{R'}_{\bot} = \frac{\eta}{\eta'} (\mathbf{R} + |\mathbf{R}| \cos(\theta) \mathbf{n}) $$ + $$ \mathbf{R'}_{\parallel} = -\sqrt{1 - |\mathbf{R'}_{\bot}|^2} \mathbf{n} $$ You can go ahead and prove this for yourself if you want, but we will treat it as fact and move on. The rest of the book will not require you to understand the proof. -We still need to solve for $\cos\theta$. It is well known that the dot product of two vectors can -be explained in terms of the cosine of the angle between them: +We know the value of every term on the right-hand side except for $\cos\theta$. It is well known +that the dot product of two vectors can be explained in terms of the cosine of the angle between +them: - $$ \mathbf{A} \cdot \mathbf{B} = |\mathbf{A}| |\mathbf{B}| \cos\theta $$ + $$ \mathbf{a} \cdot \mathbf{b} = |\mathbf{a}| |\mathbf{b}| \cos\theta $$ -If we restrict $\mathbf{A}$ and $\mathbf{B}$ to be unit vectors: +If we restrict $\mathbf{a}$ and $\mathbf{b}$ to be unit vectors: - $$ \mathbf{A} \cdot \mathbf{B} = \cos\theta $$ + $$ \mathbf{a} \cdot \mathbf{b} = \cos\theta $$ -We can now rewrite $\mathbf{R'}_{\parallel}$ in terms of known quantities: +We can now rewrite $\mathbf{R'}_{\bot}$ in terms of known quantities: - $$ \mathbf{R'}_{\parallel} = - \frac{\eta}{\eta'} (\mathbf{R} + (\mathbf{-R} \cdot \mathbf{N}) \mathbf{N}) $$ + $$ \mathbf{R'}_{\bot} = + \frac{\eta}{\eta'} (\mathbf{R} + (\mathbf{-R} \cdot \mathbf{n}) \mathbf{n}) $$ +<div class='together'> When we combine them back together, we can write a function to calculate $\mathbf{R'}$: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - vec3 refract(const vec3& uv, const vec3& n, double etai_over_etat) { - auto cos_theta = dot(-uv, n); - vec3 r_out_parallel = etai_over_etat * (uv + cos_theta*n); - vec3 r_out_perp = -sqrt(1.0 - r_out_parallel.length_squared()) * n; - return r_out_parallel + r_out_perp; + ... + + inline vec3 reflect(const vec3& v, const vec3& n) { + return v - 2*dot(v,n)*n; + } + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + inline vec3 refract(const vec3& uv, const vec3& n, double etai_over_etat) { + auto cos_theta = std::fmin(dot(-uv, n), 1.0); + vec3 r_out_perp = etai_over_etat * (uv + cos_theta*n); + vec3 r_out_parallel = -std::sqrt(std::fabs(1.0 - r_out_perp.length_squared())) * n; + return r_out_perp + r_out_parallel; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [refract]: <kbd>[vec3.h]</kbd> Refraction function] + </div> <div class='together'> And the dielectric material that always refracts is: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + ... + + class metal : public material { + ... + }; + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight class dielectric : public material { - public: - dielectric(double ri) : ref_idx(ri) {} - - virtual bool scatter( - const ray& r_in, const hit_record& rec, vec3& attenuation, ray& scattered - ) const { - attenuation = vec3(1.0, 1.0, 1.0); - double etai_over_etat; - if (rec.front_face) { - etai_over_etat = 1.0 / ref_idx; - } else { - etai_over_etat = ref_idx; - } + public: + dielectric(double refraction_index) : refraction_index(refraction_index) {} - vec3 unit_direction = unit_vector(r_in.direction()); - vec3 refracted = refract(unit_direction, rec.normal, etai_over_etat); - scattered = ray(rec.p, refracted); - return true; - } + bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) + const override { + attenuation = color(1.0, 1.0, 1.0); + double ri = rec.front_face ? (1.0/refraction_index) : refraction_index; + + vec3 unit_direction = unit_vector(r_in.direction()); + vec3 refracted = refract(unit_direction, rec.normal, ri); + + scattered = ray(rec.p, refracted); + return true; + } - double ref_idx; + private: + // Refractive index in vacuum or air, or the ratio of the material's refractive index over + // the refractive index of the enclosing media + double refraction_index; }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [dielectric]: <kbd>[material.h]</kbd> Dielectric material class that always refracts] + [Listing [dielectric-always-refract]: <kbd>[material.h]</kbd> Dielectric material class that always refracts] - <div class="render"> +</div> - ![Glass sphere that always refracts](../images/img.glass-always-refract.png) +<div class='together'> +Now we'll update the scene to illustrate refraction by changing the left sphere to glass, which has +an index of refraction of approximately 1.5. - </div> -</div> + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + auto material_ground = make_shared<lambertian>(color(0.8, 0.8, 0.0)); + auto material_center = make_shared<lambertian>(color(0.1, 0.2, 0.5)); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + auto material_left = make_shared<dielectric>(1.50); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + auto material_right = make_shared<metal>(color(0.8, 0.6, 0.2), 1.0); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [two-glass]: <kbd>[main.cc]</kbd> Changing the left sphere to glass] +</div> <div class='together'> -That definitely doesn't look right. One troublesome practical issue is that when the ray is in the -material with the higher refractive index, there is no real solution to Snell’s law, and thus there -is no refraction possible. If we refer back to Snell's law and the derivation of $\sin\theta'$: +This gives us the following result: + + ![<span class='num'>Image 16:</span> Glass sphere that always refracts + ](../images/img-1.16-glass-always-refract.png class='pixel') + +</div> + + +Total Internal Reflection +-------------------------- +One troublesome practical issue with refraction is that there are ray angles for which no solution +is possible using Snell's law. When a ray enters a medium of lower index of refraction at a +sufficiently glancing angle, it can refract with an angle greater than 90°. If we refer back to +Snell's law and the derivation of $\sin\theta'$: $$ \sin\theta' = \frac{\eta}{\eta'} \cdot \sin\theta $$ @@ -2218,277 +3436,370 @@ $$ \sin\theta' = \frac{1.5}{1.0} \cdot \sin\theta $$ +<div class='together'> The value of $\sin\theta'$ cannot be greater than 1. So, if, $$ \frac{1.5}{1.0} \cdot \sin\theta > 1.0 $$ -The equality between the two sides of the equation is broken, and a solution cannot exist. If a +the equality between the two sides of the equation is broken, and a solution cannot exist. If a solution does not exist, the glass cannot refract, and therefore must reflect the ray: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - if(etai_over_etat * sin_theta > 1.0) { + if (ri * sin_theta > 1.0) { // Must Reflect ... - } - else { + } else { // Can Refract ... } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [dielectric]: <kbd>[material.h]</kbd> Determining if the ray can refract] + [Listing [dielectric-can-refract-1]: <kbd>[material.h]</kbd> Determining if the ray can refract] -Here all the light is reflected, and because in practice that is usually inside solid objects, it -is called “total internal reflection”. This is why sometimes the water-air boundary acts as a -perfect mirror when you are submerged. </div> -<div class='together'> -We can solve for `sin_theta` using the trigonometric qualities: +Here all the light is reflected, and because in practice that is usually inside solid objects, it is +called _total internal reflection_. This is why sometimes the water-to-air boundary acts as a +perfect mirror when you are submerged -- if you're under water looking up, you can see things above +the water, but when you are close to the surface and looking sideways, the water surface looks like +a mirror. + +We can solve for `sin_theta` using the trigonometric identities: $$ \sin\theta = \sqrt{1 - \cos^2\theta} $$ and - $$ \cos\theta = \mathbf{R} \cdot \mathbf{N} $$ + $$ \cos\theta = \mathbf{R} \cdot \mathbf{n} $$ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - double cos_theta = ffmin(dot(-unit_direction, rec.normal), 1.0); - double sin_theta = sqrt(1.0 - cos_theta*cos_theta); - if(etai_over_etat * sin_theta > 1.0) { + double cos_theta = std::fmin(dot(-unit_direction, rec.normal), 1.0); + double sin_theta = std::sqrt(1.0 - cos_theta*cos_theta); + + if (ri * sin_theta > 1.0) { // Must Reflect ... - } - else { + } else { // Can Refract ... } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [dielectric]: <kbd>[material.h]</kbd> Determining if the ray can refract] - -</div> + [Listing [dielectric-can-refract-2]: <kbd>[material.h]</kbd> Determining if the ray can refract] <div class='together'> And the dielectric material that always refracts (when possible) is: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class dielectric : public material { - public: - dielectric(double ri) : ref_idx(ri) {} - - virtual bool scatter( - const ray& r_in, const hit_record& rec, vec3& attenuation, ray& scattered - ) const { - attenuation = vec3(1.0, 1.0, 1.0); - double etai_over_etat = (rec.front_face) ? (1.0 / ref_idx) : (ref_idx); - - vec3 unit_direction = unit_vector(r_in.direction()); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - double cos_theta = ffmin(dot(-unit_direction, rec.normal), 1.0); - double sin_theta = sqrt(1.0 - cos_theta*cos_theta); - if (etai_over_etat * sin_theta > 1.0 ) { - vec3 reflected = reflect(unit_direction, rec.normal); - scattered = ray(rec.p, reflected); - return true; - } + public: + dielectric(double refraction_index) : refraction_index(refraction_index) {} + + bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) + const override { + attenuation = color(1.0, 1.0, 1.0); + double ri = rec.front_face ? (1.0/refraction_index) : refraction_index; + + vec3 unit_direction = unit_vector(r_in.direction()); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + double cos_theta = std::fmin(dot(-unit_direction, rec.normal), 1.0); + double sin_theta = std::sqrt(1.0 - cos_theta*cos_theta); - vec3 refracted = refract(unit_direction, rec.normal, etai_over_etat); - scattered = ray(rec.p, refracted); - return true; + bool cannot_refract = ri * sin_theta > 1.0; + vec3 direction; + + if (cannot_refract) + direction = reflect(unit_direction, rec.normal); + else + direction = refract(unit_direction, rec.normal, ri); + + scattered = ray(rec.p, direction); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - } + return true; + } + + private: + // Refractive index in vacuum or air, or the ratio of the material's refractive index over + // the refractive index of the enclosing media + double refraction_index; + }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [dielectric-with-refraction]: <kbd>[material.h]</kbd> Dielectric material class with reflection] + +</div> + +Attenuation is always 1 -- the glass surface absorbs nothing. + +If we render the prior scene with the new `dielectric::scatter()` function, we see … no change. Huh? + +Well, it turns out that given a sphere of material with an index of refraction greater than air, +there's no incident angle that will yield total internal reflection -- neither at the ray-sphere +entrance point nor at the ray exit. This is due to the geometry of spheres, as a grazing incoming +ray will always be bent to a smaller angle, and then bent back to the original angle on exit. + +So how can we illustrate total internal reflection? Well, if the sphere has an index of refraction +_less_ than the medium it's in, then we can hit it with shallow grazing angles, getting total +_external_ reflection. That should be good enough to observe the effect. + +We'll model a world filled with water (index of refraction approximately 1.33), and change the +sphere material to air (index of refraction 1.00) -- an air bubble! To do this, change the left +sphere material's index of refraction to + + $$\frac{\text{index of refraction of air}}{\text{index of refraction of water}}$$ + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + auto material_ground = make_shared<lambertian>(color(0.8, 0.8, 0.0)); + auto material_center = make_shared<lambertian>(color(0.1, 0.2, 0.5)); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + auto material_left = make_shared<dielectric>(1.00 / 1.33); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + auto material_right = make_shared<metal>(color(0.8, 0.6, 0.2), 1.0); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [two-glass]: <kbd>[main.cc]</kbd> Left sphere is an air bubble in water] + +<div class='together'> +This change yields the following render: + + ![<span class='num'>Image 17:</span> Air bubble sometimes refracts, sometimes reflects + ](../images/img-1.17-air-bubble-total-reflection.png class='pixel') + +Here you can see that more-or-less direct rays refract, while glancing rays reflect. - public: - double ref_idx; - }; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [dielectric]: <kbd>[material.h]</kbd> Dielectric material class with reflection] </div> -<div class='together'> -Attenuation is always 1 -- the glass surface absorbs nothing. If we try that out with these parameters: + +Schlick Approximation +---------------------- +Now real glass has reflectivity that varies with angle -- look at a window at a steep angle and it +becomes a mirror. There is a big ugly equation for that, but almost everybody uses a cheap and +surprisingly accurate polynomial approximation by Christophe Schlick. This yields our full glass +material: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - world.add(make_shared<sphere>( - vec3(0,0,-1), 0.5, make_shared<lambertian>(vec3(0.1, 0.2, 0.5)))); + class dielectric : public material { + public: + dielectric(double refraction_index) : refraction_index(refraction_index) {} - world.add(make_shared<sphere>( - vec3(0,-100.5,-1), 100, make_shared<lambertian>(vec3(0.8, 0.8, 0.0)))); + bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) + const override { + attenuation = color(1.0, 1.0, 1.0); + double ri = rec.front_face ? (1.0/refraction_index) : refraction_index; - world.add(make_shared<sphere>(vec3(1,0,-1), 0.5, make_shared<metal>(vec3(0.8, 0.6, 0.2), 0.0))); - world.add(make_shared<sphere>(vec3(-1,0,-1), 0.5, make_shared<dielectric>(1.5))); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [scene-dielectric]: <kbd>[main.cc]</kbd> Scene with dielectric sphere] + vec3 unit_direction = unit_vector(r_in.direction()); + double cos_theta = std::fmin(dot(-unit_direction, rec.normal), 1.0); + double sin_theta = std::sqrt(1.0 - cos_theta*cos_theta); -We get: + bool cannot_refract = ri * sin_theta > 1.0; + vec3 direction; - <div class="render"> - ![Glass sphere that sometimes refracts](../images/img.glass-sometimes-refract.png) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + if (cannot_refract || reflectance(cos_theta, ri) > random_double()) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + direction = reflect(unit_direction, rec.normal); + else + direction = refract(unit_direction, rec.normal, ri); - </div> -</div> + scattered = ray(rec.p, direction); + return true; + } -<div class='together'> -Now real glass has reflectivity that varies with angle -- look at a window at a steep angle and it -becomes a mirror. There is a big ugly equation for that, but almost everybody uses a simple and -surprisingly simple polynomial approximation by Christophe Schlick: + private: + // Refractive index in vacuum or air, or the ratio of the material's refractive index over + // the refractive index of the enclosing media + double refraction_index; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - double schlick(double cosine, double ref_idx) { - auto r0 = (1-ref_idx) / (1+ref_idx); - r0 = r0*r0; - return r0 + (1-r0)*pow((1 - cosine),5); - } - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [schlick]: <kbd>[material.h]</kbd> Schlick approximation] -</div> -<div class='together'> -This yields our full glass material: - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - class dielectric : public material { - public: - dielectric(double ri) : ref_idx(ri) {} - - virtual bool scatter( - const ray& r_in, const hit_record& rec, vec3& attenuation, ray& scattered - ) const { - attenuation = vec3(1.0, 1.0, 1.0); - double etai_over_etat = (rec.front_face) ? (1.0 / ref_idx) : (ref_idx); - - vec3 unit_direction = unit_vector(r_in.direction()); - double cos_theta = ffmin(dot(-unit_direction, rec.normal), 1.0); - double sin_theta = sqrt(1.0 - cos_theta*cos_theta); - if (etai_over_etat * sin_theta > 1.0 ) { - vec3 reflected = reflect(unit_direction, rec.normal); - scattered = ray(rec.p, reflected); - return true; - } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - double reflect_prob = schlick(cos_theta, etai_over_etat); - if (random_double() < reflect_prob) - { - vec3 reflected = reflect(unit_direction, rec.normal); - scattered = ray(rec.p, reflected); - return true; - } + static double reflectance(double cosine, double refraction_index) { + // Use Schlick's approximation for reflectance. + auto r0 = (1 - refraction_index) / (1 + refraction_index); + r0 = r0*r0; + return r0 + (1-r0)*std::pow((1 - cosine),5); + } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - vec3 refracted = refract(unit_direction, rec.normal, etai_over_etat); - scattered = ray(rec.p, refracted); - return true; - } - - public: - double ref_idx; }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [glass]: <kbd>[material.h]</kbd> Full glass material] -</div> -<div class='together'> -An interesting and easy trick with dielectric spheres is to note that if you use a negative radius, -the geometry is unaffected, but the surface normal points inward. This can be used as a bubble to -make a hollow glass sphere: +Modeling a Hollow Glass Sphere +------------------------------- +Let's model a hollow glass sphere. This is a sphere of some thickness with another sphere of air +inside it. If you think about the path of a ray going through such an object, it will hit the outer +sphere, refract, hit the inner sphere (assuming we do hit it), refract a second time, and travel +through the air inside. Then it will continue on, hit the inside surface of the inner sphere, +refract back, then hit the inside surface of the outer sphere, and finally refract and exit back +into the scene atmosphere. + +The outer sphere is just modeled with a standard glass sphere, with a refractive index of around +1.50 (modeling a refraction from the outside air into glass). The inner sphere is a bit different +because _its_ refractive index should be relative to the material of the surrounding outer sphere, +thus modeling a transition from glass into the inner air. + +This is actually simple to specify, as the `refraction_index` parameter to the dielectric material +can be interpreted as the _ratio_ of the refractive index of the object divided by the refractive +index of the enclosing medium. In this case, the inner sphere would have an refractive index of air +(the inner sphere material) over the index of refraction of glass (the enclosing medium), or +$1.00/1.50 = 0.67$. + +Here's the code: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + ... + auto material_ground = make_shared<lambertian>(color(0.8, 0.8, 0.0)); + auto material_center = make_shared<lambertian>(color(0.1, 0.2, 0.5)); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + auto material_left = make_shared<dielectric>(1.50); + auto material_bubble = make_shared<dielectric>(1.00 / 1.50); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - world.add(make_shared<sphere>(vec3(0,0,-1), 0.5, make_shared<lambertian>(vec3(0.1, 0.2, 0.5)))); - world.add(make_shared<sphere>( - vec3(0,-100.5,-1), 100, make_shared<lambertian>(vec3(0.8, 0.8, 0.0)))); - world.add(make_shared<sphere>(vec3(1,0,-1), 0.5, make_shared<metal>(vec3(0.8, 0.6, 0.2), 0.3))); - world.add(make_shared<sphere>(vec3(-1,0,-1), 0.5, make_shared<dielectric>(1.5))); - world.add(make_shared<sphere>(vec3(-1,0,-1), -0.45, make_shared<dielectric>(1.5))); + auto material_right = make_shared<metal>(color(0.8, 0.6, 0.2), 0.0); + + world.add(make_shared<sphere>(point3( 0.0, -100.5, -1.0), 100.0, material_ground)); + world.add(make_shared<sphere>(point3( 0.0, 0.0, -1.2), 0.5, material_center)); + world.add(make_shared<sphere>(point3(-1.0, 0.0, -1.0), 0.5, material_left)); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + world.add(make_shared<sphere>(point3(-1.0, 0.0, -1.0), 0.4, material_bubble)); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + world.add(make_shared<sphere>(point3( 1.0, 0.0, -1.0), 0.5, material_right)); + ... ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [scene-hollow-glass]: <kbd>[main.cc]</kbd> Scene with hollow glass sphere] -</div> <div class='together'> -This gives: - - <div class="render"> - - ![A hollow glass sphere](../images/img.glass-hollow.png) +And here's the result: - </div> + ![<span class='num'>Image 18:</span> A hollow glass sphere + ](../images/img-1.18-glass-hollow.png class='pixel') </div> + Positionable Camera ==================================================================================================== +Cameras, like dielectrics, are a pain to debug, so I always develop mine incrementally. First, let’s +allow for an adjustable field of view (_fov_). This is the visual angle from edge to edge of the +rendered image. Since our image is not square, the fov is different horizontally and vertically. I +always use vertical fov. I also usually specify it in degrees and change to radians inside a +constructor -- a matter of personal taste. -Cameras, like dielectrics, are a pain to debug. So I always develop mine incrementally. First, let’s -allow an adjustable field of view (_fov_). This is the angle you see through the portal. Since our -image is not square, the fov is different horizontally and vertically. I always use vertical fov. I -also usually specify it in degrees and change to radians inside a constructor -- a matter of -personal taste. - -<div class='together'> -I first keep the rays coming from the origin and heading to the $z = -1$ plane. We could make it the -$z = -2$ plane, or whatever, as long as we made $h$ a ratio to that distance. Here is our setup: - ![Figure [cam-view-geom]: Camera viewing geometry](../images/fig.cam-view-geom.jpg) +Camera Viewing Geometry +------------------------ +First, we'll keep the rays coming from the origin and heading to the $z = -1$ plane. We could make +it the $z = -2$ plane, or whatever, as long as we made $h$ a ratio to that distance. Here is our +setup: -</div> + ![Figure [cam-view-geom]: Camera viewing geometry (from the side) + ](../images/fig-1.18-cam-view-geom.jpg) <div class='together'> This implies $h = \tan(\frac{\theta}{2})$. Our camera now becomes: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class camera { - public: + public: + double aspect_ratio = 1.0; // Ratio of image width over height + int image_width = 100; // Rendered image width in pixel count + int samples_per_pixel = 10; // Count of random samples for each pixel + int max_depth = 10; // Maximum number of ray bounces into scene + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - camera( - double vfov, // top to bottom, in degrees - double aspect - ) { - origin = vec3(0.0, 0.0, 0.0); + double vfov = 90; // Vertical view angle (field of view) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - auto theta = degrees_to_radians(vfov); - auto half_height = tan(theta/2); - auto half_width = aspect * half_height; + void render(const hittable& world) { + ... + + private: + ... - lower_left_corner = vec3(-half_width, -half_height, -1.0); + void initialize() { + image_height = int(image_width / aspect_ratio); + image_height = (image_height < 1) ? 1 : image_height; - horizontal = vec3(2*half_width, 0.0, 0.0); - vertical = vec3(0.0, 2*half_height, 0.0); - } + pixel_samples_scale = 1.0 / samples_per_pixel; + + center = point3(0, 0, 0); + + // Determine viewport dimensions. + auto focal_length = 1.0; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + auto theta = degrees_to_radians(vfov); + auto h = std::tan(theta/2); + auto viewport_height = 2 * h * focal_length; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + auto viewport_width = viewport_height * (double(image_width)/image_height); - ray get_ray(double u, double v) { - return ray(origin, lower_left_corner + u*horizontal + v*vertical - origin); - } + // Calculate the vectors across the horizontal and down the vertical viewport edges. + auto viewport_u = vec3(viewport_width, 0, 0); + auto viewport_v = vec3(0, -viewport_height, 0); - public: - vec3 origin; - vec3 lower_left_corner; - vec3 horizontal; - vec3 vertical; + // Calculate the horizontal and vertical delta vectors from pixel to pixel. + pixel_delta_u = viewport_u / image_width; + pixel_delta_v = viewport_v / image_height; + + // Calculate the location of the upper left pixel. + auto viewport_upper_left = + center - vec3(0, 0, focal_length) - viewport_u/2 - viewport_v/2; + pixel00_loc = viewport_upper_left + 0.5 * (pixel_delta_u + pixel_delta_v); + } + + ... }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [camera-fov]: <kbd>[camera.h]</kbd> Camera with adjustable field-of-view (fov)] + </div> <div class='together'> -When calling it with camera `cam(90, double(image_width)/image_height)` and these spheres: +We'll test out these changes with a simple scene of two touching spheres, using a 90° field of view. + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + int main() { + hittable_list world; + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + auto R = std::cos(pi/4); + auto material_left = make_shared<lambertian>(color(0,0,1)); + auto material_right = make_shared<lambertian>(color(1,0,0)); + + world.add(make_shared<sphere>(point3(-R, 0, -1), R, material_left)); + world.add(make_shared<sphere>(point3( R, 0, -1), R, material_right)); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + camera cam; + + cam.aspect_ratio = 16.0 / 9.0; + cam.image_width = 400; + cam.samples_per_pixel = 100; + cam.max_depth = 50; + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + cam.vfov = 90; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - auto R = cos(pi/4); - hittable_list world; - world.add(make_shared<sphere>(vec3(-R,0,-1), R, make_shared<lambertian>(vec3(0, 0, 1)))); - world.add(make_shared<sphere>(vec3( R,0,-1), R, make_shared<lambertian>(vec3(1, 0, 0)))); + + cam.render(world); + } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [scene-wide-angle]: <kbd>[main.cc]</kbd> Scene with wide-angle camera] -gives: - - <div class="render"> +</div> - ![A wide-angle view](../images/img.wide-view.png) +<div class='together'> +This gives us the rendering: - </div> + ![<span class='num'>Image 19:</span> A wide-angle view + ](../images/img-1.19-wide-view.png class='pixel') </div> + +Positioning and Orienting the Camera +------------------------------------- To get an arbitrary viewpoint, let’s first name the points we care about. We’ll call the position where we place the camera _lookfrom_, and the point we look at _lookat_. (Later, if you want, you could define a direction to look in instead of a point to look at.) @@ -2496,90 +3807,171 @@ We also need a way to specify the roll, or sideways tilt, of the camera: the rotation around the lookat-lookfrom axis. Another way to think about it is that even if you keep `lookfrom` and `lookat` constant, you can still rotate your head around your nose. What we need is a way to specify an “up” -vector for the camera. This up vector should lie in the plane orthogonal to the view direction. +vector for the camera. - ![Figure [cam-look]: Camera view direction](../images/fig.cam-look.jpg) + ![Figure [cam-view-dir]: Camera view direction](../images/fig-1.19-cam-view-dir.jpg) -We can actually use any up vector we want, and simply project it onto this plane to get an up vector -for the camera. I use the common convention of naming a “view up” (_vup_) vector. A couple of cross -products, and we now have a complete orthonormal basis (u,v,w) to describe our camera’s orientation. +We can specify any up vector we want, as long as it's not parallel to the view direction. Project +this up vector onto the plane orthogonal to the view direction to get a camera-relative up vector. I +use the common convention of naming this the “view up” (_vup_) vector. After a few cross products +and vector normalizations, we now have a complete orthonormal basis $(u,v,w)$ to describe our +camera’s orientation. $u$ will be the unit vector pointing to camera right, $v$ is the unit vector +pointing to camera up, $w$ is the unit vector pointing opposite the view direction (since we use +right-hand coordinates), and the camera center is at the origin. - ![Figure [cam-up]: Camera view up direction](../images/fig.cam-up.jpg) + ![Figure [cam-view-up]: Camera view up direction](../images/fig-1.20-cam-view-up.jpg) -Remember that `vup`, `v`, and `w` are all in the same plane. Note that, like before when our fixed -camera faced -Z, our arbitrary view camera faces -w. And keep in mind that we can -- but we don’t -have to -- use world up (0,1,0) to specify vup. This is convenient and will naturally keep your -camera horizontally level until you decide to experiment with crazy camera angles. +Like before, when our fixed camera faced $-Z$, our arbitrary view camera faces $-w$. Keep in mind +that we can -- but we don’t have to -- use world up $(0,1,0)$ to specify vup. This is convenient and +will naturally keep your camera horizontally level until you decide to experiment with crazy camera +angles. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class camera { - public: - camera( + public: + double aspect_ratio = 1.0; // Ratio of image width over height + int image_width = 100; // Rendered image width in pixel count + int samples_per_pixel = 10; // Count of random samples for each pixel + int max_depth = 10; // Maximum number of ray bounces into scene + + double vfov = 90; // Vertical view angle (field of view) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + point3 lookfrom = point3(0,0,0); // Point camera is looking from + point3 lookat = point3(0,0,-1); // Point camera is looking at + vec3 vup = vec3(0,1,0); // Camera-relative "up" direction + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + ... + + private: + int image_height; // Rendered image height + double pixel_samples_scale; // Color scale factor for a sum of pixel samples + point3 center; // Camera center + point3 pixel00_loc; // Location of pixel 0, 0 + vec3 pixel_delta_u; // Offset to pixel to the right + vec3 pixel_delta_v; // Offset to pixel below + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + vec3 u, v, w; // Camera frame basis vectors + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + void initialize() { + image_height = int(image_width / aspect_ratio); + image_height = (image_height < 1) ? 1 : image_height; + + pixel_samples_scale = 1.0 / samples_per_pixel; + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - vec3 lookfrom, vec3 lookat, vec3 vup, + center = lookfrom; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - double vfov, // top to bottom, in degrees - double aspect - ) { + + // Determine viewport dimensions. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - origin = lookfrom; + auto focal_length = (lookfrom - lookat).length(); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - vec3 u, v, w; + auto theta = degrees_to_radians(vfov); + auto h = std::tan(theta/2); + auto viewport_height = 2 * h * focal_length; + auto viewport_width = viewport_height * (double(image_width)/image_height); - auto theta = degrees_to_radians(vfov); - auto half_height = tan(theta/2); - auto half_width = aspect * half_height; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - w = unit_vector(lookfrom - lookat); - u = unit_vector(cross(vup, w)); - v = cross(w, u); + // Calculate the u,v,w unit basis vectors for the camera coordinate frame. + w = unit_vector(lookfrom - lookat); + u = unit_vector(cross(vup, w)); + v = cross(w, u); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + // Calculate the vectors across the horizontal and down the vertical viewport edges. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + vec3 viewport_u = viewport_width * u; // Vector across viewport horizontal edge + vec3 viewport_v = viewport_height * -v; // Vector down viewport vertical edge + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - lower_left_corner = origin - half_width*u - half_height*v - w; + // Calculate the horizontal and vertical delta vectors from pixel to pixel. + pixel_delta_u = viewport_u / image_width; + pixel_delta_v = viewport_v / image_height; - horizontal = 2*half_width*u; - vertical = 2*half_height*v; + // Calculate the location of the upper left pixel. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + auto viewport_upper_left = center - (focal_length * w) - viewport_u/2 - viewport_v/2; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - } + pixel00_loc = viewport_upper_left + 0.5 * (pixel_delta_u + pixel_delta_v); + } - ray get_ray(double s, double t) { - return ray(origin, lower_left_corner + s*horizontal + t*vertical - origin); - } + ... - public: - vec3 origin; - vec3 lower_left_corner; - vec3 horizontal; - vec3 vertical; + private: }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [camera-orient]: <kbd>[camera.h]</kbd> Positionable and orientable camera] <div class='together'> -This allows us to change the viewpoint: +We'll change back to the prior scene, and use the new viewpoint: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - const auto aspect_ratio = double(image_width) / image_height; - ... - camera cam(vec3(-2,2,1), vec3(0,0,-1), vup, 90, aspect_ratio); + int main() { + hittable_list world; + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + auto material_ground = make_shared<lambertian>(color(0.8, 0.8, 0.0)); + auto material_center = make_shared<lambertian>(color(0.1, 0.2, 0.5)); + auto material_left = make_shared<dielectric>(1.50); + auto material_bubble = make_shared<dielectric>(1.00 / 1.50); + auto material_right = make_shared<metal>(color(0.8, 0.6, 0.2), 1.0); + + world.add(make_shared<sphere>(point3( 0.0, -100.5, -1.0), 100.0, material_ground)); + world.add(make_shared<sphere>(point3( 0.0, 0.0, -1.2), 0.5, material_center)); + world.add(make_shared<sphere>(point3(-1.0, 0.0, -1.0), 0.5, material_left)); + world.add(make_shared<sphere>(point3(-1.0, 0.0, -1.0), 0.4, material_bubble)); + world.add(make_shared<sphere>(point3( 1.0, 0.0, -1.0), 0.5, material_right)); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + camera cam; + + cam.aspect_ratio = 16.0 / 9.0; + cam.image_width = 400; + cam.samples_per_pixel = 100; + cam.max_depth = 50; + + + cam.vfov = 90; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + cam.lookfrom = point3(-2,2,1); + cam.lookat = point3(0,0,-1); + cam.vup = vec3(0,1,0); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + cam.render(world); + } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [scene-free-view]: <kbd>[main.cc]</kbd> Scene with alternate viewpoint] +</div> + +<div class='together'> to get: - <div class="render"> + ![<span class='num'>Image 20:</span> A distant view + ](../images/img-1.20-view-distant.png class='pixel') - ![A distant view](../images/img.view-distant.png) +</div> - </div> +<div class='together'> +And we can change field of view: -And we can change field of view to get: + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + cam.vfov = 20; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [change-field-view]: <kbd>[main.cc]</kbd> Change field of view] - <div class="render"> +</div> - ![Zooming in](../images/img.view-zoom.png) +<div class='together'> +to get: - </div> + ![<span class='num'>Image 21:</span> Zooming in](../images/img-1.21-view-zoom.png class='pixel') </div> @@ -2587,141 +3979,259 @@ Defocus Blur ==================================================================================================== - -Now our final feature: defocus blur. Note, all photographers will call it “depth of field” so be -aware of only using “defocus blur” among friends. - -The reason we defocus blur in real cameras is because they need a big hole (rather than just a -pinhole) to gather light. This would defocus everything, but if we stick a lens in the hole, there -will be a certain distance where everything is in focus. You can think of a lens this way: all light -rays coming _from_ a specific point at the focal distance -- and that hit the lens -- will be bent -back _to_ a single point on the image sensor. - -In a physical camera, the distance to that plane where things are in focus is controlled by the -distance between the lens and the film/sensor. That is why you see the lens move relative to the -camera when you change what is in focus (that may happen in your phone camera too, but the sensor -moves). The “aperture” is a hole to control how big the lens is effectively. For a real camera, if -you need more light you make the aperture bigger, and will get more defocus blur. For our virtual -camera, we can have a perfect sensor and never need more light, so we only have an aperture when we +Now our final feature: _defocus blur_. Note, photographers call this _depth of field_, so be sure to +only use the term _defocus blur_ among your raytracing friends. + +The reason we have defocus blur in real cameras is because they need a big hole (rather than just a +pinhole) through which to gather light. A large hole would defocus everything, but if we stick a +lens in front of the film/sensor, there will be a certain distance at which everything is in focus. +Objects placed at that distance will appear in focus and will linearly appear blurrier the further +they are from that distance. You can think of a lens this way: all light rays coming _from_ a +specific point at the focus distance -- and that hit the lens -- will be bent back _to_ a single +point on the image sensor. + +We call the distance between the camera center and the plane where everything is in perfect focus +the _focus distance_. Be aware that the focus distance is not usually the same as the focal length +-- the _focal length_ is the distance between the camera center and the image plane. For our model, +however, these two will have the same value, as we will put our pixel grid right on the focus plane, +which is _focus distance_ away from the camera center. + +In a physical camera, the focus distance is controlled by the distance between the lens and the +film/sensor. That is why you see the lens move relative to the camera when you change what is in +focus (that may happen in your phone camera too, but the sensor moves). The “aperture” is a hole to +control how big the lens is effectively. For a real camera, if you need more light you make the +aperture bigger, and will get more blur for objects away from the focus distance. For our virtual +camera, we can have a perfect sensor and never need more light, so we only use an aperture when we want defocus blur. -<div class="together"> -A real camera has a complicated compound lens. For our code we could simulate the order: sensor, + +A Thin Lens Approximation +-------------------------- +A real camera has a complicated compound lens. For our code, we could simulate the order: sensor, then lens, then aperture. Then we could figure out where to send the rays, and flip the image after it's computed (the image is projected upside down on the film). Graphics people, however, usually use a thin lens approximation: - ![Figure [cam-lens]: Camera lens model](../images/fig.cam-lens.jpg) + ![Figure [cam-lens]: Camera lens model](../images/fig-1.21-cam-lens.jpg) -</div> +We don’t need to simulate any of the inside of the camera -- for the purposes of rendering an image +outside the camera, that would be unnecessary complexity. Instead, I usually start rays from an +infinitely thin circular "lens", and send them toward the pixel of interest on the focus plane +(`focal_length` away from the lens), where everything on that plane in the 3D world is in perfect +focus. -<div class="together"> -We don’t need to simulate any of the inside of the camera. For the purposes of rendering an image -outside the camera, that would be unnecessary complexity. Instead, I usually start rays from the -surface of the lens, and send them toward a virtual film plane, by finding the projection of the -film on the plane that is in focus (at the distance `focus_dist`). +In practice, we accomplish this by placing the viewport in this plane. Putting everything together: - ![Figure [cam-film-plane]: Camera focus plane](../images/fig.cam-film-plane.jpg) + 1. The focus plane is orthogonal to the camera view direction. + 2. The focus distance is the distance between the camera center and the focus plane. + 3. The viewport lies on the focus plane, centered on the camera view direction vector. + 4. The grid of pixel locations lies inside the viewport (located in the 3D world). + 5. Random image sample locations are chosen from the region around the current pixel location. + 6. The camera fires rays from random points on the lens through the current image sample location. + + ![Figure [cam-film-plane]: Camera focus plane](../images/fig-1.22-cam-film-plane.jpg) -</div> -Normally, all scene rays originate from the `lookfrom` point. In order to accomplish defocus blur, -generate random scene rays originating from inside a disk centered at the `lookfrom` point. The -larger the radius, the greater the defocus blur. You can think of our original camera as having a -defocus disk of radius zero (no blur at all), so all rays originated at the disk center -(`lookfrom`). +Generating Sample Rays +----------------------- +Without defocus blur, all scene rays originate from the camera center (or `lookfrom`). In order to +accomplish defocus blur, we construct a disk centered at the camera center. The larger the radius, +the greater the defocus blur. You can think of our original camera as having a defocus disk of +radius zero (no blur at all), so all rays originated at the disk center (`lookfrom`). + +So, how large should the defocus disk be? Since the size of this disk controls how much defocus blur +we get, that should be a parameter of the camera class. We could just take the radius of the disk as +a camera parameter, but the blur would vary depending on the projection distance. A slightly easier +parameter is to specify the angle of the cone with apex at viewport center and base (defocus disk) +at the camera center. This should give you more consistent results as you vary the focus distance +for a given shot. + +Since we'll be choosing random points from the defocus disk, we'll need a function to do that: +`random_in_unit_disk()`. This function works using the same kind of method we use in +`random_unit_vector()`, just for two dimensions. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - vec3 random_in_unit_disk() { + ... + + inline vec3 unit_vector(const vec3& u) { + return v / v.length(); + } + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + inline vec3 random_in_unit_disk() { while (true) { auto p = vec3(random_double(-1,1), random_double(-1,1), 0); - if (p.length_squared() >= 1) continue; - return p; + if (p.length_squared() < 1) + return p; } } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + ... ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [rand-in-unit-disk]: <kbd>[vec3.h]</kbd> Generate random point inside unit disk] +Now let's update the camera to originate rays from the defocus disk: + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class camera { - public: - camera( - vec3 lookfrom, vec3 lookat, vec3 vup, - double vfov, // top to bottom, in degrees + public: + double aspect_ratio = 1.0; // Ratio of image width over height + int image_width = 100; // Rendered image width in pixel count + int samples_per_pixel = 10; // Count of random samples for each pixel + int max_depth = 10; // Maximum number of ray bounces into scene + + double vfov = 90; // Vertical view angle (field of view) + point3 lookfrom = point3(0,0,0); // Point camera is looking from + point3 lookat = point3(0,0,-1); // Point camera is looking at + vec3 vup = vec3(0,1,0); // Camera-relative "up" direction + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + double defocus_angle = 0; // Variation angle of rays through each pixel + double focus_dist = 10; // Distance from camera lookfrom point to plane of perfect focus + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + ... + + private: + int image_height; // Rendered image height + double pixel_samples_scale; // Color scale factor for a sum of pixel samples + point3 center; // Camera center + point3 pixel00_loc; // Location of pixel 0, 0 + vec3 pixel_delta_u; // Offset to pixel to the right + vec3 pixel_delta_v; // Offset to pixel below + vec3 u, v, w; // Camera frame basis vectors ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - double aspect, double aperture, double focus_dist - ) { - origin = lookfrom; - lens_radius = aperture / 2; + vec3 defocus_disk_u; // Defocus disk horizontal radius + vec3 defocus_disk_v; // Defocus disk vertical radius ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - auto theta = degrees_to_radians(vfov); - auto half_height = tan(theta/2); - auto half_width = aspect * half_height; + void initialize() { + image_height = int(image_width / aspect_ratio); + image_height = (image_height < 1) ? 1 : image_height; - w = unit_vector(lookfrom - lookat); - u = unit_vector(cross(vup, w)); - v = cross(w, u); + pixel_samples_scale = 1.0 / samples_per_pixel; + center = lookfrom; + + // Determine viewport dimensions. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete + auto focal_length = (lookfrom - lookat).length(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + auto theta = degrees_to_radians(vfov); + auto h = std::tan(theta/2); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - lower_left_corner = origin - - half_width * focus_dist * u - - half_height * focus_dist * v - - focus_dist * w; + auto viewport_height = 2 * h * focus_dist; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + auto viewport_width = viewport_height * (double(image_width)/image_height); + + // Calculate the u,v,w unit basis vectors for the camera coordinate frame. + w = unit_vector(lookfrom - lookat); + u = unit_vector(cross(vup, w)); + v = cross(w, u); + + // Calculate the vectors across the horizontal and down the vertical viewport edges. + vec3 viewport_u = viewport_width * u; // Vector across viewport horizontal edge + vec3 viewport_v = viewport_height * -v; // Vector down viewport vertical edge + + // Calculate the horizontal and vertical delta vectors to the next pixel. + pixel_delta_u = viewport_u / image_width; + pixel_delta_v = viewport_v / image_height; - horizontal = 2*half_width*focus_dist*u; - vertical = 2*half_height*focus_dist*v; + // Calculate the location of the upper left pixel. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + auto viewport_upper_left = center - (focus_dist * w) - viewport_u/2 - viewport_v/2; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - } + pixel00_loc = viewport_upper_left + 0.5 * (pixel_delta_u + pixel_delta_v); + - ray get_ray(double s, double t) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - vec3 rd = lens_radius * random_in_unit_disk(); - vec3 offset = u * rd.x() + v * rd.y(); + // Calculate the camera defocus disk basis vectors. + auto defocus_radius = focus_dist * std::tan(degrees_to_radians(defocus_angle / 2)); + defocus_disk_u = u * defocus_radius; + defocus_disk_v = v * defocus_radius; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } - return ray( - origin + offset, - lower_left_corner + s*horizontal + t*vertical - origin - offset - ); + ray get_ray(int i, int j) const { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + // Construct a camera ray originating from the defocus disk and directed at a randomly + // sampled point around the pixel location i, j. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - } - public: - vec3 origin; - vec3 lower_left_corner; - vec3 horizontal; - vec3 vertical; + auto offset = sample_square(); + auto pixel_sample = pixel00_loc + + ((i + offset.x()) * pixel_delta_u) + + ((j + offset.y()) * pixel_delta_v); + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + auto ray_origin = (defocus_angle <= 0) ? center : defocus_disk_sample(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + auto ray_direction = pixel_sample - ray_origin; + + return ray(ray_origin, ray_direction); + } + + vec3 sample_square() const { + ... + } + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - vec3 u, v, w; - double lens_radius; + point3 defocus_disk_sample() const { + // Returns a random point in the camera defocus disk. + auto p = random_in_unit_disk(); + return center + (p[0] * defocus_disk_u) + (p[1] * defocus_disk_v); + } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + color ray_color(const ray& r, int depth, const hittable& world) const { + ... + } }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [camera-dof]: <kbd>[camera.h]</kbd> Camera with adjustable depth-of-field (dof)] + [Listing [camera-dof]: <kbd>[camera.h]</kbd> Camera with adjustable depth-of-field] <div class='together'> -Using a big aperture: +Using a large aperture: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - const auto aspect_ratio = double(image_width) / image_height; - ... - vec3 lookfrom(3,3,2); - vec3 lookat(0,0,-1); - vec3 vup(0,1,0); - auto dist_to_focus = (lookfrom-lookat).length(); - auto aperture = 2.0; + int main() { + ... + + camera cam; + + cam.aspect_ratio = 16.0 / 9.0; + cam.image_width = 400; + cam.samples_per_pixel = 100; + cam.max_depth = 50; + + cam.vfov = 20; + cam.lookfrom = point3(-2,2,1); + cam.lookat = point3(0,0,-1); + cam.vup = vec3(0,1,0); + - camera cam(lookfrom, lookat, vup, 20, aspect_ratio, aperture, dist_to_focus); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + cam.defocus_angle = 10.0; + cam.focus_dist = 3.4; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + cam.render(world); + } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [scene-camera-dof]: <kbd>[main.cc]</kbd> Scene camera with depth-of-field] -We get: - - <div class="render"> +</div> - ![Spheres with depth-of-field](../images/img.depth-of-field.png) +<div class='together'> +We get: - </div> + ![<span class='num'>Image 22:</span> Spheres with depth-of-field + ](../images/img-1.22-depth-of-field.png class='pixel') </div> @@ -2730,77 +4240,89 @@ Where Next? ==================================================================================================== -<div class='together'> -First let’s make the image on the cover of this book -- lots of random spheres: +A Final Render +--------------- +Let’s make the image on the cover of this book -- lots of random spheres. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - hittable_list random_scene() { + int main() { hittable_list world; - world.add(make_shared<sphere>( - vec3(0,-1000,0), 1000, make_shared<lambertian>(vec3(0.5, 0.5, 0.5)))); - int i = 1; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + auto ground_material = make_shared<lambertian>(color(0.5, 0.5, 0.5)); + world.add(make_shared<sphere>(point3(0,-1000,0), 1000, ground_material)); + for (int a = -11; a < 11; a++) { for (int b = -11; b < 11; b++) { auto choose_mat = random_double(); - vec3 center(a + 0.9*random_double(), 0.2, b + 0.9*random_double()); - if ((center - vec3(4, 0.2, 0)).length() > 0.9) { + point3 center(a + 0.9*random_double(), 0.2, b + 0.9*random_double()); + + if ((center - point3(4, 0.2, 0)).length() > 0.9) { + shared_ptr<material> sphere_material; + if (choose_mat < 0.8) { // diffuse - auto albedo = vec3::random() * vec3::random(); - world.add( - make_shared<sphere>(center, 0.2, make_shared<lambertian>(albedo))); + auto albedo = color::random() * color::random(); + sphere_material = make_shared<lambertian>(albedo); + world.add(make_shared<sphere>(center, 0.2, sphere_material)); } else if (choose_mat < 0.95) { // metal - auto albedo = vec3::random(.5, 1); - auto fuzz = random_double(0, .5); - world.add( - make_shared<sphere>(center, 0.2, make_shared<metal>(albedo, fuzz))); + auto albedo = color::random(0.5, 1); + auto fuzz = random_double(0, 0.5); + sphere_material = make_shared<metal>(albedo, fuzz); + world.add(make_shared<sphere>(center, 0.2, sphere_material)); } else { // glass - world.add(make_shared<sphere>(center, 0.2, make_shared<dielectric>(1.5))); + sphere_material = make_shared<dielectric>(1.5); + world.add(make_shared<sphere>(center, 0.2, sphere_material)); } } } } - world.add(make_shared<sphere>(vec3(0, 1, 0), 1.0, make_shared<dielectric>(1.5))); + auto material1 = make_shared<dielectric>(1.5); + world.add(make_shared<sphere>(point3(0, 1, 0), 1.0, material1)); - world.add( - make_shared<sphere>(vec3(-4, 1, 0), 1.0, make_shared<lambertian>(vec3(0.4, 0.2, 0.1)))); + auto material2 = make_shared<lambertian>(color(0.4, 0.2, 0.1)); + world.add(make_shared<sphere>(point3(-4, 1, 0), 1.0, material2)); - world.add( - make_shared<sphere>(vec3(4, 1, 0), 1.0, make_shared<metal>(vec3(0.7, 0.6, 0.5), 0.0))); + auto material3 = make_shared<metal>(color(0.7, 0.6, 0.5), 0.0); + world.add(make_shared<sphere>(point3(4, 1, 0), 1.0, material3)); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - return world; - } + camera cam; - int main() { - ... - auto world = random_scene(); - vec3 lookfrom(13,2,3); - vec3 lookat(0,0,0); - vec3 vup(0,1,0); - auto dist_to_focus = 10.0; - auto aperture = 0.1; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + cam.aspect_ratio = 16.0 / 9.0; + cam.image_width = 1200; + cam.samples_per_pixel = 500; + cam.max_depth = 50; + + cam.vfov = 20; + cam.lookfrom = point3(13,2,3); + cam.lookat = point3(0,0,0); + cam.vup = vec3(0,1,0); + + cam.defocus_angle = 0.6; + cam.focus_dist = 10.0; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - camera cam(lookfrom, lookat, vup, 20, aspect_ratio, aperture, dist_to_focus); - ... + cam.render(world); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [scene-final]: <kbd>[main.cc]</kbd> Final scene] -</div> + +(Note that the code above differs slightly from the project sample code: the `samples_per_pixel` is +set to 500 above for a high-quality image that will take quite a while to render. The project source +code uses a value of 10 in the interest of reasonable run times while developing and validating.) <div class='together'> This gives: - <div class="render"> - - ![Final scene](../images/img.book1-final.jpg) + ![<span class='num'>Image 23:</span> Final scene](../images/img-1.23-book1-final.jpg) - </div> </div> An interesting thing you might note is the glass balls don’t really have shadows which makes them @@ -2809,31 +4331,53 @@ under a glass ball still has lots of light hitting it because the sky is re-ordered rather than blocked. + +Next Steps +----------- You now have a cool ray tracer! What next? - 1. Lights. You can do this explicitly, by sending shadow rays to lights. Or it can be done - implicitly by making some objects emit light, - 2. Biasing scattered rays toward them, and then downweighting those rays to cancel out the bias. - Both work. I am in the minority in favoring the latter approach. +### Book 2: _Ray Tracing: The Next Week_ +The second book in this series builds on the ray tracer you've developed here. This includes new +features such as: + + - Motion blur -- Realistically render moving objects. + - Bounding volume hierarchies -- speeding up the rendering of complex scenes. + - Texture maps -- placing images on objects. + - Perlin noise -- a random noise generator very useful for many techniques. + - Quadrilaterals -- something to render besides spheres! Also, the foundation to implement disks, + triangles, rings or just about any other 2D primitive. + - Lights -- add sources of light to your scene. + - Transforms -- useful for placing and rotating objects. + - Volumetric rendering -- render smoke, clouds and other gaseous volumes. - 3. Triangles. Most cool models are in triangle form. The model I/O is the worst, and almost - everybody tries to get somebody else’s code to do this. - 4. Surface textures. This lets you paste images on like wall paper. Pretty easy, and a good thing - to do. +### Book 3: _Ray Tracing: The Rest of Your Life_ +This book expands again on the content from the second book. A lot of this book is about improving +both the rendered image quality and the renderer performance, and focuses on generating the _right_ +rays and accumulating them appropriately. - 5. Solid textures. Ken Perlin has his code online. Andrew Kensler has some very cool info at his - blog. +This book is for the reader seriously interested in writing professional-level ray tracers, and/or +interested in the foundation to implement advanced effects like subsurface scattering or nested +dielectrics. - 6. Volumes and media. Cool stuff, and will challenge your software architecture. I favor making - volumes have the hittable interface and probabilistically have intersections based on density. - Your rendering code doesn’t even have to know it has volumes with that method. - 7. Parallelism. Run $N$ copies of your code on $N$ cores with different random seeds. Average the - $N$ runs. This averaging can also be done hierarchically where $N/2$ pairs can be averaged to - get $N/4$ images, and pairs of those can be averaged. That method of parallelism should extend - well into the thousands of cores with very little coding. +### Other Directions +There are so many additional directions you can take from here, including techniques we haven't +(yet?) covered in this series. These include: + +**Triangles** -- Most cool models are in triangle form. The model I/O is the worst and almost +everybody tries to get somebody else’s code to do this. This also includes efficiently handling +large _meshes_ of triangles, which present their own challenges. + +**Parallelism** -- Run $N$ copies of your code on $N$ cores with different random seeds. Average the +$N$ runs. This averaging can also be done hierarchically where $N/2$ pairs can be averaged to get +$N/4$ images, and pairs of those can be averaged. That method of parallelism should extend well into +the thousands of cores with very little coding. + +**Shadow Rays** -- When firing rays at light sources, you can determine exactly how a particular +point is shadowed. With this, you can render crisp or soft shadows, adding another degreee of +realism to your scenes. Have fun, and please send me your cool images! @@ -2843,9 +4387,96 @@ +Citing This Book +==================================================================================================== +Consistent citations make it easier to identify the source, location and versions of this work. If +you are citing this book, we ask that you try to use one of the following forms if possible. + +Basic Data +----------- + - **Title (series)**: “Ray Tracing in One Weekend Series” + - **Title (book)**: “Ray Tracing in One Weekend” + - **Author**: Peter Shirley, Trevor David Black, Steve Hollasch + - **Version/Edition**: v4.0.2 + - **Date**: 2025-04-25 + - **URL (series)**: <https://raytracing.github.io/> + - **URL (book)**: <https://raytracing.github.io/books/RayTracingInOneWeekend.html> + +Snippets +--------- + + ### Markdown + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [_Ray Tracing in One Weekend_](https://raytracing.github.io/books/RayTracingInOneWeekend.html) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + ### HTML + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + <a href='https://raytracing.github.io/books/RayTracingInOneWeekend.html'> + <cite>Ray Tracing in One Weekend</cite> + </a> + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + ### LaTeX and BibTex + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ~\cite{Shirley2025RTW1} + + @misc{Shirley2025RTW1, + title = {Ray Tracing in One Weekend}, + author = {Peter Shirley, Trevor David Black, Steve Hollasch}, + year = {2025}, + month = {April}, + note = {\small \texttt{https://raytracing.github.io/books/RayTracingInOneWeekend.html}}, + url = {https://raytracing.github.io/books/RayTracingInOneWeekend.html} + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + ### BibLaTeX + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + \usepackage{biblatex} + + ~\cite{Shirley2025RTW1} + + @online{Shirley2025RTW1, + title = {Ray Tracing in One Weekend}, + author = {Peter Shirley, Trevor David Black, Steve Hollasch}, + year = {2025}, + month = {April}, + url = {https://raytracing.github.io/books/RayTracingInOneWeekend.html} + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + ### IEEE + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + “Ray Tracing in One Weekend.” raytracing.github.io/books/RayTracingInOneWeekend.html + (accessed MMM. DD, YYYY) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + ### MLA: + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Ray Tracing in One Weekend. raytracing.github.io/books/RayTracingInOneWeekend.html + Accessed DD MMM. YYYY. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + + +[Peter Shirley]: https://github.com/petershirley +[Steve Hollasch]: https://github.com/hollasch +[Trevor David Black]: https://github.com/trevordblack + +[discussions]: https://github.com/RayTracing/raytracing.github.io/discussions/ +[gfx-codex]: https://graphicscodex.com/ +[readme]: ../README.md +[releases]: https://github.com/RayTracing/raytracing.github.io/releases/ +[repo]: https://github.com/RayTracing/raytracing.github.io/ +[square-pixels]: https://www.researchgate.net/publication/244986797 +[wiki-further]: https://github.com/RayTracing/raytracing.github.io/wiki/Further-Readings + + + <!-- Markdeep: https://casual-effects.com/markdeep/ --> <link rel='stylesheet' href='../style/book.css'> <style class="fallback">body{visibility:hidden;white-space:pre;font-family:monospace}</style> <script src="markdeep.min.js"></script> -<script src="https://casual-effects.com/markdeep/latest/markdeep.min.js"></script> +<script src="https://morgan3d.github.io/markdeep/latest/markdeep.min.js"></script> <script>window.alreadyProcessedMarkdeep||(document.body.style.visibility="visible")</script> diff --git a/books/RayTracingTheNextWeek.html b/books/RayTracingTheNextWeek.html index c4d1b10f5..262aaba0f 100644 --- a/books/RayTracingTheNextWeek.html +++ b/books/RayTracingTheNextWeek.html @@ -1,29 +1,29 @@ +<!DOCTYPE html> <meta charset="utf-8"> +<link rel="icon" type="image/png" href="../favicon.png"> <!-- Markdeep: https://casual-effects.com/markdeep/ --> **Ray Tracing: The Next Week** - Peter Shirley - edited by Steve Hollasch and Trevor David Black + [Peter Shirley][], [Trevor David Black][], [Steve Hollasch][] <br> - Version 3.0.2, 2020-04-11 + Version 4.0.2, 2025-04-25 <br> - Copyright 2018-2020 Peter Shirley. All rights reserved. + Copyright 2018-2024 Peter Shirley. All rights reserved. Overview ==================================================================================================== - In Ray Tracing in One Weekend, you built a simple brute force path tracer. In this installment we’ll add textures, volumes (like fog), rectangles, instances, lights, and support for lots of objects using a BVH. When done, you’ll have a “real” ray tracer. A heuristic in ray tracing that many people--including me--believe, is that most optimizations complicate the code without delivering much speedup. What I will do in this mini-book is go with the -simplest approach in each design decision I make. Check https://in1weekend.blogspot.com/ for -readings and references to a more sophisticated approach. However, I strongly encourage you to do no +simplest approach in each design decision I make. See [our Further Reading wiki page][wiki-further] +for additional project related resources. However, I strongly encourage you to do no premature optimization; if it doesn’t show up high in the execution time profile, it doesn’t need optimization until all the features are supported! @@ -32,312 +32,339 @@ you want a weekend project. Order is not very important for the concepts presented in this book, and without BVH and Perlin texture you will still get a Cornell Box! +See the [project README][readme] file for information about this project, the repository on GitHub, +directory structure, building & running, and how to make or reference corrections and contributions. + +These books have been formatted to print well directly from your browser. We also include PDFs of +each book [with each release][releases], in the "Assets" section. + Thanks to everyone who lent a hand on this project. You can find them in the acknowledgments section at the end of this book. - Motion Blur ==================================================================================================== +When you decided to ray trace, you decided that visual quality was worth more than run-time. When +rendering fuzzy reflection and defocus blur, we used multiple samples per pixel. Once you have taken +a step down that road, the good news is that almost _all_ effects can be similarly brute-forced. +Motion blur is certainly one of those. -When you decided to ray trace, you decided visual quality was worth more run-time. In your fuzzy -reflection and defocus blur you needed multiple samples per pixel. Once you have taken a step down -that road, the good news is that almost all effects can be brute-forced. Motion blur is certainly -one of those. In a real camera, the shutter opens and stays open for a time interval, and the camera -and objects may move during that time. Its really an average of what the camera sees over that -interval that we want. We can get a random estimate by sending each ray at some random time when the -shutter is open. As long as the objects are where they should be at that time, we can get the right -average answer with a ray that is at exactly a single time. This is fundamentally why random ray -tracing tends to be simple. - -The basic idea is to generate rays at random times while the shutter is open and intersect the model -at that one time. The way it is usually done is to have the camera move and the objects move, but -have each ray exist at exactly one time. This way the “engine” of the ray tracer can just make sure -the objects are where they need to be for the ray, and the intersection guts don’t change much. +In a real camera, the shutter remains open for a short time interval, during which the camera and +objects in the world may move. To accurately reproduce such a camera shot, we seek an average of +what the camera senses while its shutter is open to the world. + + +Introduction of SpaceTime Ray Tracing +-------------------------------------- +We can get a random estimate of a single (simplified) photon by sending a single ray at some random +instant in time while the shutter is open. As long as we can determine where the objects are +supposed to be at that instant, we can get an accurate measure of the light for that ray at that +same instant. This is yet another example of how random (Monte Carlo) ray tracing ends up being +quite simple. Brute force wins again! <div class='together'> -For this we will first need to have a ray store the time it exists at: +Since the “engine” of the ray tracer can just make sure the objects are where they need to be for +each ray, the intersection guts don’t change much. To accomplish this, we need to store the exact +time for each ray: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class ray { - public: - ray() {} + public: + ray() {} + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - ray(const vec3& origin, const vec3& direction, double time = 0.0) - : orig(origin), dir(direction), tm(time) - {} + ray(const point3& origin, const vec3& direction, double time) + : orig(origin), dir(direction), tm(time) {} + + ray(const point3& origin, const vec3& direction) + : ray(origin, direction, 0) {} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - vec3 origin() const { return orig; } - vec3 direction() const { return dir; } + const point3& origin() const { return orig; } + const vec3& direction() const { return dir; } + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - double time() const { return tm; } + double time() const { return tm; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - vec3 at(double t) const { - return orig + t*dir; - } + point3 at(double t) const { + return orig + t*dir; + } - public: - vec3 orig; - vec3 dir; + private: + point3 orig; + vec3 dir; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - double tm; + double tm; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [time-ray]: <kbd>[ray.h]</kbd> Ray with time information] + </div> -Now we need to modify the camera to generate rays at a random time between `time1` and `time2`. -Should the camera keep track of `time1` and `time2` or should that be up to the user of camera when -a ray is created? When in doubt, I like to make constructors complicated if it makes calls simple, -so I will make the camera keep track, but that’s a personal preference. Not many changes are needed -to camera because for now it is not allowed to move; it just sends out rays over a time period. - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - class camera { - public: - camera( - vec3 lookfrom, vec3 lookat, vec3 vup, - double vfov, // top to bottom, in degrees - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - double aspect, double aperture, double focus_dist, double t0 = 0, double t1 = 0 - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - ) { - origin = lookfrom; - lens_radius = aperture / 2; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - time0 = t0; - time1 = t1; +Managing Time +-------------- +Before continuing, let's think about time, and how we might manage it across one or more successive +renders. There are two aspects of shutter timing to think about: the time from one shutter opening +to the next shutter opening, and how long the shutter stays open for each frame. Standard movie film +used to be shot at 24 frames per second. Modern digital movies can be 24, 30, 48, 60, 120 or however +many frames per second director wants. + +Each frame can have its own shutter speed. This shutter speed need not be -- and typically isn't -- +the maximum duration of the entire frame. You could have the shutter open for 1/1000th of a second +every frame, or 1/60th of a second. + +If you wanted to render a sequence of images, you would need to set up the camera with the +appropriate shutter timings: frame-to-frame period, shutter/render duration, and the total number of +frames (total shot time). If the camera is moving and the world is static, you're good to go. +However, if anything in the world is moving, you would need to add a method to `hittable` so that +every object could be made aware of the current frame's time period. This method would provide a way +for all animated objects to set up their motion during that frame. + +This is fairly straight-forward, and definitely a fun avenue for you to experiment with if you wish. +However, for our purposes right now, we're going to proceed with a much simpler model. We will +render only a single frame, implicitly assuming a start at time = 0 and ending at time = 1. Our +first task is to modify the camera to launch rays with random times in $[0,1]$, and our second task +will be the creation of an animated sphere class. + + +Updating the Camera to Simulate Motion Blur +-------------------------------------------- +We need to modify the camera to generate rays at a random instant between the start time and the end +time. Should the camera keep track of the time interval, or should that be up to the user of the +camera when a ray is created? When in doubt, I like to make constructors complicated if it makes +calls simple, so I will make the camera keep track, but that’s a personal preference. Not many +changes are needed to camera because for now it is not allowed to move; it just sends out rays over +a time period. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - auto theta = degrees_to_radians(vfov); - auto half_height = tan(theta/2); - auto half_width = aspect * half_height; - - w = unit_vector(lookfrom - lookat); - u = unit_vector(cross(vup, w)); - v = cross(w, u); - - lower_left_corner = origin - - half_width*focus_dist*u - - half_height*focus_dist*v - - focus_dist*w; + class camera { + ... + private: + ... + ray get_ray(int i, int j) const { + // Construct a camera ray originating from the defocus disk and directed at a randomly + // sampled point around the pixel location i, j. - horizontal = 2*half_width*focus_dist*u; - vertical = 2*half_height*focus_dist*v; - } + auto offset = sample_square(); + auto pixel_sample = pixel00_loc + + ((i + offset.x()) * pixel_delta_u) + + ((j + offset.y()) * pixel_delta_v); - ray get_ray(double s, double t) { - vec3 rd = lens_radius * random_in_unit_disk(); - vec3 offset = u * rd.x() + v * rd.y(); + auto ray_origin = (defocus_angle <= 0) ? center : defocus_disk_sample(); + auto ray_direction = pixel_sample - ray_origin; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - return ray( - origin + offset, - lower_left_corner + s*horizontal + t*vertical - origin - offset, - random_double(time0, time1) - ); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - } + auto ray_time = random_double(); - public: - vec3 origin; - vec3 lower_left_corner; - vec3 horizontal; - vec3 vertical; - vec3 u, v, w; - double lens_radius; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - double time0, time1; // shutter open/close times + return ray(ray_origin, ray_direction, ray_time); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - }; + } + ... + }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [time-camera]: <kbd>[camera.h]</kbd> Camera with time information] -We also need a moving object. I’ll create a sphere class that has its center move linearly from -`center0` at `time0` to `center1` at `time1`. Outside that time interval it continues on, so those -times need not match up with the camera aperture open and close. + +Adding Moving Spheres +---------------------- +Now to create a moving object. I’ll update the sphere class so that its center moves linearly from +`center1` at time=0 to `center2` at time=1. (It continues on indefinitely outside that time +interval, so it really can be sampled at any time.) We'll do this by replacing the 3D center point +with a 3D ray that describes the original position at time=0 and the displacement to the end +position at time=1. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - class moving_sphere : public hittable { - public: - moving_sphere() {} - moving_sphere( - vec3 cen0, vec3 cen1, double t0, double t1, double r, shared_ptr<material> m) - : center0(cen0), center1(cen1), time0(t0), time1(t1), radius(r), mat_ptr(m) - {}; + class sphere : public hittable { + public: + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + // Stationary Sphere + sphere(const point3& static_center, double radius, shared_ptr<material> mat) + : center(static_center, vec3(0,0,0)), radius(std::fmax(0,radius)), mat(mat) {} + + // Moving Sphere + sphere(const point3& center1, const point3& center2, double radius, + shared_ptr<material> mat) + : center(center1, center2 - center1), radius(std::fmax(0,radius)), mat(mat) {} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - virtual bool hit(const ray& r, double tmin, double tmax, hit_record& rec) const; + ... - vec3 center(double time) const; + private: + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + ray center; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + double radius; + shared_ptr<material> mat; - public: - vec3 center0, center1; - double time0, time1; - double radius; - shared_ptr<material> mat_ptr; }; - - vec3 moving_sphere::center(double time) const{ - return center0 + ((time - time0) / (time1 - time0))*(center1 - center0); - } + #endif ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [moving-sphere]: <kbd>[moving_sphere.h]</kbd> A moving sphere] + [Listing [moving-sphere]: <kbd>[sphere.h]</kbd> A moving sphere] <div class='together'> -An alternative to making a new moving sphere class is to just make them all move, while stationary -spheres have the same begin and end position. I’m on the fence about that trade-off between fewer -classes and more efficient stationary spheres, so let your design taste guide you. The intersection -code barely needs a change: `center` just needs to become a function `center(time)`: +The updated `sphere::hit()` function is almost identical to the old `sphere::hit()` function: +we just need to now determine the current position of the animated center: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - bool moving_sphere::hit( - const ray& r, double t_min, double t_max, hit_record& rec) const { - + class sphere : public hittable { + public: + ... + bool hit(const ray& r, interval ray_t, hit_record& rec) const override { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - vec3 oc = r.origin() - center(r.time()); + point3 current_center = center.at(r.time()); + vec3 oc = current_center - r.origin(); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - auto a = r.direction().length_squared(); - auto half_b = dot(oc, r.direction()); - auto c = oc.length_squared() - radius*radius; - - auto discriminant = half_b*half_b - a*c; + auto a = r.direction().length_squared(); + auto h = dot(r.direction(), oc); + auto c = oc.length_squared() - radius*radius; - if (discriminant > 0) { - auto root = sqrt(discriminant); + ... - auto temp = (-half_b - root)/a; - if (temp < t_max && temp > t_min) { - rec.t = temp; - rec.p = r.at(rec.t); + rec.t = root; + rec.p = r.at(rec.t); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - vec3 outward_normal = (rec.p - center(r.time())) / radius; + vec3 outward_normal = (rec.p - current_center) / radius; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - rec.set_face_normal(r, outward_normal); - rec.mat_ptr = mat_ptr; - return true; - } + rec.set_face_normal(r, outward_normal); + get_sphere_uv(outward_normal, rec.u, rec.v); + rec.mat = mat; - temp = (-half_b + root) / a; - if (temp < t_max && temp > t_min) { - rec.t = temp; - rec.p = r.at(rec.t); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - vec3 outward_normal = (rec.p - center(r.time())) / radius; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - rec.set_face_normal(r, outward_normal); - rec.mat_ptr = mat_ptr; - return true; - } + return true; } - return false; - } + ... + }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [moving-sphere-hit]: <kbd>[moving-sphere.h]</kbd> Moving sphere hit function] + [Listing [moving-sphere-hit]: <kbd>[sphere.h]</kbd> Moving sphere hit function] + </div> -<div class='together'> -Be sure that in the materials you have the scattered rays be at the time of the incident ray. + +Tracking the Time of Ray Intersection +-------------------------------------- +Now that rays have a time property, we need to update the `material::scatter()` methods to account +for the time of intersection: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class lambertian : public material { - public: - lambertian(const vec3& a) : albedo(a) {} + ... + bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) + const override { + auto scatter_direction = rec.normal + random_unit_vector(); + + // Catch degenerate scatter direction + if (scatter_direction.near_zero()) + scatter_direction = rec.normal; + - virtual bool scatter( - const ray& r_in, const hit_record& rec, vec3& attenuation, ray& scattered - ) const { - vec3 scatter_direction = rec.normal + random_unit_vector(); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - scattered = ray(rec.p, scatter_direction, r_in.time()); + scattered = ray(rec.p, scatter_direction, r_in.time()); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - attenuation = albedo; - return true; - } + attenuation = albedo; + return true; + } + ... + }; + + class metal : public material { + ... + bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) + const override { + vec3 reflected = reflect(r_in.direction(), rec.normal); + reflected = unit_vector(reflected) + (fuzz * random_unit_vector()); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + scattered = ray(rec.p, reflected, r_in.time()); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + attenuation = albedo; - vec3 albedo; + return (dot(scattered.direction(), rec.normal) > 0); + } + ... + }; + + class dielectric : public material { + ... + bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) + const override { + ... + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + scattered = ray(rec.p, direction, r_in.time()); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + return true; + } + ... }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [lambertian-animate]: <kbd>[material.h]</kbd> Lambertian matrial for moving objects] -</div> + [Listing [material-time]: <kbd>[material.h]</kbd> Handle ray time in the material::scatter() methods] -<div class='together'> + +Putting Everything Together +---------------------------- The code below takes the example diffuse spheres from the scene at the end of the last book, and -makes them move during the image render. (Think of a camera with shutter opening at time 0 and -closing at time 1.) Each sphere moves from its center $\mathbf{C}$ at time $t=0$ to $\mathbf{C} + -(0, r/2, 0)$ at time $t=1$, where $r$ is a random number in $[0,1)$: +makes them move during the image render. Each sphere moves from its center $\mathbf{C}$ at time +$t=0$ to $\mathbf{C} + (0, r/2, 0)$ at time $t=1$: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - hittable_list random_scene() { + int main() { hittable_list world; - world.add(make_shared<sphere>( - vec3(0,-1000,0), 1000, make_shared<lambertian>(vec3(0.5, 0.5, 0.5)))); + auto ground_material = make_shared<lambertian>(color(0.5, 0.5, 0.5)); + world.add(make_shared<sphere>(point3(0,-1000,0), 1000, ground_material)); - int i = 1; - for (int a = -10; a < 10; a++) { - for (int b = -10; b < 10; b++) { + for (int a = -11; a < 11; a++) { + for (int b = -11; b < 11; b++) { auto choose_mat = random_double(); - vec3 center(a + 0.9*random_double(), 0.2, b + 0.9*random_double()); - if ((center - vec3(4, .2, 0)).length() > 0.9) { + point3 center(a + 0.9*random_double(), 0.2, b + 0.9*random_double()); + + if ((center - point3(4, 0.2, 0)).length() > 0.9) { + shared_ptr<material> sphere_material; + if (choose_mat < 0.8) { // diffuse - auto albedo = vec3::random() * vec3::random(); - world.add(make_shared<moving_sphere>( - center, center + vec3(0, random_double(0,.5), 0), 0.0, 1.0, 0.2, - make_shared<lambertian>(albedo))); + auto albedo = color::random() * color::random(); + sphere_material = make_shared<lambertian>(albedo); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + auto center2 = center + vec3(0, random_double(0,.5), 0); + world.add(make_shared<sphere>(center, center2, 0.2, sphere_material)); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ } else if (choose_mat < 0.95) { - // metal - auto albedo = vec3::random(.5, 1); - auto fuzz = random_double(0, .5); - world.add( - make_shared<sphere>(center, 0.2, make_shared<metal>(albedo, fuzz))); - } else { - // glass - world.add(make_shared<sphere>(center, 0.2, make_shared<dielectric>(1.5))); - } - } - } + ... } + ... - world.add(make_shared<sphere>(vec3(0, 1, 0), 1.0, make_shared<dielectric>(1.5))); - world.add(make_shared<sphere>( - vec3(-4, 1, 0), 1.0, make_shared<lambertian>(vec3(0.4, 0.2, 0.1)))); - world.add(make_shared<sphere>( - vec3(4, 1, 0), 1.0, make_shared<metal>(vec3(0.7, 0.6, 0.5), 0.0))); - - return world; - } - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [scene-spheres-moving]: - <kbd>[main.cc]</kbd> Last book's final scene, but with moving spheres] -</div> - -<div class='together'> -And with these viewing parameters: + camera cam; + cam.aspect_ratio = 16.0 / 9.0; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + cam.image_width = 400; + cam.samples_per_pixel = 100; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - const auto aspect_ratio = double(image_width) / image_height; - ... - vec3 lookfrom(13,2,3); - vec3 lookat(0,0,0); - vec3 vup(0,1,0); - auto dist_to_focus = 10.0; - auto aperture = 0.0; + cam.max_depth = 50; - camera cam(lookfrom, lookat, vup, 20, aspect_ratio, aperture, dist_to_focus, 0.0, 1.0); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [scene-spheres-moving-camera]: <kbd>[main.cc]</kbd> Viewing parameters] + cam.vfov = 20; + cam.lookfrom = point3(13,2,3); + cam.lookat = point3(0,0,0); + cam.vup = vec3(0,1,0); -gives the following result: + cam.defocus_angle = 0.6; + cam.focus_dist = 10.0; - <div class="render"> + cam.render(world); + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [scene-spheres-moving]: <kbd>[main.cc]</kbd> Last book's final scene, but with moving spheres] - ![Bouncing spheres](../images/img.bouncing-spheres.jpg) +<div class='together'> +This gives the following result: + <div id="image-bouncing-spheres"> + ![<span class='num'>Image 1:</span> Bouncing spheres + ](../images/img-2.01-bouncing-spheres.png class='pixel') </div> </div> @@ -346,24 +373,26 @@ Bounding Volume Hierarchies ==================================================================================================== - This part is by far the most difficult and involved part of the ray tracer we are working on. I am sticking it in this chapter so the code can run faster, and because it refactors `hittable` a little, and when I add rectangles and boxes we won't have to go back and refactor them. -The ray-object intersection is the main time-bottleneck in a ray tracer, and the time is linear with -the number of objects. But it’s a repeated search on the same model, so we ought to be able to make +Ray-object intersection is the main time-bottleneck in a ray tracer, and the run time is linear with +the number of objects. But it’s a repeated search on the same scene, so we ought to be able to make it a logarithmic search in the spirit of binary search. Because we are sending millions to billions -of rays on the same model, we can do an analog of sorting the model, and then each ray intersection -can be a sublinear search. The two most common families of sorting are to 1) divide the space, and -2) divide the objects. The latter is usually much easier to code up and just as fast to run for most -models. +of rays into the same scene, we can sort the objects in the scene, and then each ray intersection +can be a sublinear search. The two most common methods of sorting are to 1) subdivide the space, and +2) subdivide the objects. The latter is usually much easier to code up, and just as fast to run for +most models. -<div class='together'> -The key idea of a bounding volume over a set of primitives is to find a volume that fully encloses -(bounds) all the objects. For example, suppose you computed a bounding sphere of 10 objects. Any ray -that misses the bounding sphere definitely misses all ten objects. If the ray hits the bounding -sphere, then it might hit one of the ten objects. So the bounding code is always of the form: + +The Key Idea +------------- +The key idea of creating bounding volumes for a set of primitives is to find a volume that fully +encloses (bounds) all the objects. For example, suppose you computed a sphere that bounds 10 +objects. Any ray that misses the bounding sphere definitely misses all ten objects inside. If the +ray hits the bounding sphere, then it might hit one of the ten objects. So the bounding code is +always of the form: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if (ray hits bounding object) @@ -371,24 +400,24 @@ else return false ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -</div> -A key thing is we are dividing objects into subsets. We are not dividing the screen or the volume. -Any object is in just one bounding volume, but bounding volumes can overlap. +Note that we will use these bounding volumes to group the objects in the scene into subgroups. We +are *not* dividing the screen or the scene space. We want any given object to be in just one +bounding volume, though bounding volumes can overlap. -<div class='together'> + +Hierarchies of Bounding Volumes +-------------------------------- To make things sub-linear we need to make the bounding volumes hierarchical. For example, if we divided a set of objects into two groups, red and blue, and used rectangular bounding volumes, we’d have: - ![Figure [bvol-hierarchy]: Bounding volume hierarchy](../images/fig.bvol-hierarchy.jpg) - -</div> + ![Figure [bvol-hierarchy]: Bounding volume hierarchy](../images/fig-2.01-bvol-hierarchy.jpg) <div class='together'> -Note that the blue and red bounding volumes are contained in the purple one, but they might -overlap, and they are not ordered -- they are just both inside. So the tree shown on the right has -no concept of ordering in the left and right children; they are simply inside. The code would be: +Note that the blue and red bounding volumes are contained in the purple one, but they might overlap, +and they are not ordered -- they are just both inside. So the tree shown on the right has no concept +of ordering in the left and right children; they are simply inside. The code would be: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if (hits purple) @@ -398,1392 +427,2750 @@ return true and info of closer hit return false ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + </div> + +Axis-Aligned Bounding Boxes (AABBs) +------------------------------------ To get that all to work we need a way to make good divisions, rather than bad ones, and a way to intersect a ray with a bounding volume. A ray bounding volume intersection needs to be fast, and bounding volumes need to be pretty compact. In practice for most models, axis-aligned boxes work -better than the alternatives, but this design choice is always something to keep in mind if you -encounter unusual types of models. +better than the alternatives (such as the spherical bounds mentioned above), but this design choice +is always something to keep in mind if you encounter other types of bounding models. -From now on we will call axis-aligned bounding rectangular parallelepiped (really, that is what they -need to be called if precise) axis-aligned bounding boxes, or AABBs. Any method you want to use to -intersect a ray with an AABB is fine. And all we need to know is whether or not we hit it; we don’t -need hit points or normals or any of that stuff that we need for an object we want to display. +From now on we will call axis-aligned bounding rectangular parallelepipeds (really, that is what +they need to be called if we're being precise) _axis-aligned bounding boxes_, or AABBs. (In the +code, you will also come across the naming abbreviation "bbox" for "bounding box".) Any method you +want to use to intersect a ray with an AABB is fine. And all we need to know is whether or not we +hit it; we don’t need hit points or normals or any of the stuff we need to display the object. <div class='together'> Most people use the “slab” method. This is based on the observation that an n-dimensional AABB is -just the intersection of n axis-aligned intervals, often called “slabs” An interval is just -the points between two endpoints, _e.g._, $x$ such that $3 < x < 5$, or more succinctly $x$ in -$(3,5)$. In 2D, two intervals overlapping makes a 2D AABB (a rectangle): +just the intersection of $n$ axis-aligned intervals, often called “slabs”. Recall that an interval +is just the points within two endpoints, for example, $x$ such that $3 \leq x \leq 5$, or more +succinctly $x$ in $[3,5]$. In 2D, an AABB (a rectangle) is defined by the overlap two intervals: - ![Figure [2daabb]: 2D axis-aligned bounding box](../images/fig.2daabb.jpg) + ![Figure [2d-aabb]: 2D axis-aligned bounding box](../images/fig-2.02-2d-aabb.jpg) </div> -<div class='together'> -For a ray to hit one interval we first need to figure out whether the ray hits the boundaries. For -example, again in 2D, this is the ray parameters $t_0$ and $t_1$. (If the ray is parallel to the -plane those will be undefined.) - - ![Figure [ray-slab]: Ray-slab intersection](../images/fig.ray-slab.jpg) +To determine if a ray hits one interval, we first need to figure out whether the ray hits the +boundaries. For example, in 1D, ray intersection with two planes will yield the ray parameters $t_0$ +and $t_1$. (If the ray is parallel to the planes, its intersection with any plane will be +undefined.) -</div> + ![Figure [ray-slab]: Ray-slab intersection](../images/fig-2.03-ray-slab.jpg) -<div class='together'> -In 3D, those boundaries are planes. The equations for the planes are $x = x_0$, and $x = x_1$. Where -does the ray hit that plane? Recall that the ray can be thought of as just a function that given a -$t$ returns a location $p(t)$: +How do we find the intersection between a ray and a plane? Recall that the ray is just defined by a +function that--given a parameter $t$--returns a location $\mathbf{P}(t)$: - $$ p(t) = \mathbf{a} + t \vec{\mathbf{b}} $$ -</div> + $$ \mathbf{P}(t) = \mathbf{Q} + t \mathbf{d} $$ -<div class='together'> -That equation applies to all three of the x/y/z coordinates. For example, -$x(t) = \mathbf{a}_x + t \vec{\mathbf{b}}_x$. This ray hits the plane $x = x_0$ at the $t$ that -satisfies this equation: +This equation applies to all three of the x/y/z coordinates. For example, $x(t) = Q_x + t d_x$. This +ray hits the plane $x = x_0$ at the parameter $t$ that satisfies this equation: - $$ x_0 = \mathbf{a}_x + t_0 \vec{\mathbf{b}}_x $$ -</div> + $$ x_0 = Q_x + t_0 d_x $$ -<div class='together'> -Thus $t$ at that hitpoint is: +So $t$ at the intersection is given by - $$ t_0 = \frac{x_0 - \mathbf{a}_x}{\vec{\mathbf{b}}_x} $$ -</div> + $$ t_0 = \frac{x_0 - Q_x}{d_x} $$ -<div class='together'> We get the similar expression for $x_1$: - $$ t_1 = \frac{x_1 - \mathbf{a}_x}{\vec{\mathbf{b}}_x} $$ -</div> + $$ t_1 = \frac{x_1 - Q_x}{d_x} $$ <div class='together'> -The key observation to turn that 1D math into a hit test is that for a hit, the $t$-intervals need -to overlap. For example, in 2D the green and blue overlapping only happens if there is a hit: +The key observation to turn that 1D math into a 2D or 3D hit test is this: if a ray intersects the +box bounded by all pairs of planes, then all $t$-intervals will overlap. For example, in 2D the +green and blue overlapping only happens if the ray intersects the bounded box: - ![Figure [rstio]: Ray-slab $t$-interval overlap](../images/fig.rstio.jpg) + ![Figure [ray-slab-interval]: Ray-slab $t$-interval overlap + ](../images/fig-2.04-ray-slab-interval.jpg) + +In this figure, the upper ray intervals do not overlap, so we know the ray does _not_ hit the 2D box +bounded by the green and blue planes. The lower ray intervals _do_ overlap, so we know the lower ray +_does_ hit the bounded box. </div> -<div class='together'> -What “do the t intervals in the slabs overlap?” would like in code is something like: + +Ray Intersection with an AABB +------------------------------ +The following pseudocode determines whether the $t$ intervals in the slab overlap: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - compute (tx0, tx1) - compute (ty0, ty1) - return overlap?( (tx0, tx1), (ty0, ty1)) + interval_x ← compute_intersection_x (ray, x0, x1) + interval_y ← compute_intersection_y (ray, y0, y1) + return overlaps(interval_x, interval_y) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -That is awesomely simple, and the fact that the 3D version also works is why people love the -slab method: +<div class='together'> +That is awesomely simple, and the fact that the 3D version trivially extends the above is why people +love the slab method: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - compute (tx0, tx1) - compute (ty0, ty1) - compute (tz0, tz1) - return overlap?( (tx0, tx1), (ty0, ty1), (tz0, tz1)) + interval_x ← compute_intersection_x (ray, x0, x1) + interval_y ← compute_intersection_y (ray, y0, y1) + interval_z ← compute_intersection_z (ray, z0, z1) + return overlaps(interval_x, interval_y, interval_z) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -</div> -<div class='together'> -There are some caveats that make this less pretty than it first appears. First, suppose the ray is -travelling in the negative $x$ direction. The interval $(t_{x0}, t_{x1})$ as computed above might be -reversed, _e.g._ something like $(7, 3)$. Second, the divide in there could give us infinities. And -if the ray origin is on one of the slab boundaries, we can get a `NaN`. There are many ways these -issues are dealt with in various ray tracers’ AABB. (There are also vectorization issues like SIMD -which we will not discuss here. Ingo Wald’s papers are a great place to start if you want to go the -extra mile in vectorization for speed.) For our purposes, this is unlikely to be a major bottleneck -as long as we make it reasonably fast, so let’s go for simplest, which is often fastest anyway! -First let’s look at computing the intervals: - - $$ t_{x0} = \frac{x_0 - \mathbf{a}_x}{\vec{\mathbf{b}}_x} $$ - $$ t_{x1} = \frac{x_1 - \mathbf{a}_x}{\vec{\mathbf{b}}_x} $$ </div> -<div class='together'> -One troublesome thing is that perfectly valid rays will have $\vec{\mathbf{b}}_x = 0$, causing -division by zero. Some of those rays are inside the slab, and some are not. Also, the zero will have -a ± sign under IEEE floating point. The good news for $\vec{\mathbf{b}}_x = 0$ is that $t_{x0}$ and -$t_{x1}$ will both be +∞ or both be -∞ if not between $x_0$ and $x_1$. So, using min and max should -get us the right answers: +There are some caveats that make this less pretty than it first appears. Consider again the 1D +equations for $t_0$ and $t_1$: + + $$ t_0 = \frac{x_0 - Q_x}{d_x} $$ + $$ t_1 = \frac{x_1 - Q_x}{d_x} $$ + +First, suppose the ray is traveling in the negative $\mathbf{x}$ direction. The interval $(t_{x0}, +t_{x1})$ as computed above might be reversed, like $(7, 3)$ for example. Second, the denominator +$d_x$ could be zero, yielding infinite values. And if the ray origin lies on one of the slab +boundaries, we can get a `NaN`, since both the numerator and the denominator can be zero. Also, the +zero will have a ± sign when using IEEE floating point. + +The good news for $d_x = 0$ is that $t_{x0}$ and $t_{x1}$ will be equal: both +∞ or -∞, if not +between $x_0$ and $x_1$. So, using min and max should get us the right answers: $$ t_{x0} = \min( - \frac{x_0 - \mathbf{a}_x}{\vec{\mathbf{b}}_x}, - \frac{x_1 - \mathbf{a}_x}{\vec{\mathbf{b}}_x}) + \frac{x_0 - Q_x}{d_x}, + \frac{x_1 - Q_x}{d_x}) $$ $$ t_{x1} = \max( - \frac{x_0 - \mathbf{a}_x}{\vec{\mathbf{b}}_x}, - \frac{x_1 - \mathbf{a}_x}{\vec{\mathbf{b}}_x}) + \frac{x_0 - Q_x}{d_x}, + \frac{x_1 - Q_x}{d_x}) $$ -</div> -The remaining troublesome case if we do that is if $\vec{\mathbf{b}}_x = 0$ and either -$x_0 - \mathbf{a}_x = 0$ or $x_1 - \mathbf{a}_x = 0$ so we get a `NaN`. In that case we can probably -accept either hit or no hit answer, but we’ll revisit that later. +The remaining troublesome case if we do that is if $d_x = 0$ and either $x_0 - Q_x = 0$ or +$x_1 - Q_x = 0$ so we get a `NaN`. In that case we can arbitrarily interpret that as either hit or +no hit, but we’ll revisit that later. -<div class='together'> -Now, let’s look at that overlap function. Suppose we can assume the intervals are not reversed (so -the first value is less than the second value in the interval) and we want to return true in that -case. The boolean overlap that also computes the overlap interval $(f, F)$ of intervals $(d, D)$ and -$(e, E)$ would be: +Now, let’s look at the pseudo-function `overlaps`. Suppose we can assume the intervals are not +reversed, and we want to return true when the intervals overlap. The boolean `overlaps()` function +computes the overlap of the $t$ intervals `t_interval1` and `t_interval2`, and uses that to +determine if that overlap is non-empty: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - bool overlap(d, D, e, E, f, F) - f = max(d, e) - F = min(D, E) - return (f < F) + bool overlaps(t_interval1, t_interval2) + t_min ← max(t_interval1.min, t_interval2.min) + t_max ← min(t_interval1.max, t_interval2.max) + return t_min < t_max + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If there are any `NaN`s running around there, the compare will return false, so we need to be sure +our bounding boxes have a little padding if we care about grazing cases (and we probably _should_ +because in a ray tracer all cases come up eventually). + +<div class='together'> +To accomplish this, we'll first add a new `interval` function `expand`, which pads an interval by a +given amount: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class interval { + public: + ... + double clamp(double x) const { + if (x < min) return min; + if (x > max) return max; + return x; + } + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + interval expand(double delta) const { + auto padding = delta/2; + return interval(min - padding, max + padding); + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + static const interval empty, universe; + }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [interval-expand]: <kbd>[interval.h]</kbd> interval::expand() method] + </div> <div class='together'> -If there are any `NaN`s running around there, the compare will return false so we need to be sure -our bounding boxes have a little padding if we care about grazing cases (and we probably should -because in a ray tracer all cases come up eventually). With all three dimensions in a loop, and -passing in the interval $[t_{min}$, $t_{max}]$, we get: +Now we have everything we need to implement the new AABB class. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - #include "rtweekend.h" + #ifndef AABB_H + #define AABB_H class aabb { - public: - aabb() {} - aabb(const vec3& a, const vec3& b) { _min = a; _max = b;} - - vec3 min() const {return _min; } - vec3 max() const {return _max; } - - bool hit(const ray& r, double tmin, double tmax) const { - for (int a = 0; a < 3; a++) { - auto t0 = ffmin((_min[a] - r.origin()[a]) / r.direction()[a], - (_max[a] - r.origin()[a]) / r.direction()[a]); - auto t1 = ffmax((_min[a] - r.origin()[a]) / r.direction()[a], - (_max[a] - r.origin()[a]) / r.direction()[a]); - tmin = ffmax(t0, tmin); - tmax = ffmin(t1, tmax); - if (tmax <= tmin) - return false; + public: + interval x, y, z; + + aabb() {} // The default AABB is empty, since intervals are empty by default. + + aabb(const interval& x, const interval& y, const interval& z) + : x(x), y(y), z(z) {} + + aabb(const point3& a, const point3& b) { + // Treat the two points a and b as extrema for the bounding box, so we don't require a + // particular minimum/maximum coordinate order. + + x = (a[0] <= b[0]) ? interval(a[0], b[0]) : interval(b[0], a[0]); + y = (a[1] <= b[1]) ? interval(a[1], b[1]) : interval(b[1], a[1]); + z = (a[2] <= b[2]) ? interval(a[2], b[2]) : interval(b[2], a[2]); + } + + const interval& axis_interval(int n) const { + if (n == 1) return y; + if (n == 2) return z; + return x; + } + + bool hit(const ray& r, interval ray_t) const { + const point3& ray_orig = r.origin(); + const vec3& ray_dir = r.direction(); + + for (int axis = 0; axis < 3; axis++) { + const interval& ax = axis_interval(axis); + const double adinv = 1.0 / ray_dir[axis]; + + auto t0 = (ax.min - ray_orig[axis]) * adinv; + auto t1 = (ax.max - ray_orig[axis]) * adinv; + + if (t0 < t1) { + if (t0 > ray_t.min) ray_t.min = t0; + if (t1 < ray_t.max) ray_t.max = t1; + } else { + if (t1 > ray_t.min) ray_t.min = t1; + if (t0 < ray_t.max) ray_t.max = t0; } - return true; - } - vec3 _min; - vec3 _max; + if (ray_t.max <= ray_t.min) + return false; + } + return true; + } }; + + #endif ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [aabb]: <kbd>[aabb.h]</kbd> Axis-aligned bounding box class] + </div> -Note that we use the simple custom `ffmax()` function (defined in `rtweekend.h`) instead of the C++ -standard library `fmax()` utility. `ffmax()` is quite a bit faster because it doesn’t worry about -`NaN`s and other exceptions. -<div class='together'> -In reviewing this intersection method, Andrew Kensler at Pixar tried some experiments and proposed -the following version of the code. It works extremely well on many compilers, and I have adopted it -as my go-to method: - - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - inline bool aabb::hit(const ray& r, double tmin, double tmax) const { - for (int a = 0; a < 3; a++) { - auto invD = 1.0f / r.direction()[a]; - auto t0 = (min()[a] - r.origin()[a]) * invD; - auto t1 = (max()[a] - r.origin()[a]) * invD; - if (invD < 0.0f) - std::swap(t0, t1); - tmin = t0 > tmin ? t0 : tmin; - tmax = t1 < tmax ? t1 : tmax; - if (tmax <= tmin) - return false; - } - return true; - } - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [aabb-hit]: <kbd>[aabb.h]</kbd> Axis-aligned bounding box hit function] -</div> -<div class='together'> +Constructing Bounding Boxes for Hittables +------------------------------------------ We now need to add a function to compute the bounding boxes of all the hittables. Then we will make a hierarchy of boxes over all the primitives, and the individual primitives--like spheres--will live -at the leaves. That function returns a bool because not all primitives have bounding boxes (_e.g._, -infinite planes). In addition, moving objects will have a bounding box that encloses the object for -the entire time interval [`time1`,`time2`]. +at the leaves. + +Recall that `interval` values constructed without arguments will be empty by default. Since an +`aabb` object has an interval for each of its three dimensions, each of these will then be empty by +default, and therefore `aabb` objects will be empty by default. Thus, some objects may have empty +bounding volumes. For example, consider a `hittable_list` object with no children. Happily, the way +we've designed our interval class, the math all works out. +Finally, recall that some objects may be animated. Such objects should return their bounds over the +entire range of motion, from time=0 to time=1. + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + #include "aabb.h" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + class material; + + ... + class hittable { - public: - virtual bool hit( - const ray& r, double t_min, double t_max, hit_record& rec) const = 0; + public: + virtual ~hittable() = default; + + virtual bool hit(const ray& r, interval ray_t, hit_record& rec) const = 0; + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - virtual bool bounding_box(double t0, double t1, aabb& output_box) const = 0; + virtual aabb bounding_box() const = 0; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [hittable-bbox]: <kbd>[hittable.h]</kbd> Hittable class with bounding-box] -</div> + [Listing [hittable-bbox]: <kbd>[hittable.h]</kbd> Hittable class with bounding box] -<div class='together'> -For a sphere, that `bounding_box` function is easy: +For a stationary sphere, the `bounding_box` function is easy: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - bool sphere::bounding_box(double t0, double t1, aabb& output_box) const { - output_box = aabb( - center - vec3(radius, radius, radius), - center + vec3(radius, radius, radius)); - return true; - } + class sphere : public hittable { + public: + // Stationary Sphere + sphere(const point3& static_center, double radius, shared_ptr<material> mat) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + : center(static_center, vec3(0,0,0)), radius(std::fmax(0,radius)), mat(mat) + { + auto rvec = vec3(radius, radius, radius); + bbox = aabb(static_center - rvec, static_center + rvec); + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + ... + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + aabb bounding_box() const override { return bbox; } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + private: + ray center; + double radius; + shared_ptr<material> mat; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + aabb bbox; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + ... + }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [sphere-bbox]: <kbd>[sphere.h]</kbd> Sphere with bounding box] -</div> -<div class='together'> -For `moving sphere`, we can take the box of the sphere at $t_0$, and the box of the sphere at $t_1$, -and compute the box of those two boxes: - - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - bool moving_sphere::bounding_box(double t0, double t1, aabb& output_box) const { - aabb box0( - center(t0) - vec3(radius, radius, radius), - center(t0) + vec3(radius, radius, radius)); - aabb box1( - center(t1) - vec3(radius, radius, radius), - center(t1) + vec3(radius, radius, radius)); - output_box = surrounding_box(box0, box1); - return true; - } +For a moving sphere, we want the bounds of its entire range of motion. To do this, we can take the +box of the sphere at time=0, and the box of the sphere at time=1, and compute the box around those +two boxes. + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class sphere : public hittable { + public: + ... + + // Moving Sphere + sphere(const point3& center1, const point3& center2, double radius, + shared_ptr<material> mat) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + : center(center1, center2 - center1), radius(std::fmax(0,radius)), mat(mat) + { + auto rvec = vec3(radius, radius, radius); + aabb box1(center.at(0) - rvec, center.at(0) + rvec); + aabb box2(center.at(1) - rvec, center.at(1) + rvec); + bbox = aabb(box1, box2); + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + ... + }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [moving-sphere-bbox]: <kbd>[moving_sphere.h]</kbd> Moving sphere with bounding box] -</div> + [Listing [moving-sphere-bbox]: <kbd>[sphere.h]</kbd> Moving sphere with bounding box] <div class='together'> -For lists you can store the bounding box at construction, or compute it on the fly. I like doing it -the fly because it is only usually called at BVH construction. +Now we need a new `aabb` constructor that takes two boxes as input. First, we'll add a new interval +constructor to do this: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - bool hittable_list::bounding_box(double t0, double t1, aabb& output_box) const { - if (objects.empty()) return false; + class interval { + public: + double min, max; + + interval() : min(+infinity), max(-infinity) {} // Default interval is empty - aabb temp_box; - bool first_box = true; + interval(double _min, double _max) : min(_min), max(_max) {} - for (const auto& object : objects) { - if (!object->bounding_box(t0, t1, temp_box)) return false; - output_box = first_box ? temp_box : surrounding_box(output_box, temp_box); - first_box = false; + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + interval(const interval& a, const interval& b) { + // Create the interval tightly enclosing the two input intervals. + min = a.min <= b.min ? a.min : b.min; + max = a.max >= b.max ? a.max : b.max; } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - return true; - } + double size() const { + ... ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [hit-list-bbox]: <kbd>[hittable_list.h]</kbd> Hittable list with bounding box] -</div> + [Listing [interval-from-intervals]: <kbd>[interval.h]</kbd> Interval constructor from two intervals] -<div class='together'> -This requires the `surrounding_box` function for `aabb` which computes the bounding box of two -boxes: - - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - aabb surrounding_box(aabb box0, aabb box1) { - vec3 small(ffmin(box0.min().x(), box1.min().x()), - ffmin(box0.min().y(), box1.min().y()), - ffmin(box0.min().z(), box1.min().z())); - vec3 big (ffmax(box0.max().x(), box1.max().x()), - ffmax(box0.max().y(), box1.max().y()), - ffmax(box0.max().z(), box1.max().z())); - return aabb(small,big); - } - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [surrounding-box]: <kbd>[aabb.h]</kbd> Surrounding bounding box] </div> <div class='together'> -A BVH is also going to be a `hittable` -- just like lists of `hittable`s. It’s really a container, -but it can respond to the query “does this ray hit you?”. One design question is whether we have two -classes, one for the tree, and one for the nodes in the tree; or do we have just one class and have -the root just be a node we point to. I am a fan of the one class design when feasible. Here is such -a class: +Now we can use this to construct an axis-aligned bounding box from two input boxes. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - class bvh_node : public hittable { - public: - bvh_node(); + class aabb { + public: + ... - bvh_node(hittable_list& list, double time0, double time1) - : bvh_node(list.objects, 0, list.objects.size(), time0, time1) - {} + aabb(const point3& a, const point3& b) { + ... + } - bvh_node( - std::vector<shared_ptr<hittable>>& objects, - size_t start, size_t end, double time0, double time1); - virtual bool hit(const ray& r, double tmin, double tmax, hit_record& rec) const; - virtual bool bounding_box(double t0, double t1, aabb& output_box) const; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + aabb(const aabb& box0, const aabb& box1) { + x = interval(box0.x, box1.x); + y = interval(box0.y, box1.y); + z = interval(box0.z, box1.z); + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - public: - shared_ptr<hittable> left; - shared_ptr<hittable> right; - aabb box; + ... }; - - bool bvh_node::bounding_box(double t0, double t1, aabb& output_box) const { - output_box = box; - return true; - } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [bvh]: <kbd>[bvh.h]</kbd> Bounding volume hierarchy] + [Listing [aabb-from-two-aabb]: <kbd>[aabb.h]</kbd> AABB constructor from two AABB inputs] + </div> -Note that the children pointers are to generic hittables. They can be other `bvh_nodes`, or -`spheres`, or any other `hittable`. -<div class='together'> -The `hit` function is pretty straightforward: check whether the box for the node is hit, and if so, -check the children and sort out any details: +Creating Bounding Boxes of Lists of Objects +-------------------------------------------- +Now we'll update the `hittable_list` object, computing the bounds of its children. We'll update the +bounding box incrementally as each new child is added. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + #include "aabb.h" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - bool bvh_node::hit(const ray& r, double t_min, double t_max, hit_record& rec) const { - if (!box.hit(r, t_min, t_max)) - return false; + #include "hittable.h" - bool hit_left = left->hit(r, t_min, t_max, rec); - bool hit_right = right->hit(r, t_min, hit_left ? rec.t : t_max, rec); + #include <vector> - return hit_left || hit_right; - } - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [bvh-hit]: <kbd>[bvh.h]</kbd> Bounding volume hierarchy hit function] -</div> + class hittable_list : public hittable { + public: + std::vector<shared_ptr<hittable>> objects; -<div class='together'> -The most complicated part of any efficiency structure, including the BVH, is building it. We do this -in the constructor. A cool thing about BVHs is that as long as the list of objects in a `bvh_node` -gets divided into two sub-lists, the hit function will work. It will work best if the division is -done well, so that the two children have smaller bounding boxes than their parent’s bounding box, -but that is for speed not correctness. I’ll choose the middle ground, and at each node split the -list along one axis. I’ll go for simplicity: + ... + void add(shared_ptr<hittable> object) { + objects.push_back(object); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + bbox = aabb(bbox, object->bounding_box()); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } -1. randomly choose an axis -2. sort the primitives using library qsort -3. put half in each subtree + bool hit(const ray& r, interval ray_t, hit_record& rec) const override { + ... + } + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + aabb bounding_box() const override { return bbox; } + + private: + aabb bbox; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [hit-list-bbox]: <kbd>[hittable_list.h]</kbd> Hittable list with bounding box] -</div> + +The BVH Node Class +------------------- +A BVH is also going to be a `hittable` -- just like lists of `hittable`s. It’s really a container, +but it can respond to the query “does this ray hit you?”. One design question is whether we have two +classes, one for the tree, and one for the nodes in the tree; or do we have just one class and have +the root just be a node we point to. The `hit` function is pretty straightforward: check whether the +box for the node is hit, and if so, check the children and sort out any details. <div class='together'> +I am a fan of the one class design when feasible. Here is such a class: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #ifndef BVH_H + #define BVH_H + + #include "aabb.h" + #include "hittable.h" + #include "hittable_list.h" + + class bvh_node : public hittable { + public: + bvh_node(hittable_list list) : bvh_node(list.objects, 0, list.objects.size()) { + // There's a C++ subtlety here. This constructor (without span indices) creates an + // implicit copy of the hittable list, which we will modify. The lifetime of the copied + // list only extends until this constructor exits. That's OK, because we only need to + // persist the resulting bounding volume hierarchy. + } + + bvh_node(std::vector<shared_ptr<hittable>>& objects, size_t start, size_t end) { + // To be implemented later. + } + + bool hit(const ray& r, interval ray_t, hit_record& rec) const override { + if (!bbox.hit(r, ray_t)) + return false; + + bool hit_left = left->hit(r, ray_t, rec); + bool hit_right = right->hit(r, interval(ray_t.min, hit_left ? rec.t : ray_t.max), rec); + + return hit_left || hit_right; + } + + aabb bounding_box() const override { return bbox; } + + private: + shared_ptr<hittable> left; + shared_ptr<hittable> right; + aabb bbox; + }; + + #endif + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [bvh]: <kbd>[bvh.h]</kbd> Bounding volume hierarchy] + +</div> + + +Splitting BVH Volumes +---------------------- +The most complicated part of any efficiency structure, including the BVH, is building it. We do this +in the constructor. A cool thing about BVHs is that as long as the list of objects in a `bvh_node` +gets divided into two sub-lists, the hit function will work. It will work best if the division is +done well, so that the two children have smaller bounding boxes than their parent’s bounding box, +but that is for speed not correctness. I’ll choose the middle ground, and at each node split the +list along one axis. I’ll go for simplicity: + + 1. randomly choose an axis + 2. sort the primitives (`using std::sort`) + 3. put half in each subtree + When the list coming in is two elements, I put one in each subtree and end the recursion. The -traverse algorithm should be smooth and not have to check for null pointers, so if I just have one +traversal algorithm should be smooth and not have to check for null pointers, so if I just have one element I duplicate it in each subtree. Checking explicitly for three elements and just following one recursion would probably help a little, but I figure the whole method will get optimized later. -This yields: +The following code uses three methods--`box_x_compare`, `box_y_compare`, and `box_z_compare`--that +we haven't yet defined. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include "aabb.h" + #include "hittable.h" + #include "hittable_list.h" + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight #include <algorithm> - ... + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - bvh_node::bvh_node( - std::vector<shared_ptr<hittable>>& objects, - size_t start, size_t end, double time0, double time1 - ) { - int axis = random_int(0,2); - auto comparator = (axis == 0) ? box_x_compare - : (axis == 1) ? box_y_compare - : box_z_compare; - - size_t object_span = end - start; - - if (object_span == 1) { - left = right = objects[start]; - } else if (object_span == 2) { - if (comparator(objects[start], objects[start+1])) { + class bvh_node : public hittable { + public: + ... + + bvh_node(std::vector<shared_ptr<hittable>>& objects, size_t start, size_t end) { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + int axis = random_int(0,2); + + auto comparator = (axis == 0) ? box_x_compare + : (axis == 1) ? box_y_compare + : box_z_compare; + + size_t object_span = end - start; + + if (object_span == 1) { + left = right = objects[start]; + } else if (object_span == 2) { left = objects[start]; right = objects[start+1]; } else { - left = objects[start+1]; - right = objects[start]; + std::sort(std::begin(objects) + start, std::begin(objects) + end, comparator); + + auto mid = start + object_span/2; + left = make_shared<bvh_node>(objects, start, mid); + right = make_shared<bvh_node>(objects, mid, end); } - } else { - std::sort(objects.begin() + start, objects.begin() + end, comparator); - auto mid = start + object_span/2; - left = make_shared<bvh_node>(objects, start, mid, time0, time1); - right = make_shared<bvh_node>(objects, mid, end, time0, time1); + bbox = aabb(left->bounding_box(), right->bounding_box()); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ } - aabb box_left, box_right; + ... + }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [bvh-node]: <kbd>[bvh.h]</kbd> Bounding volume hierarchy node] + +<div class='together'> +This uses a new function: `random_int()`: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + ... + + inline double random_double(double min, double max) { + // Returns a random real in [min,max). + return min + (max-min)*random_double(); + } - if ( !left->bounding_box (time0, time1, box_left) - || !right->bounding_box(time0, time1, box_right) - ) - std::cerr << "No bounding box in bvh_node constructor.\n"; - box = surrounding_box(box_left, box_right); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + inline int random_int(int min, int max) { + // Returns a random integer in [min,max]. + return int(random_double(min, max+1)); } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + ... ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [bvh-node]: <kbd>[bvh.h]</kbd> Bounding volume hierarchy node] + [Listing [random-int]: <kbd>[rtweekend.h]</kbd> A function to return random integers in a range] + </div> The check for whether there is a bounding box at all is in case you sent in something like an infinite plane that doesn’t have a bounding box. We don’t have any of those primitives, so it shouldn’t happen until you add such a thing. -<div class='together'> + +The Box Comparison Functions +----------------------------- Now we need to implement the box comparison functions, used by `std::sort()`. To do this, create a -generic comparator returns true if the first argument is less than the second, given an additional -axis index argument. Then define axis-specific comparison functions that use the generic comparison -function. +generic comparator that returns true if the first argument is less than the second, given an +additional axis index argument. Then define axis-specific comparison functions that use the generic +comparison function. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - inline bool box_compare(const shared_ptr<hittable> a, const shared_ptr<hittable> b, int axis) { - aabb box_a; - aabb box_b; + class bvh_node : public hittable { + ... - if (!a->bounding_box(0,0, box_a) || !b->bounding_box(0,0, box_b)) - std::cerr << "No bounding box in bvh_node constructor.\n"; + private: + shared_ptr<hittable> left; + shared_ptr<hittable> right; + aabb bbox; - return box_a.min().e[axis] < box_b.min().e[axis]; - } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + static bool box_compare( + const shared_ptr<hittable> a, const shared_ptr<hittable> b, int axis_index + ) { + auto a_axis_interval = a->bounding_box().axis_interval(axis_index); + auto b_axis_interval = b->bounding_box().axis_interval(axis_index); + return a_axis_interval.min < b_axis_interval.min; + } - bool box_x_compare (const shared_ptr<hittable> a, const shared_ptr<hittable> b) { - return box_compare(a, b, 0); - } + static bool box_x_compare (const shared_ptr<hittable> a, const shared_ptr<hittable> b) { + return box_compare(a, b, 0); + } - bool box_y_compare (const shared_ptr<hittable> a, const shared_ptr<hittable> b) { - return box_compare(a, b, 1); - } + static bool box_y_compare (const shared_ptr<hittable> a, const shared_ptr<hittable> b) { + return box_compare(a, b, 1); + } - bool box_z_compare (const shared_ptr<hittable> a, const shared_ptr<hittable> b) { - return box_compare(a, b, 2); - } + static bool box_z_compare (const shared_ptr<hittable> a, const shared_ptr<hittable> b) { + return box_compare(a, b, 2); + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [bvh-x-comp]: <kbd>[bvh.h]</kbd> BVH comparison function, X-axis] -</div> + +At this point, we're ready to use our new BVH code. Let's use it on our random spheres scene. + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include "rtweekend.h" + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + #include "bvh.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include "camera.h" + #include "hittable.h" + #include "hittable_list.h" + #include "material.h" + #include "sphere.h" + + int main() { + ... + + auto material2 = make_shared<lambertian>(color(0.4, 0.2, 0.1)); + world.add(make_shared<sphere>(point3(-4, 1, 0), 1.0, material2)); + + auto material3 = make_shared<metal>(color(0.7, 0.6, 0.5), 0.0); + world.add(make_shared<sphere>(point3(4, 1, 0), 1.0, material3)); + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + world = hittable_list(make_shared<bvh_node>(world)); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + camera cam; + + ... + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [random-spheres-bvh]: <kbd>[main.cc]</kbd> Random spheres, using BVH] + +The rendered image should be identical to the non-BVH version shown in +[image 1](#image-bouncing-spheres). However, if you time the two versions, the BVH version should be +faster. I see a speedup of almost _six and a half times_ the prior version. + + +Another BVH Optimization +------------------------- +We can speed up the BVH optimization a bit more. Instead of choosing a random splitting axis, let's +split the longest axis of the enclosing bounding box to get the most subdivision. The change is +straight-forward, but we'll add a few things to the `aabb` class in the process. + +The first task is to construct an axis-aligned bounding box of the span of objects in the BVH +constructor. Basically, we'll construct the bounding box of the `bvh_node` from this span by +initializing the bounding box to empty (we'll define `aabb::empty` shortly), and then augment it +with each bounding box in the span of objects. + +Once we have the bounding box, set the splitting axis to the one with the longest side. We'll +imagine a function that does that for us: `aabb::longest_axis()`. Finally, since we're computing the +bounding box of the object span up front, we can delete the original line that computed it as the +union of the left and right sides. + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class bvh_node : public hittable { + public: + ... + bvh_node(std::vector<shared_ptr<hittable>>& objects, size_t start, size_t end) { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + // Build the bounding box of the span of source objects. + bbox = aabb::empty; + for (size_t object_index=start; object_index < end; object_index++) + bbox = aabb(bbox, objects[object_index]->bounding_box()); + + int axis = bbox.longest_axis(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + auto comparator = (axis == 0) ? box_x_compare + : (axis == 1) ? box_y_compare + : box_z_compare; + + ... + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete + bbox = aabb(left->bounding_box(), right->bounding_box()); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } + + ... + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [object-span-bbox]: <kbd>[bvh.h]</kbd> Building the bbox for the span of BVH objects] + +Now to implement the empty `aabb` code and the new `aabb::longest_axis()` function: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class aabb { + public: + ... + + bool hit(const ray& r, interval ray_t) const { + ... + } + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + int longest_axis() const { + // Returns the index of the longest axis of the bounding box. + + if (x.size() > y.size()) + return x.size() > z.size() ? 0 : 2; + else + return y.size() > z.size() ? 1 : 2; + } + + static const aabb empty, universe; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + }; + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + const aabb aabb::empty = aabb(interval::empty, interval::empty, interval::empty); + const aabb aabb::universe = aabb(interval::universe, interval::universe, interval::universe); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [aabb-empty-and-axis]: <kbd>[aabb.h]</kbd> New aabb constants and longest_axis() function] + +As before, you should see identical results to [image 1](#image-bouncing-spheres), but rendering a +little bit faster. On my system, this yields something like an additional 18% render speedup. Not +bad for a little extra work. -Solid Textures +Texture Mapping ==================================================================================================== +_Texture mapping_ in computer graphics is the process of applying a material effect to an object in +the scene. The "texture" part is the effect, and the "mapping" part is in the mathematical sense of +mapping one space onto another. This effect could be any material property: color, shininess, bump +geometry (called _Bump Mapping_), or even material existence (to create cut-out regions of the +surface). + +The most common type of texture mapping maps an image onto the surface of an object, defining the +color at each point on the object’s surface. In practice, we implement the process in reverse: given +some point on the object, we’ll look up the color defined by the texture map. + +To begin with, we'll make the texture colors procedural, and will create a texture map of constant +color. Most programs keep constant RGB colors and textures in different classes, so feel free to do +something different, but I am a big believer in this architecture because it's great being able to +make any color a texture. -A texture in graphics usually means a function that makes the colors on a surface procedural. This -procedure can be synthesis code, or it could be an image lookup, or a combination of both. We will -first make all colors a texture. Most programs keep constant rgb colors and textures different -classes so feel free to do something different, but I am a big believer in this architecture because -being able to make any color a texture is great. +In order to perform the texture lookup, we need a _texture coordinate_. This coordinate can be +defined in many ways, and we'll develop this idea as we progress. For now, we'll pass in two +dimensional texture coordinates. By convention, texture coordinates are named $u$ and $v$. For a +constant texture, every $(u,v)$ pair yields a constant color, so we can actually ignore the +coordinates completely. However, other texture types will need these coordinates, so we keep these +in the method interface. + +The primary method of our texture classes is the `color value(...)` method, which returns the +texture color given the input coordinates. In addition to taking the point's texture coordinates $u$ +and $v$, we also provide the position of the point in question, for reasons that will become +apparent later. + + +Constant Color Texture +----------------------- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - #include "rtweekend.h" + #ifndef TEXTURE_H + #define TEXTURE_H class texture { - public: - virtual vec3 value(double u, double v, const vec3& p) const = 0; + public: + virtual ~texture() = default; + + virtual color value(double u, double v, const point3& p) const = 0; }; - class constant_texture : public texture { - public: - constant_texture() {} - constant_texture(vec3 c) : color(c) {} + class solid_color : public texture { + public: + solid_color(const color& albedo) : albedo(albedo) {} - virtual vec3 value(double u, double v, const vec3& p) const { - return color; - } + solid_color(double red, double green, double blue) : solid_color(color(red,green,blue)) {} + + color value(double u, double v, const point3& p) const override { + return albedo; + } - public: - vec3 color; + private: + color albedo; }; + + #endif ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [texture]: <kbd>[texture.h]</kbd> A texture class] -We'll need to update the `hit_record` structure to store the U,V surface coordinates of the +We'll need to update the `hit_record` structure to store the $u,v$ surface coordinates of the ray-object hit point. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - struct hit_record { + class hit_record { + public: vec3 p; vec3 normal; - shared_ptr<material> mat_ptr; + shared_ptr<material> mat; double t; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight double u; double v; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ bool front_face; + ... ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [hit-record-uv]: <kbd>[hittable.h]</kbd> Adding U,V coordinates to the `hit_record`] + [Listing [hit-record-uv]: <kbd>[hittable.h]</kbd> Adding $u,v$ coordinates to the `hit_record`] + +In the future, we'll need to compute $(u,v)$ texture coordinates for a given point on each type of +`hittable`. More on that later. + + +Solid Textures: A Checker Texture +---------------------------------- +A solid (or spatial) texture depends only on the position of each point in 3D space. You can think +of a solid texture as if it's coloring all of the points in space itself, instead of coloring a +given object in that space. For this reason, the object can move through the colors of the texture +as it changes position, though usually you would to fix the relationship between the object and the +solid texture. + +To explore spatial textures, we'll implement a spatial `checker_texture` class, which implements a +three-dimensional checker pattern. Since a spatial texture function is driven by a given position in +space, the texture `value()` function ignores the `u` and `v` parameters, and uses only the `p` +parameter. + +To accomplish the checkered pattern, we'll first compute the floor of each component of the input +point. We could truncate the coordinates, but that would pull values toward zero, which would give +us the same color on both sides of zero. The floor function will always shift values to the integer +value on the left (toward negative infinity). Given these three integer results ($\lfloor x \rfloor, +\lfloor y \rfloor, \lfloor z \rfloor$) we take their sum and compute the result modulo two, which +gives us either 0 or 1. Zero maps to the even color, and one to the odd color. + +Finally, we add a scaling factor to the texture, to allow us to control the size of the checker +pattern in the scene. + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class checker_texture : public texture { + public: + checker_texture(double scale, shared_ptr<texture> even, shared_ptr<texture> odd) + : inv_scale(1.0 / scale), even(even), odd(odd) {} + + checker_texture(double scale, const color& c1, const color& c2) + : checker_texture(scale, make_shared<solid_color>(c1), make_shared<solid_color>(c2)) {} + + color value(double u, double v, const point3& p) const override { + auto xInteger = int(std::floor(inv_scale * p.x())); + auto yInteger = int(std::floor(inv_scale * p.y())); + auto zInteger = int(std::floor(inv_scale * p.z())); + + bool isEven = (xInteger + yInteger + zInteger) % 2 == 0; + + return isEven ? even->value(u, v, p) : odd->value(u, v, p); + } + + private: + double inv_scale; + shared_ptr<texture> even; + shared_ptr<texture> odd; + }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [checker-texture]: <kbd>[texture.h]</kbd> Checkered texture] + +Those checker odd/even parameters can point to a constant texture or to some other procedural +texture. This is in the spirit of shader networks introduced by Pat Hanrahan back in the 1980s. <div class='together'> -Now we can make textured materials by replacing the vec3 color with a texture pointer: +To support procedural textures, we'll extend the `lambertian` class to work with textures instead of +colors: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include "hittable.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + #include "texture.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + ... + class lambertian : public material { - public: + public: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - lambertian(shared_ptr<texture> a) : albedo(a) {} + lambertian(const color& albedo) : tex(make_shared<solid_color>(albedo)) {} + lambertian(shared_ptr<texture> tex) : tex(tex) {} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - virtual bool scatter( - const ray& r_in, const hit_record& rec, vec3& attenuation, ray& scattered - ) const { - vec3 scatter_direction = rec.normal + random_unit_vector(); - scattered = ray(rec.p, scatter_direction, r_in.time()); + bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) + const override { + auto scatter_direction = rec.normal + random_unit_vector(); + + // Catch degenerate scatter direction + if (scatter_direction.near_zero()) + scatter_direction = rec.normal; + + scattered = ray(rec.p, scatter_direction, r_in.time()); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - attenuation = albedo->value(rec.u, rec.v, rec.p); + attenuation = tex->value(rec.u, rec.v, rec.p); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - return true; - } + return true; + } - public: + private: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - shared_ptr<texture> albedo; + shared_ptr<texture> tex; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [lambertian-textured]: <kbd>[material.h]</kbd> Lambertian material with texture] + </div> <div class='together'> -where you used to have +If we add this to our main scene: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - ...make_shared<lambertian>(vec3(0.5, 0.5, 0.5)) - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [lam-solid]: <kbd>[main.cc]</kbd> Lambertian material with solid color] - -now you should replace the `vec3(...)` with `make_shared<constant_texture>(vec3(...))` + #include "rtweekend.h" + #include "bvh.h" + #include "camera.h" + #include "hittable.h" + #include "hittable_list.h" + #include "material.h" + #include "sphere.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + #include "texture.h" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - ...make_shared<lambertian>(make_shared<constant_texture>(vec3(0.5, 0.5, 0.5))) - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [lam-textured]: <kbd>[main.cc]</kbd> Lambertian material with texture] -</div> -<div class='together'> -We can create a checker texture by noting that the sign of sine and cosine just alternates in a -regular way, and if we multiply trig functions in all three dimensions, the sign of that product -forms a 3D checker pattern. + int main() { + hittable_list world; + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + auto checker = make_shared<checker_texture>(0.32, color(.2, .3, .1), color(.9, .9, .9)); + world.add(make_shared<sphere>(point3(0,-1000,0), 1000, make_shared<lambertian>(checker))); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - class checker_texture : public texture { - public: - checker_texture() {} - checker_texture(shared_ptr<texture> t0, shared_ptr<texture> t1): even(t0), odd(t1) {} - - virtual vec3 value(double u, double v, const vec3& p) const { - auto sines = sin(10*p.x())*sin(10*p.y())*sin(10*p.z()); - if (sines < 0) - return odd->value(u, v, p); - else - return even->value(u, v, p); - } - public: - shared_ptr<texture> odd; - shared_ptr<texture> even; + for (int a = -11; a < 11; a++) { + ... } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [checker-texture]: <kbd>[texture.h]</kbd> Checkered texture] -</div> + [Listing [checker-example]: <kbd>[main.cc]</kbd> Checkered texture in use] -Those checker odd/even pointers can be to a constant texture or to some other procedural texture. -This is in the spirit of shader networks introduced by Pat Hanrahan back in the 1980s. +</div> <div class='together'> -If we add this to our `random_scene()` function’s base sphere: +we get: - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - auto checker = make_shared<checker_texture>( - make_shared<constant_texture>(vec3(0.2, 0.3, 0.1)), - make_shared<constant_texture>(vec3(0.9, 0.9, 0.9)) - ); + ![<span class='num'>Image 2:</span> Spheres on checkered ground + ](../images/img-2.02-checker-ground.png class='pixel') - world.add(make_shared<sphere>(vec3(0,-1000,0), 1000, make_shared<lambertian>(checker))); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [checker-example]: <kbd>[main.cc]</kbd> Checkered texture in use] +</div> -We get: - <div class="render"> +Rendering The Solid Checker Texture +------------------------------------ +We're going to add a second scene to our program, and will add more scenes after that as we progress +through this book. To help with this, we'll set up a switch statement to select the desired scene +for a given run. It's a crude approach, but we're trying to keep things dead simple and focus on the +raytracing. You may want to use a different approach in your own raytracer, such as supporting +command-line arguments. - ![Spheres on checkered ground](../images/img.checker-ground.jpg) +<div class='together'> +Here's what our main.cc looks like after refactoring for our single random spheres scene. Rename +`main()` to `bouncing_spheres()`, and add a new `main()` function to call it: - </div> + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include "rtweekend.h" -</div> + #include "bvh.h" + #include "camera.h" + #include "hittable.h" + #include "hittable_list.h" + #include "material.h" + #include "sphere.h" + #include "texture.h" -<div class='together'> -If we add a new scene: + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete + int main() { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + void bouncing_spheres() { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - hittable_list two_spheres() { - hittable_list objects; + hittable_list world; - auto checker = make_shared<checker_texture>( - make_shared<constant_texture>(vec3(0.2, 0.3, 0.1)), - make_shared<constant_texture>(vec3(0.9, 0.9, 0.9)) - ); + auto ground_material = make_shared<lambertian>(color(0.5, 0.5, 0.5)); + world.add(make_shared<sphere>(point3(0,-1000,0), 1000, ground_material)); - objects.add(make_shared<sphere>(vec3(0,-10, 0), 10, make_shared<lambertian>(checker))); - objects.add(make_shared<sphere>(vec3(0, 10, 0), 10, make_shared<lambertian>(checker))); + ... - return objects; + cam.render(world); } - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [scene-two-checker]: <kbd>[main.cc]</kbd> Scene with two checkered spheres] - -With camera: - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - const auto aspect_ratio = double(image_width) / image_height; - ... - vec3 lookfrom(13,2,3); - vec3 lookat(0,0,0); - vec3 vup(0,1,0); - auto dist_to_focus = 10.0; - auto aperture = 0.0; - camera cam(lookfrom, lookat, vup, 20, aspect_ratio, aperture, dist_to_focus, 0.0, 1.0); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + int main() { + bouncing_spheres(); + } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [scene-two-checker-view]: <kbd>[main.cc]</kbd> Viewing parameters] + [Listing [main-scenes]: <kbd>[main.cc]</kbd> Main dispatching to selected scene] -We get: +</div> - <div class="render"> +<div class='together'> +Now add a scene with two checkered spheres, one atop the other. - ![Checkered spheres](../images/img.checker-spheres.jpg) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include "rtweekend.h" - </div> + #include "bvh.h" + #include "camera.h" + #include "hittable.h" + #include "hittable_list.h" + #include "material.h" + #include "sphere.h" + #include "texture.h" + + + void bouncing_spheres() { + ... + } + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + void checkered_spheres() { + hittable_list world; + + auto checker = make_shared<checker_texture>(0.32, color(.2, .3, .1), color(.9, .9, .9)); + + world.add(make_shared<sphere>(point3(0,-10, 0), 10, make_shared<lambertian>(checker))); + world.add(make_shared<sphere>(point3(0, 10, 0), 10, make_shared<lambertian>(checker))); + + camera cam; + + cam.aspect_ratio = 16.0 / 9.0; + cam.image_width = 400; + cam.samples_per_pixel = 100; + cam.max_depth = 50; + + cam.vfov = 20; + cam.lookfrom = point3(13,2,3); + cam.lookat = point3(0,0,0); + cam.vup = vec3(0,1,0); + + cam.defocus_angle = 0; + + cam.render(world); + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + int main() { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + switch (2) { + case 1: bouncing_spheres(); break; + case 2: checkered_spheres(); break; + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [main-two-spheres]: <kbd>[main.cc]</kbd> Two checkered spheres] + +</div> + +<div class='together'> +We get this result: + + ![<span class='num'>Image 3:</span> Checkered spheres + ](../images/img-2.03-checker-spheres.png class='pixel') + +</div> + +You may think the result looks a bit odd. Since `checker_texture` is a spatial texture, we're really +looking at the surface of the sphere cutting through the three-dimensional checker space. There are +many situations where this is perfect, or at least sufficient. In many other situations, we really +want to get a consistent effect on the surface of our objects. That approach is covered next. + + +Texture Coordinates for Spheres +-------------------------------- +Constant-color textures use no coordinates. Solid (or spatial) textures use the coordinates of a +point in space. Now it's time to make use of the $u,v$ texture coordinates. These coordinates +specify the location on 2D source image (or in some 2D parameterized space). To get this, we need a +way to find the $u,v$ coordinates of any point on the surface of a 3D object. This mapping is +completely arbitrary, but generally you'd like to cover the entire surface, and be able to scale, +orient and stretch the 2D image in a way that makes some sense. We'll start with deriving a scheme +to get the $u,v$ coordinates of a sphere. + +For spheres, texture coordinates are usually based on some form of longitude and latitude, _i.e._, +spherical coordinates. So we compute $(\theta,\phi)$ in spherical coordinates, where $\theta$ is the +angle up from the bottom pole (that is, up from -Y), and $\phi$ is the angle around the Y-axis (from +-X to +Z to +X to -Z back to -X). + +We want to map $\theta$ and $\phi$ to texture coordinates $u$ and $v$ each in $[0,1]$, where +$(u=0,v=0)$ maps to the bottom-left corner of the texture. Thus the normalization from +$(\theta,\phi)$ to $(u,v)$ would be: + + $$ u = \frac{\phi}{2\pi} $$ + $$ v = \frac{\theta}{\pi} $$ + +To compute $\theta$ and $\phi$ for a given point on the unit sphere centered at the origin, we start +with the equations for the corresponding Cartesian coordinates: + + $$ \begin{align*} + y &= -\cos(\theta) \\ + x &= -\cos(\phi) \sin(\theta) \\ + z &= \quad\sin(\phi) \sin(\theta) + \end{align*} + $$ + +We need to invert these equations to solve for $\theta$ and $\phi$. Because of the lovely `<cmath>` +function `std::atan2()`, which takes any pair of numbers proportional to sine and cosine and returns +the angle, we can pass in $x$ and $z$ (the $\sin(\theta)$ cancel) to solve for $\phi$: + + $$ \phi = \operatorname{atan2}(z, -x) $$ + +`std::atan2()` returns values in the range $-\pi$ to $\pi$, but they go from 0 to $\pi$, then flip +to $-\pi$ and proceed back to zero. While this is mathematically correct, we want $u$ to range from +$0$ to $1$, not from $0$ to $1/2$ and then from $-1/2$ to $0$. Fortunately, + + $$ \operatorname{atan2}(a,b) = \operatorname{atan2}(-a,-b) + \pi, $$ + +and the second formulation yields values from $0$ continuously to $2\pi$. Thus, we can compute +$\phi$ as + + $$ \phi = \operatorname{atan2}(-z, x) + \pi $$ + +The derivation for $\theta$ is more straightforward: + + $$ \theta = \arccos(-y) $$ + +<div class='together'> +So for a sphere, the $(u,v)$ coord computation is accomplished by a utility function that takes +points on the unit sphere centered at the origin, and computes $u$ and $v$: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class sphere : public hittable { + ... + private: + ... + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + static void get_sphere_uv(const point3& p, double& u, double& v) { + // p: a given point on the sphere of radius one, centered at the origin. + // u: returned value [0,1] of angle around the Y axis from X=-1. + // v: returned value [0,1] of angle from Y=-1 to Y=+1. + // <1 0 0> yields <0.50 0.50> <-1 0 0> yields <0.00 0.50> + // <0 1 0> yields <0.50 1.00> < 0 -1 0> yields <0.50 0.00> + // <0 0 1> yields <0.25 0.50> < 0 0 -1> yields <0.75 0.50> + + auto theta = std::acos(-p.y()); + auto phi = std::atan2(-p.z(), p.x()) + pi; + + u = phi / (2*pi); + v = theta / pi; + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [get-sphere-uv]: <kbd>[sphere.h]</kbd> get_sphere_uv function] + +</div> + +<div class='together'> +Update the `sphere::hit()` function to use this function to update the hit record UV coordinates. + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class sphere : public hittable { + public: + ... + bool hit(const ray& r, interval ray_t, hit_record& rec) const override { + ... + + rec.t = root; + rec.p = r.at(rec.t); + vec3 outward_normal = (rec.p - current_center) / radius; + rec.set_face_normal(r, outward_normal); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + get_sphere_uv(outward_normal, rec.u, rec.v); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + rec.mat = mat; + + return true; + } + ... + }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [get-sphere-uv-call]: <kbd>[sphere.h]</kbd> Sphere UV coordinates from hit] + +</div> + +From the hitpoint $\mathbf{P}$, we compute the surface coordinates $(u,v)$. We then use these to +index into our procedural solid texture (like marble). We can also read in an image and use the 2D +$(u,v)$ texture coordinate to index into the image. + +A direct way to use scaled $(u,v)$ in an image is to round the $u$ and $v$ to integers, and use that +as $(i,j)$ pixels. This is awkward, because we don’t want to have to change the code when we change +image resolution. So instead, one of the the most universal unofficial standards in graphics is to +use texture coordinates instead of image pixel coordinates. These are just some form of fractional +position in the image. For example, for pixel $(i,j)$ in an $N_x$ by $N_y$ image, the image texture +position is: + + $$ u = \frac{i}{N_x-1} $$ + $$ v = \frac{j}{N_y-1} $$ + +This is just a fractional position. + + +Accessing Texture Image Data +----------------------------- +Now it's time to create a texture class that holds an image. I am going to use my favorite image +utility, [stb_image][]. It reads image data into an array of 32-bit floating-point values. These are +just packed RGBs with each component in the range [0,1] (black to full white). In addition, images +are loaded in linear color space (gamma = 1) -- the color space in which we do all our computations. + +To help make loading our image files even easier, we provide a helper class to manage all this: +`rtw_image`. It provides a helper function -- `pixel_data(int x, int y)` -- to get the 8-bit RGB +byte values for each pixel. The following listing assumes that you have copied the `stb_image.h` +header into a folder called `external`. Adjust according to your directory structure. + +<script type="preformatted"> + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #ifndef RTW_STB_IMAGE_H + #define RTW_STB_IMAGE_H + + // Disable strict warnings for this header from the Microsoft Visual C++ compiler. + #ifdef _MSC_VER + #pragma warning (push, 0) + #endif + + #define STB_IMAGE_IMPLEMENTATION + #define STBI_FAILURE_USERMSG + #include "external/stb_image.h" + + #include <cstdlib> + #include <iostream> + + class rtw_image { + public: + rtw_image() {} + + rtw_image(const char* image_filename) { + // Loads image data from the specified file. If the RTW_IMAGES environment variable is + // defined, looks only in that directory for the image file. If the image was not found, + // searches for the specified image file first from the current directory, then in the + // images/ subdirectory, then the _parent's_ images/ subdirectory, and then _that_ + // parent, on so on, for six levels up. If the image was not loaded successfully, + // width() and height() will return 0. + + auto filename = std::string(image_filename); + auto imagedir = getenv("RTW_IMAGES"); + + // Hunt for the image file in some likely locations. + if (imagedir && load(std::string(imagedir) + "/" + image_filename)) return; + if (load(filename)) return; + if (load("images/" + filename)) return; + if (load("../images/" + filename)) return; + if (load("../../images/" + filename)) return; + if (load("../../../images/" + filename)) return; + if (load("../../../../images/" + filename)) return; + if (load("../../../../../images/" + filename)) return; + if (load("../../../../../../images/" + filename)) return; + + std::cerr << "ERROR: Could not load image file '" << image_filename << "'.\n"; + } + + ~rtw_image() { + delete[] bdata; + STBI_FREE(fdata); + } + + bool load(const std::string& filename) { + // Loads the linear (gamma=1) image data from the given file name. Returns true if the + // load succeeded. The resulting data buffer contains the three [0.0, 1.0] + // floating-point values for the first pixel (red, then green, then blue). Pixels are + // contiguous, going left to right for the width of the image, followed by the next row + // below, for the full height of the image. + + auto n = bytes_per_pixel; // Dummy out parameter: original components per pixel + fdata = stbi_loadf(filename.c_str(), &image_width, &image_height, &n, bytes_per_pixel); + if (fdata == nullptr) return false; + + bytes_per_scanline = image_width * bytes_per_pixel; + convert_to_bytes(); + return true; + } + + int width() const { return (fdata == nullptr) ? 0 : image_width; } + int height() const { return (fdata == nullptr) ? 0 : image_height; } + + const unsigned char* pixel_data(int x, int y) const { + // Return the address of the three RGB bytes of the pixel at x,y. If there is no image + // data, returns magenta. + static unsigned char magenta[] = { 255, 0, 255 }; + if (bdata == nullptr) return magenta; + + x = clamp(x, 0, image_width); + y = clamp(y, 0, image_height); + + return bdata + y*bytes_per_scanline + x*bytes_per_pixel; + } + + private: + const int bytes_per_pixel = 3; + float *fdata = nullptr; // Linear floating point pixel data + unsigned char *bdata = nullptr; // Linear 8-bit pixel data + int image_width = 0; // Loaded image width + int image_height = 0; // Loaded image height + int bytes_per_scanline = 0; + + static int clamp(int x, int low, int high) { + // Return the value clamped to the range [low, high). + if (x < low) return low; + if (x < high) return x; + return high - 1; + } + + static unsigned char float_to_byte(float value) { + if (value <= 0.0) + return 0; + if (1.0 <= value) + return 255; + return static_cast<unsigned char>(256.0 * value); + } + + void convert_to_bytes() { + // Convert the linear floating point pixel data to bytes, storing the resulting byte + // data in the `bdata` member. + + int total_bytes = image_width * image_height * bytes_per_pixel; + bdata = new unsigned char[total_bytes]; + + // Iterate through all pixel components, converting from [0.0, 1.0] float values to + // unsigned [0, 255] byte values. + + auto *bptr = bdata; + auto *fptr = fdata; + for (auto i=0; i < total_bytes; i++, fptr++, bptr++) + *bptr = float_to_byte(*fptr); + } + }; + + // Restore MSVC compiler warnings + #ifdef _MSC_VER + #pragma warning (pop) + #endif + + #endif + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [rtw_image]: <kbd>[rtw_stb_image.h] The rtw_image helper class] +</script> + +If you are writing your implementation in a language other than C or C++, you'll need to locate (or +write) an image loading library that provides similar functionality. + +<div class='together'> +The `image_texture` class uses the `rtw_image` class: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + #include "rtw_stb_image.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + ... + + class checker_texture : public texture { + ... + }; + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + class image_texture : public texture { + public: + image_texture(const char* filename) : image(filename) {} + + color value(double u, double v, const point3& p) const override { + // If we have no texture data, then return solid cyan as a debugging aid. + if (image.height() <= 0) return color(0,1,1); + + // Clamp input texture coordinates to [0,1] x [1,0] + u = interval(0,1).clamp(u); + v = 1.0 - interval(0,1).clamp(v); // Flip V to image coordinates + + auto i = int(u * image.width()); + auto j = int(v * image.height()); + auto pixel = image.pixel_data(i,j); + + auto color_scale = 1.0 / 255.0; + return color(color_scale*pixel[0], color_scale*pixel[1], color_scale*pixel[2]); + } + + private: + rtw_image image; + }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [img-texture]: <kbd>[texture.h]</kbd> Image texture class] + +</div> + + +Rendering The Image Texture +---------------------------- +I just grabbed a random earth map from the web -- any standard projection will do for our purposes. + + ![<span class='num'>Image 4:</span> earthmap.jpg](../images/earthmap.jpg class='pixel') + +<div class='together'> +Here's the code to read an image from a file and then assign it to a diffuse material: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + void earth() { + auto earth_texture = make_shared<image_texture>("earthmap.jpg"); + auto earth_surface = make_shared<lambertian>(earth_texture); + auto globe = make_shared<sphere>(point3(0,0,0), 2, earth_surface); + + camera cam; + + cam.aspect_ratio = 16.0 / 9.0; + cam.image_width = 400; + cam.samples_per_pixel = 100; + cam.max_depth = 50; + + cam.vfov = 20; + cam.lookfrom = point3(0,0,12); + cam.lookat = point3(0,0,0); + cam.vup = vec3(0,1,0); + + cam.defocus_angle = 0; + + cam.render(hittable_list(globe)); + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + int main() { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + switch (3) { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + case 1: bouncing_spheres(); break; + case 2: checkered_spheres(); break; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + case 3: earth(); break; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [stbi-load-use]: <kbd>[main.cc]</kbd> Rendering with a texture map of Earth] + +</div> + +We start to see some of the power of all colors being textures -- we can assign any kind of texture +to the lambertian material, and lambertian doesn’t need to be aware of it. + +If the photo comes back with a large cyan sphere in the middle, then `stb_image` failed to find your +Earth map photo. The program will look for the file in the same directory as the executable. Make +sure to copy the Earth into your build directory, or rewrite `earth()` to point somewhere else. + + ![<span class='num'>Image 5:</span> Earth-mapped sphere + ](../images/img-2.05-earth-sphere.png class='pixel') + + + +Perlin Noise +==================================================================================================== +To get cool looking solid textures most people use some form of Perlin noise. These are named after +their inventor Ken Perlin. Perlin texture doesn’t return white noise like this: + + ![<span class='num'>Image 6:</span> White noise](../images/img-2.06-white-noise.jpg class='pixel') + +<div class='together'> +Instead it returns something similar to blurred white noise: + + ![<span class='num'>Image 7:</span> White noise, blurred + ](../images/img-2.07-white-noise-blurred.jpg class='pixel') + +</div> + +A key part of Perlin noise is that it is repeatable: it takes a 3D point as input and always returns +the same randomish number. Nearby points return similar numbers. Another important part of Perlin +noise is that it be simple and fast, so it’s usually done as a hack. I’ll build that hack up +incrementally based on Andrew Kensler’s description. + + +Using Blocks of Random Numbers +------------------------------- +We could just tile all of space with a 3D array of random numbers and use them in blocks. You get +something blocky where the repeating is clear: + + ![<span class='num'>Image 8:</span> Tiled random patterns + ](../images/img-2.08-tile-random.jpg class='pixel') + +<div class='together'> +Let’s just use some sort of hashing to scramble this, instead of tiling. This has a bit of support +code to make it all happen: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #ifndef PERLIN_H + #define PERLIN_H + + class perlin { + public: + perlin() { + for (int i = 0; i < point_count; i++) { + randfloat[i] = random_double(); + } + + perlin_generate_perm(perm_x); + perlin_generate_perm(perm_y); + perlin_generate_perm(perm_z); + } + + double noise(const point3& p) const { + auto i = int(4*p.x()) & 255; + auto j = int(4*p.y()) & 255; + auto k = int(4*p.z()) & 255; + + return randfloat[perm_x[i] ^ perm_y[j] ^ perm_z[k]]; + } + + private: + static const int point_count = 256; + double randfloat[point_count]; + int perm_x[point_count]; + int perm_y[point_count]; + int perm_z[point_count]; + + static void perlin_generate_perm(int* p) { + for (int i = 0; i < point_count; i++) + p[i] = i; + + permute(p, point_count); + } + + static void permute(int* p, int n) { + for (int i = n-1; i > 0; i--) { + int target = random_int(0, i); + int tmp = p[i]; + p[i] = p[target]; + p[target] = tmp; + } + } + }; + + #endif + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [perlin]: <kbd>[perlin.h]</kbd> A Perlin texture class and functions] + +</div> + +<div class='together'> +Now if we create an actual texture that takes these floats between 0 and 1 and creates grey colors: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + #include "perlin.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include "rtw_stb_image.h" + + ... + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + class noise_texture : public texture { + public: + noise_texture() {} + + color value(double u, double v, const point3& p) const override { + return color(1,1,1) * noise.noise(p); + } + + private: + perlin noise; + }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [noise-texture]: <kbd>[texture.h]</kbd> Noise texture] + +</div> + +<div class='together'> +We can use that texture on some spheres: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + void perlin_spheres() { + hittable_list world; + + auto pertext = make_shared<noise_texture>(); + world.add(make_shared<sphere>(point3(0,-1000,0), 1000, make_shared<lambertian>(pertext))); + world.add(make_shared<sphere>(point3(0,2,0), 2, make_shared<lambertian>(pertext))); + + camera cam; + + cam.aspect_ratio = 16.0 / 9.0; + cam.image_width = 400; + cam.samples_per_pixel = 100; + cam.max_depth = 50; + + cam.vfov = 20; + cam.lookfrom = point3(13,2,3); + cam.lookat = point3(0,0,0); + cam.vup = vec3(0,1,0); + + cam.defocus_angle = 0; + + cam.render(world); + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + int main() { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + switch (4) { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + case 1: bouncing_spheres(); break; + case 2: checkered_spheres(); break; + case 3: earth(); break; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + case 4: perlin_spheres(); break; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [scene-perlin]: <kbd>[main.cc]</kbd> Scene with two Perlin-textured spheres] + +</div> + +<div class='together'> +And the hashing does scramble as hoped: + + ![<span class='num'>Image 9:</span> Hashed random texture + ](../images/img-2.09-hash-random.png class='pixel') + +</div> + + +Smoothing out the Result +------------------------- +To make it smooth, we can linearly interpolate: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + class perlin { + public: + ... + + double noise(const point3& p) const { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + auto u = p.x() - std::floor(p.x()); + auto v = p.y() - std::floor(p.y()); + auto w = p.z() - std::floor(p.z()); + + auto i = int(std::floor(p.x())); + auto j = int(std::floor(p.y())); + auto k = int(std::floor(p.z())); + double c[2][2][2]; + + for (int di=0; di < 2; di++) + for (int dj=0; dj < 2; dj++) + for (int dk=0; dk < 2; dk++) + c[di][dj][dk] = randfloat[ + perm_x[(i+di) & 255] ^ + perm_y[(j+dj) & 255] ^ + perm_z[(k+dk) & 255] + ]; + + return trilinear_interp(c, u, v, w); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } + + ... + + private: + ... + + static void permute(int* p, int n) { + ... + } + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + static double trilinear_interp(double c[2][2][2], double u, double v, double w) { + auto accum = 0.0; + for (int i=0; i < 2; i++) + for (int j=0; j < 2; j++) + for (int k=0; k < 2; k++) + accum += (i*u + (1-i)*(1-u)) + * (j*v + (1-j)*(1-v)) + * (k*w + (1-k)*(1-w)) + * c[i][j][k]; + + return accum; + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [perlin-trilinear]: <kbd>[perlin.h]</kbd> Perlin with trilinear interpolation] + +<div class='together'> +And we get: + + ![<span class='num'>Image 10:</span> Perlin texture with trilinear interpolation + ](../images/img-2.10-perlin-trilerp.png class='pixel') + +</div> + + +Improvement with Hermitian Smoothing +------------------------------------- +Smoothing yields an improved result, but there are obvious grid features in there. Some of it is +Mach bands, a known perceptual artifact of linear interpolation of color. A standard trick is to use +a Hermite cubic to round off the interpolation: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class perlin ( + public: + ... + double noise(const point3& p) const { + auto u = p.x() - std::floor(p.x()); + auto v = p.y() - std::floor(p.y()); + auto w = p.z() - std::floor(p.z()); + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + u = u*u*(3-2*u); + v = v*v*(3-2*v); + w = w*w*(3-2*w); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + auto i = int(std::floor(p.x())); + auto j = int(std::floor(p.y())); + auto k = int(std::floor(p.z())); + ... + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [perlin-smoothed]: <kbd>[perlin.h]</kbd> Perlin with Hermitian smoothing] + +<div class='together'> +This gives a smoother looking image: + + ![<span class='num'>Image 11:</span> Perlin texture, trilinearly interpolated, smoothed + ](../images/img-2.11-perlin-trilerp-smooth.png class='pixel') </div> +Tweaking The Frequency +----------------------- +It is also a bit low frequency. We can scale the input point to make it vary more quickly: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class noise_texture : public texture { + public: + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + noise_texture(double scale) : scale(scale) {} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + color value(double u, double v, const point3& p) const override { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + return color(1,1,1) * noise.noise(scale * p); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } + + private: + perlin noise; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + double scale; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [perlin-smoothed-2]: <kbd>[texture.h]</kbd> Perlin smoothed, higher frequency] + +<div class='together'> +We then add that scale to the `perlin_spheres()` scene description: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + void perlin_spheres() { + ... + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + auto pertext = make_shared<noise_texture>(4); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + world.add(make_shared<sphere>(point3(0,-1000,0), 1000, make_shared<lambertian>(pertext))); + world.add(make_shared<sphere>(point3(0, 2, 0), 2, make_shared<lambertian>(pertext))); + + camera cam; + ... + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [scale-perlin]: <kbd>[main.cc]</kbd> Perlin-textured spheres with a scale to the noise] + +</div> + +<div class='together'> +This yields the following result: + + ![<span class='num'>Image 12:</span> Perlin texture, higher frequency + ](../images/img-2.12-perlin-hifreq.png class='pixel') + +</div> + + +Using Random Vectors on the Lattice Points +------------------------------------------- +This is still a bit blocky looking, probably because the min and max of the pattern always lands +exactly on the integer x/y/z. Ken Perlin’s very clever trick was to instead put random unit vectors +(instead of just floats) on the lattice points, and use a dot product to move the min and max off +the lattice. So, first we need to change the random floats to random vectors. These vectors are any +reasonable set of irregular directions, and I won't bother to make them exactly uniform: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class perlin { + public: + perlin() { + for (int i = 0; i < point_count; i++) { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + randvec[i] = unit_vector(vec3::random(-1,1)); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } + + perlin_generate_perm(perm_x); + perlin_generate_perm(perm_y); + perlin_generate_perm(perm_z); + } + + ... + + private: + static const int point_count = 256; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + vec3 randvec[point_count]; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + int perm_x[point_count]; + int perm_y[point_count]; + int perm_z[point_count]; + ... + }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [perlin-randunit]: <kbd>[perlin.h]</kbd> Perlin with random unit translations] + +<div class='together'> +The Perlin class `noise()` method is now: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class perlin { + public: + ... + double noise(const point3& p) const { + auto u = p.x() - std::floor(p.x()); + auto v = p.y() - std::floor(p.y()); + auto w = p.z() - std::floor(p.z()); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete + u = u*u*(3-2*u); + v = v*v*(3-2*v); + w = w*w*(3-2*w); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + auto i = int(std::floor(p.x())); + auto j = int(std::floor(p.y())); + auto k = int(std::floor(p.z())); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + vec3 c[2][2][2]; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + for (int di=0; di < 2; di++) + for (int dj=0; dj < 2; dj++) + for (int dk=0; dk < 2; dk++) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + c[di][dj][dk] = randvec[ + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + perm_x[(i+di) & 255] ^ + perm_y[(j+dj) & 255] ^ + perm_z[(k+dk) & 255] + ]; + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + return perlin_interp(c, u, v, w); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } + + ... + }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [perlin-2]: <kbd>[perlin.h]</kbd> Perlin class with new noise() method] + +</div> + +<div class='together'> +And the interpolation becomes a bit more complicated: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class perlin { + ... + private: + ... + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete + static double trilinear_interp(double c[2][2][2], double u, double v, double w) { + ... + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + static double perlin_interp(const vec3 c[2][2][2], double u, double v, double w) { + auto uu = u*u*(3-2*u); + auto vv = v*v*(3-2*v); + auto ww = w*w*(3-2*w); + auto accum = 0.0; + + for (int i=0; i < 2; i++) + for (int j=0; j < 2; j++) + for (int k=0; k < 2; k++) { + vec3 weight_v(u-i, v-j, w-k); + accum += (i*uu + (1-i)*(1-uu)) + * (j*vv + (1-j)*(1-vv)) + * (k*ww + (1-k)*(1-ww)) + * dot(c[i][j][k], weight_v); + } + + return accum; + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [perlin-interp]: <kbd>[perlin.h]</kbd> Perlin interpolation function so far] + +</div> + +The output of the Perlin interpolation function can return negative values. These negative values +will be passed to our `linear_to_gamma()` color function, which expects only positive inputs. To +mitigate this, we'll map the $[-1,+1]$ range of values to $[0,1]$. + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class noise_texture : public texture { + public: + noise_texture(double scale) : scale(scale) {} + + color value(double u, double v, const point3& p) const override { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + return color(1,1,1) * 0.5 * (1.0 + noise.noise(scale * p)); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } + + private: + perlin noise; + double scale; + }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [perlin-smoothed-2]: <kbd>[texture.h]</kbd> Perlin smoothed, higher frequency] + +<div class='together'> +This finally gives something more reasonable looking: + + ![<span class='num'>Image 13:</span> Perlin texture, shifted off integer values + ](../images/img-2.13-perlin-shift.png class='pixel') + +</div> + + +Introducing Turbulence +----------------------- +Very often, a composite noise that has multiple summed frequencies is used. This is usually called +turbulence, and is a sum of repeated calls to noise: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class perlin { + ... + public: + ... + + double noise(const point3& p) const { + ... + } + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + double turb(const point3& p, int depth) const { + auto accum = 0.0; + auto temp_p = p; + auto weight = 1.0; + + for (int i = 0; i < depth; i++) { + accum += weight * noise(temp_p); + weight *= 0.5; + temp_p *= 2; + } + + return std::fabs(accum); + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + ... + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [perlin-turb]: <kbd>[perlin.h]</kbd> Turbulence function] + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class noise_texture : public texture { + public: + noise_texture(double scale) : scale(scale) {} + + color value(double u, double v, const point3& p) const override { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + return color(1,1,1) * noise.turb(p, 7); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } + + private: + perlin noise; + double scale; + }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [noise-tex-2]: <kbd>[texture.h]</kbd> Noise texture with turbulence] + +<div class='together'> +Used directly, turbulence gives a sort of camouflage netting appearance: + + ![<span class='num'>Image 14:</span> Perlin texture with turbulence + ](../images/img-2.14-perlin-turb.png class='pixel') + +</div> + + +Adjusting the Phase +-------------------- +However, usually turbulence is used indirectly. For example, the “hello world” of procedural solid +textures is a simple marble-like texture. The basic idea is to make color proportional to something +like a sine function, and use turbulence to adjust the phase (so it shifts $x$ in $\sin(x)$) which +makes the stripes undulate. Commenting out straight noise and turbulence, and giving a marble-like +effect is: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class noise_texture : public texture { + public: + noise_texture(double scale) : scale(scale) {} + + color value(double u, double v, const point3& p) const override { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + return color(.5, .5, .5) * (1 + std::sin(scale * p.z() + 10 * noise.turb(p, 7))); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } -Perlin Noise -==================================================================================================== + private: + perlin noise; + double scale; + }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [noise-tex-3]: <kbd>[texture.h]</kbd> Noise texture with marbled texture] <div class='together'> -To get cool looking solid textures most people use some form of Perlin noise. These are named after -their inventor Ken Perlin. Perlin texture doesn’t return white noise like this: +Which yields: - <div class="render"> + ![<span class='num'>Image 15:</span> Perlin noise, marbled texture + ](../images/img-2.15-perlin-marble.png class='pixel') - ![White noise](../images/img.white-noise.jpg) +</div> - </div> -Instead it returns something similar to blurred white noise: - <div class="render"> +Quadrilaterals +==================================================================================================== +We've managed to get more than half way through this three-book series using spheres as our only +geometric primitive. Time to add our second primitive: the quadrilateral. - ![White noise, blurred](../images/img.white-noise-blur.jpg) - </div> +Defining the Quadrilateral +--------------------------- +Though we'll name our new primitive a `quad`, it will technically be a parallelogram (opposite sides +are parallel) instead of a general quadrilateral. For our purposes, we'll use three geometric +entities to define a quad: -</div> + 1. $\mathbf{Q}$, the starting corner. + 2. $\mathbf{u}$, a vector representing the first side. + $\mathbf{Q} + \mathbf{u}$ gives one of the corners adjacent to $\mathbf{Q}$. + 3. $\mathbf{v}$, a vector representing the second side. + $\mathbf{Q} + \mathbf{v}$ gives the other corner adjacent to $\mathbf{Q}$. -A key part of Perlin noise is that it is repeatable: it takes a 3D point as input and always returns -the same randomish number. Nearby points return similar numbers. Another important part of Perlin -noise is that it be simple and fast, so it’s usually done as a hack. I’ll build that hack up -incrementally based on Andrew Kensler’s description. +The corner of the quad opposite $\mathbf{Q}$ is given by $\mathbf{Q} + \mathbf{u} + \mathbf{v}$. +These values are three-dimensional, even though a quad itself is a two-dimensional object. For +example, a quad with corner at the origin and extending two units in the Z direction and one unit in +the Y direction would have values $\mathbf{Q} = (0,0,0), \mathbf{u} = (0,0,2), \text{and } +\mathbf{v} = (0,1,0)$. <div class='together'> -We could just tile all of space with a 3D array of random numbers and use them in blocks. You get -something blocky where the repeating is clear: - - <div class="render"> - - ![Tiled random patterns](../images/img.tile-random.jpg) +The following figure illustrates the quadrilateral components. - </div> + ![Figure [quad-def]: Quadrilateral Components](../images/fig-2.05-quad-def.jpg) </div> <div class='together'> -Let’s just use some sort of hashing to scramble this, instead of tiling. This has a bit of support -code to make it all happen: +Quads are flat, so their axis-aligned bounding box will have zero thickness in one dimension if the +quad lies in the XY, YZ, or ZX plane. This can lead to numerical problems with ray intersection, but +we can address this by padding any zero-sized dimensions of the bounding box. Padding is fine +because we aren't changing the intersection of the quad; we're only expanding its bounding box to +remove the possibility of numerical problems, and the bounds are just a rough approximation to the +actual shape anyway. To remedy this situation, we insert a small padding to ensure that newly +constructed AABBs always have a non-zero volume: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - class perlin { - public: - perlin() { - ranfloat = new double[point_count]; - for (int i = 0; i < point_count; ++i) { - ranfloat[i] = random_double(); - } - - perm_x = perlin_generate_perm(); - perm_y = perlin_generate_perm(); - perm_z = perlin_generate_perm(); - } - - ~perlin() { - delete[] ranfloat; - delete[] perm_x; - delete[] perm_y; - delete[] perm_z; - } + ... + class aabb { + public: + ... + aabb(const interval& x, const interval& y, const interval& z) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + : x(x), y(y), z(z) + { + pad_to_minimums(); + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - double noise(const vec3& p) const { - auto u = p.x() - floor(p.x()); - auto v = p.y() - floor(p.y()); - auto w = p.z() - floor(p.z()); - auto i = static_cast<int>(4*p.x()) & 255; - auto j = static_cast<int>(4*p.y()) & 255; - auto k = static_cast<int>(4*p.z()) & 255; + aabb(const point3& a, const point3& b) { + // Treat the two points a and b as extrema for the bounding box, so we don't require a + // particular minimum/maximum coordinate order. - return ranfloat[perm_x[i] ^ perm_y[j] ^ perm_z[k]]; - } + x = interval(std::fmin(a[0],b[0]), std::fmax(a[0],b[0])); + y = interval(std::fmin(a[1],b[1]), std::fmax(a[1],b[1])); + z = interval(std::fmin(a[2],b[2]), std::fmax(a[2],b[2])); - private: - static const int point_count = 256; - double* ranfloat; - int* perm_x; - int* perm_y; - int* perm_z; - static int* perlin_generate_perm() { - auto p = new int[point_count]; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + pad_to_minimums(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } + ... + static const aabb empty, universe; - for (int i = 0; i < perlin::point_count; i++) - p[i] = i; - permute(p, point_count); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + private: - return p; - } + void pad_to_minimums() { + // Adjust the AABB so that no side is narrower than some delta, padding if necessary. - void permute(int* p, int n) { - for (int i = n-1; i > 0; i--) { - int target = random_int(0, i); - int tmp = p[i]; - p[i] = p[target]; - p[target] = tmp; - } - } + double delta = 0.0001; + if (x.size() < delta) x = x.expand(delta); + if (y.size() < delta) y = y.expand(delta); + if (z.size() < delta) z = z.expand(delta); + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [perlin]: <kbd>[perlin.h]</kbd> A Perlin texture class and functions] + [Listing [aabb]: <kbd>[aabb.h]</kbd> New aabb::pad_to_minimums() method] + </div> <div class='together'> -Now if we create an actual texture that takes these floats between 0 and 1 and creates grey -colors: +Now we're ready for the first sketch of the new `quad` class: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - #include "perlin.h" + #ifndef QUAD_H + #define QUAD_H - class noise_texture : public texture { - public: - noise_texture() {} + #include "hittable.h" - virtual vec3 value(double u, double v, const vec3& p) const { - return vec3(1,1,1) * noise.noise(p); - } + class quad : public hittable { + public: + quad(const point3& Q, const vec3& u, const vec3& v, shared_ptr<material> mat) + : Q(Q), u(u), v(v), mat(mat) + { + set_bounding_box(); + } - public: - perlin noise; - }; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [noise-texture]: <kbd>[texture.h]</kbd> Noise texture] -</div> + virtual void set_bounding_box() { + // Compute the bounding box of all four vertices. + auto bbox_diagonal1 = aabb(Q, Q + u + v); + auto bbox_diagonal2 = aabb(Q + u, Q + v); + bbox = aabb(bbox_diagonal1, bbox_diagonal2); + } -<div class='together'> -We can use that texture on some spheres: + aabb bounding_box() const override { return bbox; } - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - hittable_list two_perlin_spheres() { - hittable_list objects; + bool hit(const ray& r, interval ray_t, hit_record& rec) const override { + return false; // To be implemented + } - auto pertext = make_shared<noise_texture>(4); - objects.add(make_shared<sphere>(vec3(0,-1000, 0), 1000, make_shared<lambertian>(pertext))); - objects.add(make_shared<sphere>(vec3(0, 2, 0), 2, make_shared<lambertian>(pertext))); + private: + point3 Q; + vec3 u, v; + shared_ptr<material> mat; + aabb bbox; + }; - return objects; - } + #endif ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [scene-perlin]: <kbd>[main.cc]</kbd> Scene with two Perlin-textured spheres] + [Listing [quad]: <kbd>[quad.h]</kbd> 2D quadrilateral (parallelogram) class] + </div> -<div class='together'> -With the same camera as before: - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - const auto aspect_ratio = double(image_width) / image_height; - ... - vec3 lookfrom(13,2,3); - vec3 lookat(0,0,0); - vec3 vup(0,1,0); - auto dist_to_focus = 10.0; - auto aperture = 0.0; +Ray-Plane Intersection +----------------------- +As you can see in the prior listing, `quad::hit()` remains to be implemented. Just as for spheres, +we need to determine whether a given ray intersects the primitive, and if so, the various properties +of that intersection (hit point, normal, texture coordinates and so forth). - camera cam(lookfrom, lookat, vup, 20, aspect_ratio, aperture, dist_to_focus, 0.0, 1.0); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [scene-perlin-view]: <kbd>[main.cc]</kbd> Viewing parameters] -</div> +Ray-quad intersection will be determined in three steps: -<div class='together'> -Add the hashing does scramble as hoped: + 1. finding the plane that contains that quad, + 2. solving for the intersection of a ray and the quad-containing plane, + 3. determining if the hit point lies inside the quad. - <div class="render"> +We'll first tackle the middle step, solving for general ray-plane intersection. - ![Hashed random texture](../images/img.hash-random.jpg) +Spheres are generally the first ray tracing primitive taught because their implicit formula makes it +so easy to solve for ray intersection. Like spheres, planes also have an implicit formula, and we +can use their implicit formula to produce an algorithm that solves for ray-plane intersection. +Indeed, ray-plane intersection is even _easier_ to solve than ray-sphere intersection. - </div> +You may already know this implicit formula for a plane: -</div> + $$ Ax + By + Cz + D = 0 $$ -<div class='together'> -To make it smooth, we can linearly interpolate: +where $A,B,C,D$ are just constants, and $x,y,z$ are the values of any point $(x,y,z)$ that lies on +the plane. A plane is thus the set of all points $(x,y,z)$ that satisfy the formula above. It makes +things slightly easier to use the alternate formulation: - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - inline double trilinear_interp(double c[2][2][2], double u, double v, double w) { - auto accum = 0.0; - for (int i=0; i < 2; i++) - for (int j=0; j < 2; j++) - for (int k=0; k < 2; k++) - accum += (i*u + (1-i)*(1-u))* - (j*v + (1-j)*(1-v))* - (k*w + (1-k)*(1-w))*c[i][j][k]; + $$ Ax + By + Cz = D $$ - return accum; - } +(We didn't flip the sign of D because it's just some constant that we'll figure out later.) - class perlin { - public: - ... - double noise(const vec3& p) const { - auto u = p.x() - floor(p.x()); - auto v = p.y() - floor(p.y()); - auto w = p.z() - floor(p.z()); - int i = floor(p.x()); - int j = floor(p.y()); - int k = floor(p.z()); - double c[2][2][2]; - - for (int di=0; di < 2; di++) - for (int dj=0; dj < 2; dj++) - for (int dk=0; dk < 2; dk++) - c[di][dj][dk] = ranfloat[ - perm_x[(i+di) & 255] ^ - perm_y[(j+dj) & 255] ^ - perm_z[(k+dk) & 255] - ]; - - return trilinear_interp(c, u, v, w); - } - ... - } - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [perlin-trilinear]: <kbd>[perlin.h]</kbd> Perlin with trilienear interpolation] -</div> +Here's an intuitive way to think of this formula: given the plane perpendicular to the normal vector +$\mathbf{n} = (A,B,C)$, and the position vector $\mathbf{v} = (x,y,z)$ (that is, the vector from the +origin to any point on the plane), then we can use the dot product to solve for $D$: -<div class='together'> -And we get: + $$ \mathbf{n} \cdot \mathbf{v} = D $$ - <div class="render"> +for any position on the plane. This is an equivalent formulation of the $Ax + By + Cz = D$ formula +given above, only now in terms of vectors. - ![Perlin texture with trilinear interpolation](../images/img.perlin-trilerp.jpg) +Now to find the intersection with some ray $\mathbf{R}(t) = \mathbf{P} + t\mathbf{d}$. Plugging in +the ray equation, we get - </div> + $$ \mathbf{n} \cdot ( \mathbf{P} + t \mathbf{d} ) = D $$ -</div> +Solving for $t$: -<div class='together'> -Better, but there are obvious grid features in there. Some of it is Mach bands, a known perceptual -artifact of linear interpolation of color. A standard trick is to use a Hermite cubic to round off -the interpolation: + $$ \mathbf{n} \cdot \mathbf{P} + \mathbf{n} \cdot t \mathbf{d} = D $$ - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - class perlin ( - public: - ... - double noise(const vec3& p) const { - auto u = p.x() - floor(p.x()); - auto v = p.y() - floor(p.y()); - auto w = p.z() - floor(p.z()); + $$ \mathbf{n} \cdot \mathbf{P} + t(\mathbf{n} \cdot \mathbf{d}) = D $$ - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - u = u*u*(3-2*u); - v = v*v*(3-2*v); - w = w*w*(3-2*w); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + $$ t = \frac{D - \mathbf{n} \cdot \mathbf{P}}{\mathbf{n} \cdot \mathbf{d}} $$ - int i = floor(p.x()); - int j = floor(p.y()); - int k = floor(p.z()); - ... - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [perlin-smoothed]: <kbd>[perlin.h]</kbd> Perlin smoothed] -</div> +This gives us $t$, which we can plug into the ray equation to find the point of intersection. Note +that the denominator $\mathbf{n} \cdot \mathbf{d}$ will be zero if the ray is parallel to the plane. +In this case, we can immediately record a miss between the ray and the plane. As for other +primitives, if the ray $t$ parameter is less than the minimum acceptable value, we also record a +miss. -<div class='together'> -This gives a smoother looking image: +All right, we can find the point of intersection between a ray and the plane that contains a given +quadrilateral. In fact, we can use this approach to test _any_ planar primitive, like triangles and +disks (more on that later). - <div class="render"> - ![Perlin texture, trilinearly interpolated, smoothed](../images/img.perlin-tlerp-smooth.jpg) +Finding the Plane That Contains a Given Quadrilateral +------------------------------------------------------ +We've solved step two above: solving the ray-plane intersection, assuming we have the plane +equation. To do this, we need to tackle step one above: finding the equation for the plane that +contains the quad. We have quadrilateral parameters $\mathbf{Q}$, $\mathbf{u}$, and $\mathbf{v}$, +and want the corresponding equation of the plane containing the quad defined by these three values. - </div> +Fortunately, this is very simple. Recall that in the equation $Ax + By + Cz = D$, $(A,B,C)$ +represents the normal vector. To get this, we just use the cross product of the two side vectors +$\mathbf{u}$ and $\mathbf{v}$: -</div> + $$ \mathbf{n} = \operatorname{unit\_vector}(\mathbf{u} \times \mathbf{v}) $$ + +The plane is defined as all points $(x,y,z)$ that satisfy the equation $Ax + By + Cz = D$. Well, we +know that $\mathbf{Q}$ lies on the plane, so that's enough to solve for $D$: + + $$ \begin{align*} + D &= n_x Q_x + n_y Q_y + n_z Q_z \\ + &= \mathbf{n} \cdot \mathbf{Q} \\ + \end{align*} + $$ <div class='together'> -It is also a bit low frequency. We can scale the input point to make it vary more quickly: +Add the planar values to the `quad` class: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - class noise_texture : public texture { - public: - noise_texture() {} - noise_texture(double sc) : scale(sc) {} - - virtual vec3 value(double u, double v, const vec3& p) const { + class quad : public hittable { + public: + quad(const point3& Q, const vec3& u, const vec3& v, shared_ptr<material> mat) + : Q(Q), u(u), v(v), mat(mat) + { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - return vec3(1,1,1) * noise.noise(scale * p); + auto n = cross(u, v); + normal = unit_vector(n); + D = dot(normal, Q); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - } - public: - perlin noise; - double scale; + set_bounding_box(); + } + ... + + private: + point3 Q; + vec3 u, v; + shared_ptr<material> mat; + aabb bbox; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + vec3 normal; + double D; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [perlin-smoothed-2]: <kbd>[texture.h]</kbd> Perlin smoothed, higher frequency] + [Listing [quad-plane1]: <kbd>[quad.h]</kbd> Caching the planar values] -which gives: +</div> - <div class="render"> +We will use the two values `normal` and `D` to find the point of intersection between a given ray +and the plane containing the quadrilateral. - ![Perlin texture, higher frequency](../images/img.perlin-hifreq.jpg) +As an incremental step, let's implement the `hit()` method to handle the infinite plane containing +our quadrilateral. - </div> + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class quad : public hittable { + ... -</div> + bool hit(const ray& r, interval ray_t, hit_record& rec) const override { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + auto denom = dot(normal, r.direction()); -<div class='together'> -This is still a bit grid blocky looking, probably because the min and max of the pattern always -lands exactly on the integer x/y/z. Ken Perlin’s very clever trick was to instead put random unit -vectors (instead of just floats) on the lattice points, and use a dot product to move the min and -max off the lattice. So, first we need to change the random floats to random vectors. These vectors -are any reasonable set of irregular directions, and I won't bother to make them exactly uniform: + // No hit if the ray is parallel to the plane. + if (std::fabs(denom) < 1e-8) + return false; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - class perlin { - public: - perlin() { - ranvec = new vec3[point_count]; + // Return false if the hit point parameter t is outside the ray interval. + auto t = (D - dot(normal, r.origin())) / denom; + if (!ray_t.contains(t)) + return false; - for (int i = 0; i < point_count; ++i) { - ranvec[i] = unit_vector(vec3::random(-1,1)); - } + auto intersection = r.at(t); - perm_x = perlin_generate_perm(); - perm_y = perlin_generate_perm(); - perm_z = perlin_generate_perm(); - } + rec.t = t; + rec.p = intersection; + rec.mat = mat; + rec.set_face_normal(r, normal); + + return true; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } - ~perlin() { - delete[] ranvec; - delete[] perm_x; - delete[] perm_y; - delete[] perm_z; - } ... - private: - vec3* ranvec; - int* perm_x; - int* perm_y; - int* perm_z; - ... } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [perlin-randunit]: <kbd>[perlin.h]</kbd> Perlin with random unit translations] -</div> + [Listing [quad-plane2]: <kbd>[quad.h]</kbd> hit() method for the infinite plane] -<div class='together'> -The Perlin class `noise()` method is now: - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - class perlin { - public: - ... - double noise(const vec3& p) const { - auto u = p.x() - floor(p.x()); - auto v = p.y() - floor(p.y()); - auto w = p.z() - floor(p.z()); - int i = floor(p.x()); - int j = floor(p.y()); - int k = floor(p.z()); - vec3 c[2][2][2]; - - for (int di=0; di < 2; di++) - for (int dj=0; dj < 2; dj++) - for (int dk=0; dk < 2; dk++) - c[di][dj][dk] = ranvec[ - perm_x[(i+di) & 255] ^ - perm_y[(j+dj) & 255] ^ - pexm_z[(k+dk) & 255] - ]; - - return perlin_interp(c, u, v, w); - } - ... - } - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [perlin-2]: <kbd>[perlin.h]</kbd> Perlin class with new noise() method] -</div> +Orienting Points on The Plane +------------------------------ +At this stage, the intersection point is on the plane that contains the quadrilateral, but it could +be _anywhere_ on the plane: the ray-plane intersection point will lie inside or outside the +quadrilateral. We need to test for intersection points that lie inside the quadrilateral (hit), and +reject points that lie outside (miss). To determine where a point lies relative to the quad, and to +assign texture coordinates to the point of intersection, we need to orient the intersection point on +the plane. -<div class='together'> -And the interpolation becomes a bit more complicated: +To do this, we'll construct a _coordinate frame_ for the plane -- a way of orienting any point +located on the plane. We've already been using a coordinate frame for our 3D space -- this is +defined by an origin point $\mathbf{O}$ and three basis vectors $\mathbf{x}$, $\mathbf{y}$, and +$\mathbf{z}$. - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - class perlin { - ... - private: - ... - inline double perlin_interp(vec3 c[2][2][2], double u, double v, double w) { - auto uu = u*u*(3-2*u); - auto vv = v*v*(3-2*v); - auto ww = w*w*(3-2*w); - auto accum = 0.0; - - for (int i=0; i < 2; i++) - for (int j=0; j < 2; j++) - for (int k=0; k < 2; k++) { - vec3 weight_v(u-i, v-j, w-k); - accum += (i*uu + (1-i)*(1-uu)) - * (j*vv + (1-j)*(1-vv)) - * (k*ww + (1-k)*(1-ww)) - * dot(c[i][j][k], weight_v); - } +Since a plane is a 2D construct, we just need a plane origin point $\mathbf{Q}$ and _two_ basis +vectors: $\mathbf{u}$ and $\mathbf{v}$. Normally, axes are perpendicular to each other. However, +this doesn't need to be the case in order to span the entire space -- you just need two axes that +are not parallel to each other. - return accum; - } - ... - } - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [perlin-interp]: <kbd>[perlin.h]</kbd> Perlin interpolation function so far] -</div> + ![Figure [ray-plane]: Ray-plane intersection](../images/fig-2.06-ray-plane.jpg) -<div class='together'> -The output of the perlin interpretation can return negative values. These negative values will be -passed to the `sqrt()` function of our gamma function and get turned into `NaN`s. We will cast the -perlin output back to between 0 and 1. +Consider figure [ray-plane] as an example. Ray $\mathbf{R}$ intersects the plane, yielding +intersection point $\mathbf{P}$ (not to be confused with the ray origin point $\mathbf{P}$ above). +Measuring against plane vectors $\mathbf{u}$ and $\mathbf{v}$, the intersection point $\mathbf{P}$ +in the example above is at $\mathbf{Q} + (1)\mathbf{u} + (\frac{1}{2})\mathbf{v}$. In other words, +the $\mathbf{UV}$ (plane) coordinates of intersection point $\mathbf{P}$ are $(1,\frac{1}{2})$. - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - class noise_texture : public texture { - public: - noise_texture() {} - noise_texture(double sc) : scale(sc) {} +Generally, given some arbitrary point $\mathbf{P}$, we seek two scalar values $\alpha$ and $\beta$, +so that + + $$ \mathbf{P} = \mathbf{Q} + \alpha \mathbf{u} + \beta \mathbf{v} $$ - virtual vec3 value(double u, double v, const vec3& p) const { +Pulling a rabbit out of my hat, the planar coordinates $\alpha$ and $\beta$ are given by the +following equations: + + $$ \alpha = \mathbf{w} \cdot (\mathbf{p} \times \mathbf{v}) $$ + $$ \beta = \mathbf{w} \cdot (\mathbf{u} \times \mathbf{p}) $$ + +where + + $$ \mathbf{p} = \mathbf{P} - \mathbf{Q} $$ + $$ \mathbf{w} = \frac{\mathbf{n}}{\mathbf{n} \cdot (\mathbf{u} \times \mathbf{v})} + = \frac{\mathbf{n}}{\mathbf{n} \cdot \mathbf{n}}$$ + +The vector $\mathbf{w}$ is constant for a given quadrilateral, so we'll cache that value. + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class quad : public hittable { + public: + quad(const point3& Q, const vec3& u, const vec3& v, shared_ptr<material> mat) + : Q(Q), u(u), v(v), mat(mat) + { + auto n = cross(u, v); + normal = unit_vector(n); + D = dot(normal, Q); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - return vec3(1,1,1) * 0.5 * (1.0 + noise.noise(scale * p)); + w = n / dot(n,n); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - } - public: - perlin noise; - double scale; + set_bounding_box(); + } + + ... + + private: + point3 Q; + vec3 u, v; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + vec3 w; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + shared_ptr<material> mat; + aabb bbox; + vec3 normal; + double D; }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [perlin-smoothed-2]: <kbd>[perlin.h]</kbd> Perlin smoothed, higher frequency] -</div> + [Listing [quad-w]: <kbd>[quad.h]</kbd> Caching the quadrilateral's w value] -<div class='together'> -This finally gives something more reasonable looking: - <div class="render"> +Deriving the Planar Coordinates +-------------------------------- - ![Perlin texture, shifted off integer values](../images/img.perlin-shift.jpg) +(This section covers the derivation of the equations above. Feel free to skip to the next section if +you're not interested.) - </div> +Refer back to figure [ray-plane]. If the planar basis vectors $\mathbf{u}$ and $\mathbf{v}$ were +guaranteed to be orthogonal to each other (forming a 90° angle between them), then solving for +$\alpha$ and $\beta$ would be a simple matter of using the dot product to project $\mathbf{P}$ onto +each of the basis vectors $\mathbf{u}$ and $\mathbf{v}$. However, since we are not restricting +$\mathbf{u}$ and $\mathbf{v}$ to be orthogonal, the math's a little bit trickier. -</div> +To set things up, consider that -<div class='together'> -Very often, a composite noise that has multiple summed frequencies is used. This is usually called -turbulence, and is a sum of repeated calls to noise: + $$ \mathbf{P} = \mathbf{Q} + \alpha \mathbf{u} + \beta \mathbf{v}$$ - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - class perlin { - ... - public: - ... - double turb(const vec3& p, int depth=7) const { - auto accum = 0.0; - vec3 temp_p = p; - auto weight = 1.0; - - for (int i = 0; i < depth; i++) { - accum += weight*noise(temp_p); - weight *= 0.5; - temp_p *= 2; - } + $$ \mathbf{p} = \mathbf{P} - \mathbf{Q} = \alpha \mathbf{u} + \beta \mathbf{v} $$ - return fabs(accum); - } - ... - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [perlin-turb]: <kbd>[perlin.h]</kbd> Turbulence function] -</div> +Here, $\mathbf{P}$ is the _point_ of intersection, and $\mathbf{p}$ is the _vector_ from +$\mathbf{Q}$ to $\mathbf{P}$. -Here `fabs()` is the absolute value function defined in `<cmath>`. +Cross the equation for $\mathbf{p}$ with $\mathbf{u}$ and $\mathbf{v}$, respectively: -<div class='together'> -Used directly, turbulence gives a sort of camouflage netting appearance: + $$ \begin{align*} + \mathbf{u} \times \mathbf{p} &= \mathbf{u} \times (\alpha \mathbf{u} + \beta \mathbf{v}) \\ + &= \mathbf{u} \times \alpha \mathbf{u} + \mathbf{u} \times \beta \mathbf{v} \\ + &= \alpha(\mathbf{u} \times \mathbf{u}) + \beta(\mathbf{u} \times \mathbf{v}) + \end{align*} $$ - <div class="render"> + $$ \begin{align*} + \mathbf{v} \times \mathbf{p} &= \mathbf{v} \times (\alpha \mathbf{u} + \beta \mathbf{v}) \\ + &= \mathbf{v} \times \alpha \mathbf{u} + \mathbf{v} \times \beta \mathbf{v} \\ + &= \alpha(\mathbf{v} \times \mathbf{u}) + \beta(\mathbf{v} \times \mathbf{v}) + \end{align*} $$ - ![Perlin texture with turbulence](../images/img.perlin-turb.jpg) +Since any vector crossed with itself yields zero, these equations simplify to - </div> + $$ \mathbf{v} \times \mathbf{p} = \alpha(\mathbf{v} \times \mathbf{u}) $$ + $$ \mathbf{u} \times \mathbf{p} = \beta(\mathbf{u} \times \mathbf{v}) $$ -</div> +Now to solve for the coefficients $\alpha$ and $\beta$. If you're new to vector math, you might try +to divide by $\mathbf{u} \times \mathbf{v}$ and $\mathbf{v} \times \mathbf{u}$, but you can't divide +by vectors. Instead, we can take the dot product of both sides of the above equations with the plane +normal $\mathbf{n} = \mathbf{u} \times \mathbf{v}$, reducing both sides to scalars, which we _can_ +divide by. -<div class='together'> -However, usually turbulence is used indirectly. For example, the “hello world” of procedural solid -textures is a simple marble-like texture. The basic idea is to make color proportional to something -like a sine function, and use turbulence to adjust the phase (so it shifts $x$ in $\sin(x)$) which -makes the stripes undulate. Commenting out straight noise and turbulence, and giving a marble-like -effect is: + $$ \mathbf{n} \cdot (\mathbf{v} \times \mathbf{p}) + = \mathbf{n} \cdot \alpha(\mathbf{v} \times \mathbf{u}) $$ - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - class noise_texture : public texture { - public: - noise_texture() {} - noise_texture(double sc) : scale(sc) {} + $$ \mathbf{n} \cdot (\mathbf{u} \times \mathbf{p}) + = \mathbf{n} \cdot \beta(\mathbf{u} \times \mathbf{v}) $$ - virtual vec3 value(double u, double v, const vec3& p) const { - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - return vec3(1,1,1) * 0.5 * (1 + sin(scale*p.z() + 10*noise.turb(p))); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - } +Now isolating the coefficients is a simple matter of division: - public: - perlin noise; - double scale; - }; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [noise-tex-2]: <kbd>[texture.h]</kbd> Noise texture with turbulence] + $$ \alpha = \frac{\mathbf{n} \cdot (\mathbf{v} \times \mathbf{p})} + {\mathbf{n} \cdot (\mathbf{v} \times \mathbf{u})} $$ -Which yields: + $$ \beta = \frac{\mathbf{n} \cdot (\mathbf{u} \times \mathbf{p})} + {\mathbf{n} \cdot (\mathbf{u} \times \mathbf{v})} $$ - <div class="render"> +Reversing the cross products for both the numerator and denominator of $\alpha$ (recall that +$\mathbf{a} \times \mathbf{b} = - \mathbf{b} \times \mathbf{a}$) gives us a common denominator for +both coefficients: - ![Perlin noise, marbled texture](../images/img.perlin-marble.jpg) + $$ \alpha = \frac{\mathbf{n} \cdot (\mathbf{p} \times \mathbf{v})} + {\mathbf{n} \cdot (\mathbf{u} \times \mathbf{v})} $$ - </div> + $$ \beta = \frac{\mathbf{n} \cdot (\mathbf{u} \times \mathbf{p})} + {\mathbf{n} \cdot (\mathbf{u} \times \mathbf{v})} $$ -</div> +Now we can perform one final simplification, computing a vector $\mathbf{w}$ that will be constant +for the plane's basis frame, for any planar point $\mathbf{P}$: + $$ \mathbf{w} = \frac{\mathbf{n}}{\mathbf{n} \cdot (\mathbf{u} \times \mathbf{v})} + = \frac{\mathbf{n}}{\mathbf{n} \cdot \mathbf{n}}$$ + $$ \alpha = \mathbf{w} \cdot (\mathbf{p} \times \mathbf{v}) $$ + $$ \beta = \mathbf{w} \cdot (\mathbf{u} \times \mathbf{p}) $$ -Image Texture Mapping -==================================================================================================== -We used the hitpoint p before to index a procedure solid texture like marble. We can also read in an -image and use a 2D $(u,v)$ texture coordinate to index into the image. +Interior Testing of The Intersection Using UV Coordinates +---------------------------------------------------------- +Now that we have the intersection point's planar coordinates $\alpha$ and $\beta$, we can easily use +these to determine if the intersection point is inside the quadrilateral -- that is, if the ray +actually hit the quadrilateral. <div class='together'> -A direct way to use scaled $(u,v)$ in an image is to round the $u$ and $v$ to integers, and use that -as $(i,j)$ pixels. This is awkward, because we don’t want to have to change the code when we change -image resolution. So instead, one of the the most universal unofficial standards in graphics is to -use texture coordinates instead of image pixel coordinates. These are just some form of fractional -position in the image. For example, for pixel $(i,j)$ in an $N_x$ by $N_y$ image, the image texture -position is: +The plane is divided into coordinate regions like so: + + ![Figure [quad-coords]: Quadrilateral coordinates](../images/fig-2.07-quad-coords.jpg) - $$ u = \frac{i}{N_x-1} $$ - $$ v = \frac{j}{N_y-1} $$ </div> -<div class='together'> -This is just a fractional position. For a hittable, we need to also return a $u$ and $v$ in the hit -record. For spheres, this is usually based on some form of longitude and latitude, _i.e._, spherical -coordinates. So if we have a $(\theta,\phi)$ in spherical coordinates we just need to scale $\theta$ -and $\phi$ to fractions. If $\theta$ is the angle down from the pole, and $\phi$ is the angle around -the axis through the poles, the normalization to $[0,1]$ would be: +Thus, to see if a point with planar coordinates $(\alpha,\beta)$ lies inside the quadrilateral, it +just needs to meet the following criteria: - $$ u = \frac{\phi}{2\pi} $$ - $$ v = \frac{\theta}{\pi} $$ -</div> + 1. $ 0 \leq \alpha \leq 1 $ + 2. $ 0 \leq \beta \leq 1 $ -<div class='together'> -To compute $\theta$ and $\phi$, for a given hitpoint, the formula for spherical coordinates of a -unit radius sphere on the origin is: +That's the last piece needed to implement quadrilateral primitives. - $$ x = \cos(\phi) \cos(\theta) $$ - $$ y = \sin(\phi) \cos(\theta) $$ - $$ z = \sin(\theta) $$ -</div> +In order to make such experimentation a bit easier, we'll factor out the $(\alpha,\beta)$ interior +test method from the hit method. -<div class='together'> -We need to invert that. Because of the lovely `<cmath>` function `atan2()` (which takes any number -proportional to sine and cosine and returns the angle), we can pass in $x$ and $y$ (the -$\cos(\theta)$ cancel): + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class quad : public hittable { + public: + ... - $$ \phi = \text{atan2}(y, x) $$ -</div> + bool hit(const ray& r, interval ray_t, hit_record& rec) const override { + auto denom = dot(normal, r.direction()); -<div class='together'> -The $atan2$ returns in the range $-\pi$ to $\pi$ so we need to take a little care there. -The $\theta$ is more straightforward: + // No hit if the ray is parallel to the plane. + if (std::fabs(denom) < 1e-8) + return false; + + // Return false if the hit point parameter t is outside the ray interval. + auto t = (D - dot(normal, r.origin())) / denom; + if (!ray_t.contains(t)) + return false; + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + // Determine if the hit point lies within the planar shape using its plane coordinates. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + auto intersection = r.at(t); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + vec3 planar_hitpt_vector = intersection - Q; + auto alpha = dot(w, cross(planar_hitpt_vector, v)); + auto beta = dot(w, cross(u, planar_hitpt_vector)); + + if (!is_interior(alpha, beta, rec)) + return false; + + // Ray hits the 2D shape; set the rest of the hit record and return true. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + rec.t = t; + rec.p = intersection; + rec.mat = mat; + rec.set_face_normal(r, normal); + + return true; + } - $$ \theta = \text{asin}(z) $$ -which returns numbers in the range $-\pi/2$ to $\pi/2$. -</div> + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + virtual bool is_interior(double a, double b, hit_record& rec) const { + interval unit_interval = interval(0, 1); + // Given the hit point in plane coordinates, return false if it is outside the + // primitive, otherwise set the hit record UV coordinates and return true. -<div class='together'> -So for a sphere, the $(u,v)$ coord computation is accomplished by a utility function that expects -things on the unit sphere centered at the origin. The call inside sphere::hit should be: + if (!unit_interval.contains(a) || !unit_interval.contains(b)) + return false; + rec.u = a; + rec.v = b; + return true; + } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - get_sphere_uv((rec.p-center)/radius, rec.u, rec.v); + + private: + point3 Q; + vec3 u, v; + vec3 w; + shared_ptr<material> mat; + aabb bbox; + vec3 normal; + double D; + }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [get-sphere-uv-call]: <kbd>[sphere.h]</kbd> Sphere UV coordinates from hit] -</div> + [Listing [quad-final]: <kbd>[quad.h]</kbd> Final quad class] <div class='together'> -The utility function is: +And now we add a new scene to demonstrate our new `quad` primitive: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - void get_sphere_uv(const vec3& p, double& u, double& v) { - auto phi = atan2(p.z(), p.x()); - auto theta = asin(p.y()); - u = 1-(phi + pi) / (2*pi); - v = (theta + pi/2) / pi; - } - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [get-sphere-uv]: <kbd>[sphere.h]</kbd> get_sphere_uv function] -</div> - -Now we also need to create a texture class that holds an image. I am going to use my favorite image -utility `stb_image`. It reads in an image into a big array of unsigned char. These are just packed -RGBs that each range 0..255 for black to fully-on. + #include "rtweekend.h" + #include "bvh.h" + #include "camera.h" + #include "hittable.h" + #include "hittable_list.h" + #include "material.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + #include "quad.h" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include "sphere.h" #include "texture.h" - class image_texture : public texture { - public: - image_texture() {} - image_texture(unsigned char *pixels, int A, int B) - : data(pixels), nx(A), ny(B) {} + ... - ~image_texture() { - delete data; - } - virtual vec3 value(double u, double v, const vec3& p) const { - // If we have no texture data, then always emit cyan (as a debugging aid). - if (data == nullptr) - return vec3(0,1,1); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + void quads() { + hittable_list world; - auto i = static_cast<int>(( u)*nx); - auto j = static_cast<int>((1-v)*ny-0.001); + // Materials + auto left_red = make_shared<lambertian>(color(1.0, 0.2, 0.2)); + auto back_green = make_shared<lambertian>(color(0.2, 1.0, 0.2)); + auto right_blue = make_shared<lambertian>(color(0.2, 0.2, 1.0)); + auto upper_orange = make_shared<lambertian>(color(1.0, 0.5, 0.0)); + auto lower_teal = make_shared<lambertian>(color(0.2, 0.8, 0.8)); - if (i < 0) i = 0; - if (j < 0) j = 0; - if (i > nx-1) i = nx-1; - if (j > ny-1) j = ny-1; + // Quads + world.add(make_shared<quad>(point3(-3,-2, 5), vec3(0, 0,-4), vec3(0, 4, 0), left_red)); + world.add(make_shared<quad>(point3(-2,-2, 0), vec3(4, 0, 0), vec3(0, 4, 0), back_green)); + world.add(make_shared<quad>(point3( 3,-2, 1), vec3(0, 0, 4), vec3(0, 4, 0), right_blue)); + world.add(make_shared<quad>(point3(-2, 3, 1), vec3(4, 0, 0), vec3(0, 0, 4), upper_orange)); + world.add(make_shared<quad>(point3(-2,-3, 5), vec3(4, 0, 0), vec3(0, 0,-4), lower_teal)); - auto r = static_cast<int>(data[3*i + 3*nx*j+0]) / 255.0; - auto g = static_cast<int>(data[3*i + 3*nx*j+1]) / 255.0; - auto b = static_cast<int>(data[3*i + 3*nx*j+2]) / 255.0; + camera cam; - return vec3(r, g, b); - } + cam.aspect_ratio = 1.0; + cam.image_width = 400; + cam.samples_per_pixel = 100; + cam.max_depth = 50; - public: - unsigned char *data; - int nx, ny; - }; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [img-texture]: <kbd>[surface_texture.h]</kbd> Image texture class] + cam.vfov = 80; + cam.lookfrom = point3(0,0,9); + cam.lookat = point3(0,0,0); + cam.vup = vec3(0,1,0); -<div class='together'> -The representation of a packed array in that order is pretty standard. Thankfully, the `stb_image` -package makes that super simple -- just include the header `rtw_stb_image.h` in `main.h`: + cam.defocus_angle = 0; + cam.render(world); + } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - #include "rtw_stb_image.h" - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [incl-stb-img]: Including the STB image package] -</div> - -<div class='together'> - - <div class="render"> - - ![earthmap.jpg](../images/earthmap.jpg) - - </div> - -To read an image from a file earthmap.jpg (I just grabbed a random earth map from the web -- any -standard projection will do for our purposes), and then assign it to a diffuse material, the code -is: + int main() { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + switch (5) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - hittable_list earth() { - int nx, ny, nn; - unsigned char* texture_data = stbi_load("earthmap.jpg", &nx, &ny, &nn, 0); - - auto earth_surface = - make_shared<lambertian>(make_shared<image_texture>(texture_data, nx, ny)); - auto globe = make_shared<sphere>(vec3(0,0,0), 2, earth_surface); - - return hittable_list(globe); + case 1: bouncing_spheres(); break; + case 2: checkered_spheres(); break; + case 3: earth(); break; + case 4: perlin_spheres(); break; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + case 5: quads(); break; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [stbi-load-use]: <kbd>[main.cc]</kbd> Using stbi_load() to load an image] -</div> + [Listing [quad-scene]: <kbd>[main.cc]</kbd> A new scene with quads] -We start to see some of the power of all colors being textures -- we can assign any kind of texture -to the lambertian material, and lambertian doesn’t need to be aware of it. +</div> -<div class='together'> -To test this, assign it to a sphere, and then temporarily cripple the `ray_color()` function in main -to just return attenuation. You should get something like: + ![<span class='num'>Image 16:</span> Quads](../images/img-2.16-quads.png class='pixel') - <div class="render"> - ![Earth-mapped sphere](../images/img.earth-sphere.jpg) +Additional 2D Primitives +------------------------- +Pause a bit here and consider that if you use the $(\alpha,\beta)$ coordinates to determine if a +point lies inside a quadrilateral (parallelogram), it's not too hard to imagine using these same 2D +coordinates to determine if the intersection point lies inside _any_ other 2D (planar) primitive! - </div> +For example, suppose we change the `is_interior()` function to return true if `sqrt(a*a + b*b) < r`. +This would then implement disk primitives of radius `r`. For triangles, try +`a > 0 && b > 0 && a + b < 1`. -</div> +We'll leave additional 2D shape possibilities as an exercise to the reader, depending on your desire +to explore. You could even create cut-out stencils based on the pixels of a texture map, or a +Mandelbrot shape! As a little Easter egg, check out the `alternate-2D-primitives` tag in the source +repository. This has solutions for triangles, ellipses and annuli (rings) in +`src/TheNextWeek/quad.h` -Rectangles and Lights +Lights ==================================================================================================== +Lighting is a key component of raytracing. Early simple raytracers used abstract light sources, like +points in space, or directions. Modern approaches have more physically based lights, which have +position and size. To create such light sources, we need to be able to take any regular object and +turn it into something that emits light into our scene. -<div class='together'> + +Emissive Materials +------------------- First, let’s make a light emitting material. We need to add an emitted function (we could also add -it to `hit_record instead` -- that’s a matter of design taste). Like the background, it just tells +it to `hit_record` instead -- that’s a matter of design taste). Like the background, it just tells the ray what color it is and performs no reflection. It’s very simple: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - class diffuse_light : public material { - public: - diffuse_light(shared_ptr<texture> a) : emit(a) {} + class dielectric : public material { + ... + } - virtual bool scatter( - const ray& r_in, const hit_record& rec, vec3& attenuation, ray& scattered - ) const { - return false; - } - virtual vec3 emitted(double u, double v, const vec3& p) const { - return emit->value(u, v, p); - } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + class diffuse_light : public material { + public: + diffuse_light(shared_ptr<texture> tex) : tex(tex) {} + diffuse_light(const color& emit) : tex(make_shared<solid_color>(emit)) {} + + color emitted(double u, double v, const point3& p) const override { + return tex->value(u, v, p); + } - public: - shared_ptr<texture> emit; + private: + shared_ptr<texture> tex; }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [diffuse-light]: <kbd>[material.h]</kbd> A diffuse light class] -</div> <div class='together'> So that I don’t have to make all the non-emitting materials implement `emitted()`, I have the base @@ -1791,698 +3178,741 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class material { - public: - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - virtual vec3 emitted(double u, double v, const vec3& p) const { - return vec3(0,0,0); - } + public: + virtual ~material() = default; + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + virtual color emitted(double u, double v, const point3& p) const { + return color(0,0,0); + } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - virtual bool scatter( - const ray& r_in, const hit_record& rec, vec3& attenuation, ray& scattered - ) const = 0; + + virtual bool scatter( + const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered + ) const { + return false; + } }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [matl-emit]: <kbd>[material.h]</kbd> New emitted function in class material] + </div> -<div class='together'> + +Adding Background Color to the Ray Color Function +-------------------------------------------------- Next, we want a pure black background so the only light in the scene is coming from the emitters. To do this, we’ll add a background color parameter to our `ray_color` function, and pay attention to -the new `emitted` value. +the new `color_from_emission` value. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - vec3 ray_color(const ray& r, const vec3& background, const hittable& world, int depth) { - hit_record rec; - - // If we've exceeded the ray bounce limit, no more light is gathered. - if (depth <= 0) - return vec3(0,0,0); - - // If the ray hits nothing, return the background color. - if (!world.hit(r, 0.001, infinity, rec)) - return background; - - ray scattered; - vec3 attenuation; - vec3 emitted = rec.mat_ptr->emitted(rec.u, rec.v, rec.p); - if (!rec.mat_ptr->scatter(r, rec, attenuation, scattered)) - return emitted; - - return emitted + attenuation * ray_color(scattered, background, world, depth-1); - } - ... + class camera { + public: + double aspect_ratio = 1.0; // Ratio of image width over height + int image_width = 100; // Rendered image width in pixel count + int samples_per_pixel = 10; // Count of random samples for each pixel + int max_depth = 10; // Maximum number of ray bounces into scene + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + color background; // Scene background color + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - int main() { - ... - const vec3 background(0,0,0); ... - color += ray_color(r, background, world, max_depth); + + private: ... - } - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [ray-color-emitted]: <kbd>[main.cc]</kbd> ray_color function for emitting materials] -</div> + color ray_color(const ray& r, int depth, const hittable& world) const { + // If we've exceeded the ray bounce limit, no more light is gathered. + if (depth <= 0) + return color(0,0,0); -Now, let’s make some rectangles. Rectangles are often convenient for modeling man-made environments. -I’m a fan of doing axis-aligned rectangles because they are easy. (We’ll get to instancing so we can -rotate them later.) + hit_record rec; -<div class='together'> -First, here is a rectangle in an xy plane. Such a plane is defined by its z value. For example, $z = -k$. An axis-aligned rectangle is defined by the lines $x=x_0$, $x=x_1$, $y=y_0$, and $y=y_1$. - ![Figure [ray-rect]: Ray-rectangle intersection](../images/fig.ray-rect.jpg) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + // If the ray hits nothing, return the background color. + if (!world.hit(r, interval(0.001, infinity), rec)) + return background; -</div> + ray scattered; + color attenuation; + color color_from_emission = rec.mat->emitted(rec.u, rec.v, rec.p); -<div class='together'> -To determine whether a ray hits such a rectangle, we first determine where the ray hits the plane. -Recall that a ray $p(t) = \mathbf{a} + t \cdot \vec{\mathbf{b}}$ has its z component defined by -$z(t) = \mathbf{a}_z + t \cdot \vec{\mathbf{b}}_z$. Rearranging those terms we can solve for what -the t is where $z=k$. + if (!rec.mat->scatter(r, rec, attenuation, scattered)) + return color_from_emission; - $$ t = \frac{k-\mathbf{a}_z}{\vec{\mathbf{b}}_z} $$ -</div> + color color_from_scatter = attenuation * ray_color(scattered, depth-1, world); + + return color_from_emission + color_from_scatter; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } + }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [ray-color-emitted]: <kbd>[camera.h]</kbd> ray_color function with background and emitting materials] <div class='together'> -Once we have $t$, we can plug that into the equations for $x$ and $y$: +`main()` is updated to set the background color for the prior scenes: - $$ x = \mathbf{a}_x + t \cdot \vec{\mathbf{b}}_x $$ - $$ y = \mathbf{a}_y + t \cdot \vec{\mathbf{b}}_y $$ -</div> + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + void bouncing_spheres() { + ... + camera cam; -It is a hit if $x_0 < x < x_1$ and $y_0 < y < y_1$. + cam.aspect_ratio = 16.0 / 9.0; + cam.image_width = 400; + cam.samples_per_pixel = 100; + cam.max_depth = 20; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + cam.background = color(0.70, 0.80, 1.00); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + ... + } -<div class='together'> -The actual `xy_rect` class is thus: + void checkered_spheres() { + ... + camera cam; + cam.aspect_ratio = 16.0 / 9.0; + cam.image_width = 400; + cam.samples_per_pixel = 100; + cam.max_depth = 50; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + cam.background = color(0.70, 0.80, 1.00); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - class xy_rect: public hittable { - public: - xy_rect() {} + ... + } - xy_rect(double _x0, double _x1, double _y0, double _y1, double _k, material *mat) - : x0(_x0), x1(_x1), y0(_y0), y1(_y1), k(_k), mp(mat) {}; + void earth() { + ... + camera cam; - virtual bool hit(const ray& r, double t0, double t1, hit_record& rec) const; + cam.aspect_ratio = 16.0 / 9.0; + cam.image_width = 400; + cam.samples_per_pixel = 100; + cam.max_depth = 50; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + cam.background = color(0.70, 0.80, 1.00); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + ... + } - virtual bool bounding_box(double t0, double t1, aabb& output_box) const { - output_box = aabb(vec3(x0,y0, k-0.0001), vec3(x1, y1, k+0.0001)); - return true; - } + void perlin_spheres() { + ... + camera cam; - public: - shared_ptr<material> mp; - double x0, x1, y0, y1, k; - }; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [xy-rect]: <kbd>[aarect.h]</kbd> XY-plane rectangle objects] -</div> + cam.aspect_ratio = 16.0 / 9.0; + cam.image_width = 400; + cam.samples_per_pixel = 100; + cam.max_depth = 50; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + cam.background = color(0.70, 0.80, 1.00); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + ... + } -<div class='together'> -And the hit function is: + void quads() { + ... + camera cam; + cam.aspect_ratio = 1.0; + cam.image_width = 400; + cam.samples_per_pixel = 100; + cam.max_depth = 50; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + cam.background = color(0.70, 0.80, 1.00); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - bool xy_rect::hit(const ray& r, double t0, double t1, hit_record& rec) const { - auto t = (k-r.origin().z()) / r.direction().z(); - if (t < t0 || t > t1) - return false; - auto x = r.origin().x() + t*r.direction().x(); - auto y = r.origin().y() + t*r.direction().y(); - if (x < x0 || x > x1 || y < y0 || y > y1) - return false; - rec.u = (x-x0)/(x1-x0); - rec.v = (y-y0)/(y1-y0); - rec.t = t; - vec3 outward_normal = vec3(0, 0, 1); - rec.set_face_normal(r, outward_normal); - rec.mat_ptr = mp; - rec.p = r.at(t); - return true; + ... } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [xy-rect-hit]: <kbd>[aarect.h]</kbd> Hit function for XY rectangle objects] + [Listing [use-bg-color]: <kbd>[main.cc]</kbd> Specifying new background color] + </div> -<div class='together'> +Since we're removing the code that we used to determine the color of the sky when a ray hit it, we +need to pass in a new color value for our old scene renders. We've elected to stick with a flat +bluish-white for the whole sky. You could always pass in a boolean to switch between the previous +skybox code versus the new solid color background. We're keeping it simple here. + + +Turning Objects into Lights +---------------------------- If we set up a rectangle as a light: - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - hittable_list simple_light() { - hittable_list objects; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + void simple_light() { + hittable_list world; auto pertext = make_shared<noise_texture>(4); - objects.add(make_shared<sphere>(vec3(0,-1000, 0), 1000, make_shared<lambertian>(pertext))); - objects.add(make_shared<sphere>(vec3(0,2,0), 2, make_shared<lambertian>(pertext))); + world.add(make_shared<sphere>(point3(0,-1000,0), 1000, make_shared<lambertian>(pertext))); + world.add(make_shared<sphere>(point3(0,2,0), 2, make_shared<lambertian>(pertext))); + + auto difflight = make_shared<diffuse_light>(color(4,4,4)); + world.add(make_shared<quad>(point3(3,1,-2), vec3(2,0,0), vec3(0,2,0), difflight)); + + camera cam; + + cam.aspect_ratio = 16.0 / 9.0; + cam.image_width = 400; + cam.samples_per_pixel = 100; + cam.max_depth = 50; + cam.background = color(0,0,0); - auto difflight = make_shared<diffuse_light>(make_shared<constant_texture>(vec3(4,4,4))); - objects.add(make_shared<sphere>(vec3(0,7,0), 2, difflight)); - objects.add(make_shared<xy_rect>(3, 5, 1, 3, -2, difflight)); + cam.vfov = 20; + cam.lookfrom = point3(26,3,6); + cam.lookat = point3(0,2,0); + cam.vup = vec3(0,1,0); + + cam.defocus_angle = 0; + + cam.render(world); + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - return objects; + int main() { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + switch (6) { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + case 1: bouncing_spheres(); break; + case 2: checkered_spheres(); break; + case 3: earth(); break; + case 4: perlin_spheres(); break; + case 5: quads(); break; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + case 6: simple_light(); break; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [rect-light]: <kbd>[main.cc]</kbd> A simple rectangle light] -</div> <div class='together'> We get: - <div class="render"> - - ![Scene with rectangle light source](../images/img.rect-light.jpg) - - </div> + ![<span class='num'>Image 17:</span> Scene with rectangle light source + ](../images/img-2.17-rect-light.png class='pixel') </div> Note that the light is brighter than $(1,1,1)$. This allows it to be bright enough to light things. -<div class='together'> Fool around with making some spheres lights too. - <div class="render"> - - ![Scene with rectangle and sphere light sources](../images/img.rect-sph-light.jpg) - - </div> - -</div> - -Now let’s add the other two axes and the famous Cornell Box. - -<div class='together'> -This is xz and yz: - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - class xz_rect: public hittable { - public: - xz_rect() {} - - xz_rect(double _x0, double _x1, double _z0, double _z1, double _k, material *mat) - : x0(_x0), x1(_x1), z0(_z0), z1(_z1), k(_k), mp(mat) {}; + void simple_light() { + ... + auto difflight = make_shared<diffuse_light>(color(4,4,4)); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + world.add(make_shared<sphere>(point3(0,7,0), 2, difflight)); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + world.add(make_shared<quad>(point3(3,1,-2), vec3(2,0,0), vec3(0,2,0), difflight)); + ... + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [rect-light]: <kbd>[main.cc]</kbd> A simple rectangle light plus illuminating ball] - virtual bool hit(const ray& r, double t0, double t1, hit_record& rec) const; + ![<span class='num'>Image 18:</span> Scene with rectangle and sphere light sources + ](../images/img-2.18-rect-sphere-light.png class='pixel') - virtual bool bounding_box(double t0, double t1, aabb& output_box) const { - output_box = aabb(vec3(x0,k-0.0001,z0), vec3(x1, k+0.0001, z1)); - return true; - } - public: - shared_ptr<material> mp; - double x0, x1, z0, z1, k; - }; +Creating an Empty “Cornell Box” +-------------------------------- +The “Cornell Box” was introduced in 1984 to model the interaction of light between diffuse surfaces. +Let’s make the 5 walls and the light of the box: - class yz_rect: public hittable { - public: - yz_rect() {} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + void cornell_box() { + hittable_list world; - yz_rect(double _y0, double _y1, double _z0, double _z1, double _k, material *mat) - : y0(_y0), y1(_y1), z0(_z0), z1(_z1), k(_k), mp(mat) {}; + auto red = make_shared<lambertian>(color(.65, .05, .05)); + auto white = make_shared<lambertian>(color(.73, .73, .73)); + auto green = make_shared<lambertian>(color(.12, .45, .15)); + auto light = make_shared<diffuse_light>(color(15, 15, 15)); - virtual bool hit(const ray& r, double t0, double t1, hit_record& rec) const; + world.add(make_shared<quad>(point3(555,0,0), vec3(0,555,0), vec3(0,0,555), green)); + world.add(make_shared<quad>(point3(0,0,0), vec3(0,555,0), vec3(0,0,555), red)); + world.add(make_shared<quad>(point3(343, 554, 332), vec3(-130,0,0), vec3(0,0,-105), light)); + world.add(make_shared<quad>(point3(0,0,0), vec3(555,0,0), vec3(0,0,555), white)); + world.add(make_shared<quad>(point3(555,555,555), vec3(-555,0,0), vec3(0,0,-555), white)); + world.add(make_shared<quad>(point3(0,0,555), vec3(555,0,0), vec3(0,555,0), white)); - virtual bool bounding_box(double t0, double t1, aabb& output_box) const { - output_box = aabb(vec3(k-0.0001, y0, z0), vec3(k+0.0001, y1, z1)); - return true; - } + camera cam; - public: - shared_ptr<material> mp; - double y0, y1, z0, z1, k; - }; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [xz-yz-rects]: <kbd>[aarect.h]</kbd> XZ and YZ rectangle objects] -</div> + cam.aspect_ratio = 1.0; + cam.image_width = 600; + cam.samples_per_pixel = 200; + cam.max_depth = 50; + cam.background = color(0,0,0); -<div class='together'> -With unsurprising hit functions: + cam.vfov = 40; + cam.lookfrom = point3(278, 278, -800); + cam.lookat = point3(278, 278, 0); + cam.vup = vec3(0,1,0); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - bool xz_rect::hit(const ray& r, double t0, double t1, hit_record& rec) const { - auto t = (k-r.origin().y()) / r.direction().y(); - if (t < t0 || t > t1) - return false; - auto x = r.origin().x() + t*r.direction().x(); - auto z = r.origin().z() + t*r.direction().z(); - if (x < x0 || x > x1 || z < z0 || z > z1) - return false; - rec.u = (x-x0)/(x1-x0); - rec.v = (z-z0)/(z1-z0); - rec.t = t; - vec3 outward_normal = vec3(0, 1, 0); - rec.set_face_normal(r, outward_normal); - rec.mat_ptr = mp; - rec.p = r.at(t); - return true; - } + cam.defocus_angle = 0; - bool yz_rect::hit(const ray& r, double t0, double t1, hit_record& rec) const { - auto t = (k-r.origin().x()) / r.direction().x(); - if (t < t0 || t > t1) - return false; - auto y = r.origin().y() + t*r.direction().y(); - auto z = r.origin().z() + t*r.direction().z(); - if (y < y0 || y > y1 || z < z0 || z > z1) - return false; - rec.u = (y-y0)/(y1-y0); - rec.v = (z-z0)/(z1-z0); - rec.t = t; - vec3 outward_normal = vec3(1, 0, 0); - rec.set_face_normal(r, outward_normal); - rec.mat_ptr = mp; - rec.p = r.at(t); - return true; + cam.render(world); } - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [xz-yz]: <kbd>[aarect.h]</kbd> XZ and YZ rectangle object hit functions] -</div> - -<div class='together'> -Let’s make the 5 walls and the light of the box: - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - hittable_list cornell_box() { - hittable_list objects; - - auto red = make_shared<lambertian>(make_shared<constant_texture>(vec3(0.65, 0.05, 0.05))); - auto white = make_shared<lambertian>(make_shared<constant_texture>(vec3(0.73, 0.73, 0.73))); - auto green = make_shared<lambertian>(make_shared<constant_texture>(vec3(0.12, 0.45, 0.15))); - auto light = make_shared<diffuse_light>(make_shared<constant_texture>(vec3(15, 15, 15))); - objects.add(make_shared<yz_rect>(0, 555, 0, 555, 555, green)); - objects.add(make_shared<yz_rect>(0, 555, 0, 555, 0, red)); - objects.add(make_shared<xz_rect>(213, 343, 227, 332, 554, light)); - objects.add(make_shared<xz_rect>(0, 555, 0, 555, 0, white)); - objects.add(make_shared<xy_rect>(0, 555, 0, 555, 555, white)); - - return objects; + int main() { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + switch (7) { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + case 1: bouncing_spheres(); break; + case 2: checkered_spheres(); break; + case 3: earth(); break; + case 4: perlin_spheres(); break; + case 5: quads(); break; + case 6: simple_light(); break; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + case 7: cornell_box(); break; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [cornell-box-empty]: <kbd>[main.cc]</kbd> Cornell box scene, empty] -</div> <div class='together'> -And the view info: +We get: - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - const auto aspect_ratio = double(image_width) / image_height; - ... - vec3 lookfrom(278, 278, -800); - vec3 lookat(278,278,0); - vec3 vup(0,1,0); - auto dist_to_focus = 10.0; - auto aperture = 0.0; - auto vfov = 40.0; + ![<span class='num'>Image 19:</span> Empty Cornell box + ](../images/img-2.19-cornell-empty.png class='pixel') + +This image is very noisy because the light is small, so most random rays don't hit the light source. - camera cam(lookfrom, lookat, vup, vfov, aspect_ratio, aperture, dist_to_focus, 0.0, 1.0); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [cornell-box-view]: <kbd>[main.cc]</kbd> Viewing parameters] </div> -<div class='together'> -We get: - <div class="render"> - ![Empty Cornell box](../images/img.cornell-empty.jpg) +Instances +==================================================================================================== +The Cornell Box usually has two blocks in it. These are rotated relative to the walls. First, let’s +create a function that returns a box, by creating a `hittable_list` of six rectangles: - </div> + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include "hittable.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + #include "hittable_list.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ -</div> + class quad : public hittable { + ... + }; -<div class='together'> -This is very noisy because the light is small. But we have a problem: some of the walls are facing -the wrong way. We haven't specified that a diffuse material should behave differently on different -faces of the object, but what if the Cornell box had a different pattern on the inside and outside -walls? The rectangle objects are described such that their front faces are always in the -$(1, 0, 0)$, $(0, 1, 0)$, or $(0, 0, 1)$ directions. We need a way to switch the faces of an -object. Let’s make a hittable that does nothing but hold another hittable, and flips the face: - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - class flip_face : public hittable { - public: - flip_face(shared_ptr<hittable> p) : ptr(p) {} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + inline shared_ptr<hittable_list> box(const point3& a, const point3& b, shared_ptr<material> mat) + { + // Returns the 3D box (six sides) that contains the two opposite vertices a & b. - virtual bool hit(const ray& r, double t_min, double t_max, hit_record& rec) const { - if (!ptr->hit(r, t_min, t_max, rec)) - return false; + auto sides = make_shared<hittable_list>(); - rec.front_face = !rec.front_face; - return true; - } + // Construct the two opposite vertices with the minimum and maximum coordinates. + auto min = point3(std::fmin(a.x(),b.x()), std::fmin(a.y(),b.y()), std::fmin(a.z(),b.z())); + auto max = point3(std::fmax(a.x(),b.x()), std::fmax(a.y(),b.y()), std::fmax(a.z(),b.z())); - virtual bool bounding_box(double t0, double t1, aabb& output_box) const { - return ptr->bounding_box(t0, t1, output_box); - } + auto dx = vec3(max.x() - min.x(), 0, 0); + auto dy = vec3(0, max.y() - min.y(), 0); + auto dz = vec3(0, 0, max.z() - min.z()); - public: - shared_ptr<hittable> ptr; - }; + sides->add(make_shared<quad>(point3(min.x(), min.y(), max.z()), dx, dy, mat)); // front + sides->add(make_shared<quad>(point3(max.x(), min.y(), max.z()), -dz, dy, mat)); // right + sides->add(make_shared<quad>(point3(max.x(), min.y(), min.z()), -dx, dy, mat)); // back + sides->add(make_shared<quad>(point3(min.x(), min.y(), min.z()), dz, dy, mat)); // left + sides->add(make_shared<quad>(point3(min.x(), max.y(), max.z()), dx, -dz, mat)); // top + sides->add(make_shared<quad>(point3(min.x(), min.y(), min.z()), dx, dz, mat)); // bottom + + return sides; + } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [flip-face]: <kbd>[hittable.h]</kbd> Flip-Face function] -</div> + [Listing [box-class]: <kbd>[quad.h]</kbd> A box object] <div class='together'> -This makes Cornell: +Now we can add two blocks (but not rotated). ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - hittable_list cornell_box() { - hittable_list objects; - - auto red = make_shared<lambertian>(make_shared<constant_texture>(vec3(0.65, 0.05, 0.05))); - auto white = make_shared<lambertian>(make_shared<constant_texture>(vec3(0.73, 0.73, 0.73))); - auto green = make_shared<lambertian>(make_shared<constant_texture>(vec3(0.12, 0.45, 0.15))); - auto light = make_shared<diffuse_light>(make_shared<constant_texture>(vec3(15, 15, 15))); + void cornell_box() { + ... + world.add(make_shared<quad>(point3(555,0,0), vec3(0,555,0), vec3(0,0,555), green)); + world.add(make_shared<quad>(point3(0,0,0), vec3(0,555,0), vec3(0,0,555), red)); + world.add(make_shared<quad>(point3(343, 554, 332), vec3(-130,0,0), vec3(0,0,-105), light)); + world.add(make_shared<quad>(point3(0,0,0), vec3(555,0,0), vec3(0,0,555), white)); + world.add(make_shared<quad>(point3(555,555,555), vec3(-555,0,0), vec3(0,0,-555), white)); + world.add(make_shared<quad>(point3(0,0,555), vec3(555,0,0), vec3(0,555,0), white)); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - objects.add(make_shared<flip_face>(make_shared<yz_rect>(0, 555, 0, 555, 555, green))); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - objects.add(make_shared<yz_rect>(0, 555, 0, 555, 0, red)); - objects.add(make_shared<xz_rect>(213, 343, 227, 332, 554, light)); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - objects.add(make_shared<flip_face>(make_shared<xz_rect>(0, 555, 0, 555, 555, white))); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - objects.add(make_shared<xz_rect>(0, 555, 0, 555, 0, white)); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - objects.add(make_shared<flip_face>(make_shared<xy_rect>(0, 555, 0, 555, 555, white))); + world.add(box(point3(130, 0, 65), point3(295, 165, 230), white)); + world.add(box(point3(265, 0, 295), point3(430, 330, 460), white)); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - return objects; + camera cam; + ... } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [cornell-box-flipped]: <kbd>[main.cc]</kbd> Empty Cornell box with flipped rectangles] + [Listing [add-boxes]: <kbd>[main.cc]</kbd> Adding box objects] + </div> <div class='together'> -And voila: - - <div class="render"> - - ![Empty Cornell box with fixed walls](../images/img.cornell-empty.jpg) +This gives: - </div> + ![<span class='num'>Image 20:</span> Cornell box with two blocks + ](../images/img-2.20-cornell-blocks.png class='pixel') </div> +Now that we have boxes, we need to rotate them a bit to have them match the _real_ Cornell box. In +ray tracing, this is usually done with an _instance_. An instance is a copy of a geometric primitive +that has been placed into the scene. This instance is entirely independent of the other copies of +the primitive and can be moved or rotated. In this case, our geometric primitive is our hittable +`box` object, and we want to rotate it. This is especially easy in ray tracing because we don’t +actually need to move objects in the scene; instead we move the rays in the opposite direction. For +example, consider a _translation_ (often called a _move_). We could take the pink box at the origin +and add two to all its x components, or (as we almost always do in ray tracing) leave the box where +it is, but in its hit routine subtract two off the x-component of the ray origin. + ![Figure [ray-box]: Ray-box intersection with moved ray vs box](../images/fig-2.08-ray-box.jpg) -Instances -==================================================================================================== - -The Cornell Box usually has two blocks in it. These are rotated relative to the walls. First, let’s -make an axis-aligned block primitive that holds 6 rectangles: - - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - class box: public hittable { - public: - box() {} - box(const vec3& p0, const vec3& p1, shared_ptr<material> ptr); - virtual bool hit(const ray& r, double t0, double t1, hit_record& rec) const; +Instance Translation +--------------------- +Whether you think of this as a move or a change of coordinates is up to you. The way to reason about +this is to think of moving the incident ray backwards the offset amount, determining if an +intersection occurs, and then moving that intersection point forward the offset amount. - virtual bool bounding_box(double t0, double t1, aabb& output_box) const { - output_box = aabb(box_min, box_max); - return true; - } +We need to move the intersection point forward the offset amount so that the intersection is +actually in the path of the incident ray. If we forgot to move the intersection point forward then +the intersection would be in the path of the offset ray, which isn't correct. Let's add the code to +make this happen. - public: - vec3 box_min; - vec3 box_max; - hittable_list sides; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class hittable { + ... }; - box::box(const vec3& p0, const vec3& p1, shared_ptr<material> ptr) { - box_min = p0; - box_max = p1; - sides.add(make_shared<xy_rect>(p0.x(), p1.x(), p0.y(), p1.y(), p1.z(), ptr)); - sides.add(make_shared<flip_face>( - make_shared<xy_rect>(p0.x(), p1.x(), p0.y(), p1.y(), p0.z(), ptr))); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + class translate : public hittable { + public: + bool hit(const ray& r, interval ray_t, hit_record& rec) const override { + // Move the ray backwards by the offset + ray offset_r(r.origin() - offset, r.direction(), r.time()); + + // Determine whether an intersection exists along the offset ray (and if so, where) + if (!object->hit(offset_r, ray_t, rec)) + return false; - sides.add(make_shared<xz_rect>(p0.x(), p1.x(), p0.z(), p1.z(), p1.y(), ptr)); - sides.add(make_shared<flip_face>( - make_shared<xz_rect>(p0.x(), p1.x(), p0.z(), p1.z(), p0.y(), ptr))); + // Move the intersection point forwards by the offset + rec.p += offset; - sides.add(make_shared<yz_rect>(p0.y(), p1.y(), p0.z(), p1.z(), p1.x(), ptr)); - sides.add(make_shared<flip_face>( - make_shared<yz_rect>(p0.y(), p1.y(), p0.z(), p1.z(), p0.x(), ptr))); - } + return true; + } - bool box::hit(const ray& r, double t0, double t1, hit_record& rec) const { - return sides.hit(r, t0, t1, rec); - } + private: + shared_ptr<hittable> object; + vec3 offset; + }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [box-class]: <kbd>[box.h]</kbd> A box object] + [Listing [translate-hit]: <kbd>[hittable.h]</kbd> Hittable translation hit function] <div class='together'> -Now we can add two blocks (but not rotated) +... and then flesh out the rest of the `translate` class: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - objects.add(make_shared<box>(vec3(130, 0, 65), vec3(295, 165, 230), white)); - objects.add(make_shared<box>(vec3(265, 0, 295), vec3(430, 330, 460), white)); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [add-boxes]: <kbd>[main.cc]</kbd> Adding box objects] - -</div> + class translate : public hittable { + public: + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + translate(shared_ptr<hittable> object, const vec3& offset) + : object(object), offset(offset) + { + bbox = object->bounding_box() + offset; + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ -<div class='together'> -This gives: + bool hit(const ray& r, interval ray_t, hit_record& rec) const override { + ... + } - <div class="render"> - ![Cornell box with two blocks](../images/img.cornell-blocks.jpg) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + aabb bounding_box() const override { return bbox; } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - </div> + private: + shared_ptr<hittable> object; + vec3 offset; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + aabb bbox; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [translate-class]: <kbd>[hittable.h]</kbd> Hittable translation class] </div> -<div class='together'> -Now that we have boxes, we need to rotate them a bit to have them match the _real_ Cornell box. In -ray tracing, this is usually done with an _instance_. An instance is a geometric primitive that has -been moved or rotated somehow. This is especially easy in ray tracing because we don’t move -anything; instead we move the rays in the opposite direction. For example, consider a _translation_ -(often called a _move_). We could take the pink box at the origin and add 2 to all its x components, -or (as we almost always do in ray tracing) leave the box where it is, but in its hit routine -subtract 2 off the x-component of the ray origin. +We also need to remember to offset the bounding box, otherwise the incident ray might be looking in +the wrong place and trivially reject the intersection. The expression `object->bounding_box() + +offset` above requires some additional support. - ![Figure [ray-box]: Ray-box intersection with moved ray vs box](../images/fig.ray-box.jpg) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class aabb { + ... + }; -</div> + const aabb aabb::empty = aabb(interval::empty, interval::empty, interval::empty); + const aabb aabb::universe = aabb(interval::universe, interval::universe, interval::universe); -<div class='together'> -Whether you think of this as a move or a change of coordinates is up to you. The code for this, to -move any underlying hittable is a _translate_ instance. - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - class translate : public hittable { - public: - translate(shared_ptr<hittable> p, const vec3& displacement) - : ptr(p), offset(displacement) {} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + aabb operator+(const aabb& bbox, const vec3& offset) { + return aabb(bbox.x + offset.x(), bbox.y + offset.y(), bbox.z + offset.z()); + } + + aabb operator+(const vec3& offset, const aabb& bbox) { + return bbox + offset; + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [aabb-plus-offset]: <kbd>[aabb.h]</kbd> The aabb + offset operator] - virtual bool hit(const ray& r, double t_min, double t_max, hit_record& rec) const; - virtual bool bounding_box(double t0, double t1, aabb& output_box) const; +Since each dimension of an `aabb` is represented as an interval, we'll need to extend `interval` +with an addition operator as well. - public: - shared_ptr<hittable> ptr; - vec3 offset; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class interval { + ... }; - bool translate::hit(const ray& r, double t_min, double t_max, hit_record& rec) const { - ray moved_r(r.origin() - offset, r.direction(), r.time()); - if (!ptr->hit(moved_r, t_min, t_max, rec)) - return false; + const interval interval::empty = interval(+infinity, -infinity); + const interval interval::universe = interval(-infinity, +infinity); - rec.p += offset; - rec.set_face_normal(moved_r, rec.normal); - return true; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + interval operator+(const interval& ival, double displacement) { + return interval(ival.min + displacement, ival.max + displacement); } - bool translate::bounding_box(double t0, double t1, aabb& output_box) const { - if (!ptr->bounding_box(t0, t1, output_box)) - return false; - - output_box = aabb( - output_box.min() + offset, - output_box.max() + offset); - - return true; + interval operator+(double displacement, const interval& ival) { + return ival + displacement; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [translate-class]: <kbd>[hittable.h]</kbd> Hittable translation class] -</div> + [Listing [interval-plus-displacement]: <kbd>[interval.h]</kbd> The interval + displacement operator] -<div class='together'> + +Instance Rotation +------------------ Rotation isn’t quite as easy to understand or generate the formulas for. A common graphics tactic is to apply all rotations about the x, y, and z axes. These rotations are in some sense axis-aligned. First, let’s rotate by theta about the z-axis. That will be changing only x and y, and in ways that don’t depend on z. - ![Figure [rotz]: Rotation about the Z axis](../images/fig.rotz.jpg) - -</div> + ![Figure [rot-z]: Rotation about the Z axis](../images/fig-2.09-rot-z.jpg) -<div class='together'> -This involves some basic trigonometry that uses formulas that I will not cover here. That gives you -the correct impression it’s a little involved, but it is straightforward, and you can find it in any -graphics text and in many lecture notes. The result for rotating counter-clockwise about z is: +This involves some basic trigonometry using formulas that I will not cover here. It’s a little +involved, but it is straightforward, and you can find it in any graphics text and in many lecture +notes. The result for rotating counter-clockwise about z is: $$ x' = \cos(\theta) \cdot x - \sin(\theta) \cdot y $$ $$ y' = \sin(\theta) \cdot x + \cos(\theta) \cdot y $$ -</div> The great thing is that it works for any $\theta$ and doesn’t need any cases for quadrants or anything like that. The inverse transform is the opposite geometric operation: rotate by $-\theta$. Here, recall that $\cos(\theta) = \cos(-\theta)$ and $\sin(-\theta) = -\sin(\theta)$, so the formulas are very simple. -<div class='together'> Similarly, for rotating about y (as we want to do for the blocks in the box) the formulas are: $$ x' = \cos(\theta) \cdot x + \sin(\theta) \cdot z $$ $$ z' = -\sin(\theta) \cdot x + \cos(\theta) \cdot z $$ -And about the x-axis: +And if we want to rotate about the x-axis: $$ y' = \cos(\theta) \cdot y - \sin(\theta) \cdot z $$ $$ z' = \sin(\theta) \cdot y + \cos(\theta) \cdot z $$ -</div> -Unlike the situation with translations, the surface normal vector also changes, so we need to -transform directions too if we get a hit. Fortunately for rotations, the same formulas apply. If you -add scales, things get more complicated. See the web page https://in1weekend.blogspot.com/ for links -to that. +Thinking of translation as a simple movement of the initial ray is a fine way to reason about what's +going on. But, for a more complex operation like a rotation, it can be easy to accidentally get your +terms crossed (or forget a negative sign), so it's better to consider a rotation as a change of +coordinates. + +The pseudocode for the `translate::hit` function above describes the function in terms of _moving_: + + 1. Move the ray backwards by the offset + 2. Determine whether an intersection exists along the offset ray (and if so, where) + 3. Move the intersection point forwards by the offset + +But this can also be thought of in terms of a _changing of coordinates_: + + 1. Change the ray from world space to object space + 2. Determine whether an intersection exists in object space (and if so, where) + 3. Change the intersection point from object space to world space + +Rotating an object will not only change the point of intersection, but will also change the surface +normal vector, which will change the direction of reflections and refractions. So we need to change +the normal as well. Fortunately, the normal will rotate similarly to a vector, so we can use the +same formulas as above. While normals and vectors may appear identical for an object undergoing +rotation and translation, an object undergoing scaling requires special attention to keep the +normals orthogonal to the surface. We won't cover that here, but you should research surface normal +transformations if you implement scaling. + +We need to start by changing the ray from world space to object space, which for rotation means +rotating by $-\theta$. + + $$ x' = \cos(\theta) \cdot x - \sin(\theta) \cdot z $$ + $$ z' = \sin(\theta) \cdot x + \cos(\theta) \cdot z $$ <div class='together'> -For a y-rotation class we have: +We can now create a class for y-rotation. Let's tackle the hit function first: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class translate : public hittable { + ... + }; + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight class rotate_y : public hittable { - public: - rotate_y(shared_ptr<hittable> p, double angle); + public: - virtual bool hit(const ray& r, double t_min, double t_max, hit_record& rec) const; - virtual bool bounding_box(double t0, double t1, aabb& output_box) const { - output_box = bbox; - return hasbox; - } + bool hit(const ray& r, interval ray_t, hit_record& rec) const override { - public: - shared_ptr<hittable> ptr; - double sin_theta; - double cos_theta; - bool hasbox; - aabb bbox; - }; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [rot-y]: <kbd>[hittable.h]</kbd> Hittable rotate-Y class] -</div> + // Transform the ray from world space to object space. -<div class='together'> -With constructor: + auto origin = point3( + (cos_theta * r.origin().x()) - (sin_theta * r.origin().z()), + r.origin().y(), + (sin_theta * r.origin().x()) + (cos_theta * r.origin().z()) + ); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - rotate_y::rotate_y(hittable *p, double angle) : ptr(p) { - auto radians = degrees_to_radians(angle); - sin_theta = sin(radians); - cos_theta = cos(radians); - hasbox = ptr->bounding_box(0, 1, bbox); + auto direction = vec3( + (cos_theta * r.direction().x()) - (sin_theta * r.direction().z()), + r.direction().y(), + (sin_theta * r.direction().x()) + (cos_theta * r.direction().z()) + ); - vec3 min( infinity, infinity, infinity); - vec3 max(-infinity, -infinity, -infinity); + ray rotated_r(origin, direction, r.time()); - for (int i = 0; i < 2; i++) { - for (int j = 0; j < 2; j++) { - for (int k = 0; k < 2; k++) { - auto x = i*bbox.max().x() + (1-i)*bbox.min().x(); - auto y = j*bbox.max().y() + (1-j)*bbox.min().y(); - auto z = k*bbox.max().z() + (1-k)*bbox.min().z(); + // Determine whether an intersection exists in object space (and if so, where). - auto newx = cos_theta*x + sin_theta*z; - auto newz = -sin_theta*x + cos_theta*z; + if (!object->hit(rotated_r, ray_t, rec)) + return false; - vec3 tester(newx, y, newz); + // Transform the intersection from object space back to world space. - for (int c = 0; c < 3; c++) { - min[c] = ffmin(min[c], tester[c]); - max[c] = ffmax(max[c], tester[c]); - } - } - } - } + rec.p = point3( + (cos_theta * rec.p.x()) + (sin_theta * rec.p.z()), + rec.p.y(), + (-sin_theta * rec.p.x()) + (cos_theta * rec.p.z()) + ); - bbox = aabb(min, max); - } + rec.normal = vec3( + (cos_theta * rec.normal.x()) + (sin_theta * rec.normal.z()), + rec.normal.y(), + (-sin_theta * rec.normal.x()) + (cos_theta * rec.normal.z()) + ); + + return true; + } + }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [rot-y-rot]: <kbd>[hittable.h]</kbd> Rotate-Y rotate method] + [Listing [rot-y-hit]: <kbd>[hittable.h]</kbd> Hittable rotate-Y hit function] + </div> <div class='together'> -And the hit function: +... and now for the rest of the class: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - bool rotate_y::hit(const ray& r, double t_min, double t_max, hit_record& rec) const { - vec3 origin = r.origin(); - vec3 direction = r.direction(); - - origin[0] = cos_theta*r.origin()[0] - sin_theta*r.origin()[2]; - origin[2] = sin_theta*r.origin()[0] + cos_theta*r.origin()[2]; - - direction[0] = cos_theta*r.direction()[0] - sin_theta*r.direction()[2]; - direction[2] = sin_theta*r.direction()[0] + cos_theta*r.direction()[2]; - - ray rotated_r(origin, direction, r.time()); - - if (!ptr->hit(rotated_r, t_min, t_max, rec)) - return false; + class rotate_y : public hittable { + public: + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + rotate_y(shared_ptr<hittable> object, double angle) : object(object) { + auto radians = degrees_to_radians(angle); + sin_theta = std::sin(radians); + cos_theta = std::cos(radians); + bbox = object->bounding_box(); + + point3 min( infinity, infinity, infinity); + point3 max(-infinity, -infinity, -infinity); + + for (int i = 0; i < 2; i++) { + for (int j = 0; j < 2; j++) { + for (int k = 0; k < 2; k++) { + auto x = i*bbox.x.max + (1-i)*bbox.x.min; + auto y = j*bbox.y.max + (1-j)*bbox.y.min; + auto z = k*bbox.z.max + (1-k)*bbox.z.min; + + auto newx = cos_theta*x + sin_theta*z; + auto newz = -sin_theta*x + cos_theta*z; + + vec3 tester(newx, y, newz); + + for (int c = 0; c < 3; c++) { + min[c] = std::fmin(min[c], tester[c]); + max[c] = std::fmax(max[c], tester[c]); + } + } + } + } - vec3 p = rec.p; - vec3 normal = rec.normal; + bbox = aabb(min, max); + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - p[0] = cos_theta*rec.p[0] + sin_theta*rec.p[2]; - p[2] = -sin_theta*rec.p[0] + cos_theta*rec.p[2]; + bool hit(const ray& r, interval ray_t, hit_record& rec) const override { + ... + } - normal[0] = cos_theta*rec.normal[0] + sin_theta*rec.normal[2]; - normal[2] = -sin_theta*rec.normal[0] + cos_theta*rec.normal[2]; - rec.p = p; - rec.set_face_normal(rotated_r, normal); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + aabb bounding_box() const override { return bbox; } - return true; - } + private: + shared_ptr<hittable> object; + double sin_theta; + double cos_theta; + aabb bbox; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [rot-y-hit]: <kbd>[hittable.h]</kbd> Hittable Y-rotate hit function] + [Listing [rot-y]: <kbd>[hittable.h]</kbd> Hittable rotate-Y class] + </div> <div class='together'> And the changes to Cornell are: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - shared_ptr<hittable> box1 = make_shared<box>(vec3(0, 0, 0), vec3(165, 330, 165), white); - box1 = make_shared<rotate_y>(box1, 15); - box1 = make_shared<translate>(box1, vec3(265,0,295)); - objects.add(box1); + void cornell_box() { + ... + world.add(make_shared<quad>(point3(0,0,555), vec3(555,0,0), vec3(0,555,0), white)); + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + shared_ptr<hittable> box1 = box(point3(0,0,0), point3(165,330,165), white); + box1 = make_shared<rotate_y>(box1, 15); + box1 = make_shared<translate>(box1, vec3(265,0,295)); + world.add(box1); + + shared_ptr<hittable> box2 = box(point3(0,0,0), point3(165,165,165), white); + box2 = make_shared<rotate_y>(box2, -18); + box2 = make_shared<translate>(box2, vec3(130,0,65)); + world.add(box2); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - shared_ptr<hittable> box2 = make_shared<box>(vec3(0,0,0), vec3(165,165,165), white); - box2 = make_shared<rotate_y>(box2, -18); - box2 = make_shared<translate>(box2, vec3(130,0,65)); - objects.add(box2); + camera cam; + ... + } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [scene-rot-y]: <kbd>[main.cc]</kbd> Cornell scene with Y-rotated boxes] + </div> <div class='together'> Which yields: - <div class="render"> - - ![Standard Cornell box scene](../images/img.cornell-box.jpg) - - </div> + ![<span class='num'>Image 21:</span> Standard Cornell box scene + ](../images/img-2.21-cornell-standard.png class='pixel') </div> @@ -2490,7 +3920,6 @@ Volumes ==================================================================================================== - One thing it’s nice to add to a ray tracer is smoke/fog/mist. These are sometimes called _volumes_ or _participating media_. Another feature that is nice to add is subsurface scattering, which is sort of like dense fog inside an object. This usually adds software architectural mayhem because @@ -2498,126 +3927,127 @@ surface. A bunch of smoke can be replaced with a surface that probabilistically might or might not be there at every point in the volume. This will make more sense when you see the code. + +Constant Density Mediums +------------------------- First, let’s start with a volume of constant density. A ray going through there can either scatter inside the volume, or it can make it all the way through like the middle ray in the figure. More thin transparent volumes, like a light fog, are more likely to have rays like the middle one. How far the ray has to travel through the volume also determines how likely it is for the ray to make it through. - ![Figure [ray-vol]: Ray-volume interaction](../images/fig.ray-vol.jpg) + ![Figure [ray-vol]: Ray-volume interaction](../images/fig-2.10-ray-vol.jpg) -<div class='together'> As the ray passes through the volume, it may scatter at any point. The denser the volume, the more likely that is. The probability that the ray scatters in any small distance $\Delta L$ is: - $$ \text{probability} = C \cdot \Delta L $$ -</div> + $$ \mathit{probability} = C \cdot \Delta L $$ -<div class='together'> where $C$ is proportional to the optical density of the volume. If you go through all the differential equations, for a random number you get a distance where the scattering occurs. If that distance is outside the volume, then there is no “hit”. For a constant volume we just need the -density $C$ and the boundary. I’ll use another hittable for the boundary. The resulting class is: +density $C$ and the boundary. I’ll use another hittable for the boundary. + +<div class='together'> +The resulting class is: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #ifndef CONSTANT_MEDIUM_H + #define CONSTANT_MEDIUM_H + + #include "hittable.h" + #include "material.h" + #include "texture.h" + class constant_medium : public hittable { - public: - constant_medium(shared_ptr<hittable> b, double d, shared_ptr<texture> a) - : boundary(b), neg_inv_density(-1/d) - { - phase_function = make_shared<isotropic>(a); - } + public: + constant_medium(shared_ptr<hittable> boundary, double density, shared_ptr<texture> tex) + : boundary(boundary), neg_inv_density(-1/density), + phase_function(make_shared<isotropic>(tex)) + {} - virtual bool hit(const ray& r, double t_min, double t_max, hit_record& rec) const; + constant_medium(shared_ptr<hittable> boundary, double density, const color& albedo) + : boundary(boundary), neg_inv_density(-1/density), + phase_function(make_shared<isotropic>(albedo)) + {} - virtual bool bounding_box(double t0, double t1, aabb& output_box) const { - return boundary->bounding_box(t0, t1, output_box); - } + bool hit(const ray& r, interval ray_t, hit_record& rec) const override { + hit_record rec1, rec2; - public: - shared_ptr<hittable> boundary; - shared_ptr<material> phase_function; - double neg_inv_density; - }; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [const-med-class]: <kbd>[constant_medium.h]</kbd> Constant medium class] -</div> + if (!boundary->hit(r, interval::universe, rec1)) + return false; -<div class='together'> -The scattering function of isotropic picks a uniform random direction: + if (!boundary->hit(r, interval(rec1.t+0.0001, infinity), rec2)) + return false; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - class isotropic : public material { - public: - isotropic(shared_ptr<texture> a) : albedo(a) {} - - virtual bool scatter( - const ray& r_in, const hit_record& rec, vec3& attenuation, ray& scattered - ) const { - scattered = ray(rec.p, random_in_unit_sphere(), r_in.time()); - attenuation = albedo->value(rec.u, rec.v, rec.p); - return true; - } + if (rec1.t < ray_t.min) rec1.t = ray_t.min; + if (rec2.t > ray_t.max) rec2.t = ray_t.max; - public: - shared_ptr<texture> albedo; - }; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [isotropic-class]: <kbd>[material.h]</kbd> The isotropic class] -</div> + if (rec1.t >= rec2.t) + return false; -<div class='together'> -And the hit function is: + if (rec1.t < 0) + rec1.t = 0; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - bool constant_medium::hit(const ray& r, double t_min, double t_max, hit_record& rec) const { - // Print occasional samples when debugging. To enable, set enableDebug true. - const bool enableDebug = false; - const bool debugging = enableDebug && random_double() < 0.00001; + auto ray_length = r.direction().length(); + auto distance_inside_boundary = (rec2.t - rec1.t) * ray_length; + auto hit_distance = neg_inv_density * std::log(random_double()); - hit_record rec1, rec2; + if (hit_distance > distance_inside_boundary) + return false; - if (!boundary->hit(r, -infinity, infinity, rec1)) - return false; + rec.t = rec1.t + hit_distance / ray_length; + rec.p = r.at(rec.t); - if (!boundary->hit(r, rec1.t+0.0001, infinity, rec2)) - return false; + rec.normal = vec3(1,0,0); // arbitrary + rec.front_face = true; // also arbitrary + rec.mat = phase_function; - if (debugging) std::cerr << "\nt0=" << rec1.t << ", t1=" << rec2.t << '\n'; + return true; + } - if (rec1.t < t_min) rec1.t = t_min; - if (rec2.t > t_max) rec2.t = t_max; + aabb bounding_box() const override { return boundary->bounding_box(); } - if (rec1.t >= rec2.t) - return false; + private: + shared_ptr<hittable> boundary; + double neg_inv_density; + shared_ptr<material> phase_function; + }; + + #endif + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [const-med-class]: <kbd>[constant_medium.h]</kbd> Constant medium class] - if (rec1.t < 0) - rec1.t = 0; +</div> - const auto ray_length = r.direction().length(); - const auto distance_inside_boundary = (rec2.t - rec1.t) * ray_length; - const auto hit_distance = neg_inv_density * log(random_double()); +<div class='together'> +The scattering function of isotropic picks a uniform random direction: - if (hit_distance > distance_inside_boundary) - return false; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class diffuse_light : public material { + ... + }; - rec.t = rec1.t + hit_distance / ray_length; - rec.p = r.at(rec.t); - if (debugging) { - std::cerr << "hit_distance = " << hit_distance << '\n' - << "rec.t = " << rec.t << '\n' - << "rec.p = " << rec.p << '\n'; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + class isotropic : public material { + public: + isotropic(const color& albedo) : tex(make_shared<solid_color>(albedo)) {} + isotropic(shared_ptr<texture> tex) : tex(tex) {} + + bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) + const override { + scattered = ray(rec.p, random_unit_vector(), r_in.time()); + attenuation = tex->value(rec.u, rec.v, rec.p); + return true; } - rec.normal = vec3(1,0,0); // arbitrary - rec.front_face = true; // also arbitrary - rec.mat_ptr = phase_function; - - return true; - } + private: + shared_ptr<texture> tex; + }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [const-med-hit]: <kbd>[constant_medium.h]</kbd> Constant medium hit method] + [Listing [isotropic-class]: <kbd>[material.h]</kbd> The isotropic class] + </div> The reason we have to be so careful about the logic around the boundary is we need to make sure this @@ -2630,53 +4060,98 @@ not work with toruses or shapes that contain voids. It's possible to write an implementation that handles arbitrary shapes, but we'll leave that as an exercise for the reader. -<div class='together'> + +Rendering a Cornell Box with Smoke and Fog Boxes +------------------------------------------------- If we replace the two blocks with smoke and fog (dark and light particles), and make the light bigger (and dimmer so it doesn’t blow out the scene) for faster convergence: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - hittable_list cornell_smoke() { - hittable_list objects; + #include "bvh.h" + #include "camera.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + #include "constant_medium.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include "hittable.h" + #include "hittable_list.h" + #include "material.h" + #include "quad.h" + #include "sphere.h" + #include "texture.h" + + ... + - auto red = make_shared<lambertian>(make_shared<constant_texture>(vec3(0.65, 0.05, 0.05))); - auto white = make_shared<lambertian>(make_shared<constant_texture>(vec3(0.73, 0.73, 0.73))); - auto green = make_shared<lambertian>(make_shared<constant_texture>(vec3(0.12, 0.45, 0.15))); - auto light = make_shared<diffuse_light>(make_shared<constant_texture>(vec3(7, 7, 7))); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + void cornell_smoke() { + hittable_list world; - objects.add(make_shared<flip_face>(make_shared<yz_rect>(0, 555, 0, 555, 555, green))); - objects.add(make_shared<yz_rect>(0, 555, 0, 555, 0, red)); - objects.add(make_shared<xz_rect>(113, 443, 127, 432, 554, light)); - objects.add(make_shared<flip_face>(make_shared<xz_rect>(0, 555, 0, 555, 555, white))); - objects.add(make_shared<xz_rect>(0, 555, 0, 555, 0, white)); - objects.add(make_shared<flip_face>(make_shared<xy_rect>(0, 555, 0, 555, 555, white))); + auto red = make_shared<lambertian>(color(.65, .05, .05)); + auto white = make_shared<lambertian>(color(.73, .73, .73)); + auto green = make_shared<lambertian>(color(.12, .45, .15)); + auto light = make_shared<diffuse_light>(color(7, 7, 7)); - shared_ptr<hittable> box1 = make_shared<box>(vec3(0,0,0), vec3(165,330,165), white); - box1 = make_shared<rotate_y>(box1, 15); + world.add(make_shared<quad>(point3(555,0,0), vec3(0,555,0), vec3(0,0,555), green)); + world.add(make_shared<quad>(point3(0,0,0), vec3(0,555,0), vec3(0,0,555), red)); + world.add(make_shared<quad>(point3(113,554,127), vec3(330,0,0), vec3(0,0,305), light)); + world.add(make_shared<quad>(point3(0,555,0), vec3(555,0,0), vec3(0,0,555), white)); + world.add(make_shared<quad>(point3(0,0,0), vec3(555,0,0), vec3(0,0,555), white)); + world.add(make_shared<quad>(point3(0,0,555), vec3(555,0,0), vec3(0,555,0), white)); + + shared_ptr<hittable> box1 = box(point3(0,0,0), point3(165,330,165), white); + box1 = make_shared<rotate_y>(box1, 15); box1 = make_shared<translate>(box1, vec3(265,0,295)); - shared_ptr<hittable> box2 = make_shared<box>(vec3(0,0,0), vec3(165,165,165), white); + shared_ptr<hittable> box2 = box(point3(0,0,0), point3(165,165,165), white); box2 = make_shared<rotate_y>(box2, -18); box2 = make_shared<translate>(box2, vec3(130,0,65)); - objects.add( - make_shared<constant_medium>(box1, 0.01, make_shared<constant_texture>(vec3(0,0,0)))); - objects.add( - make_shared<constant_medium>(box2, 0.01, make_shared<constant_texture>(vec3(1,1,1)))); + world.add(make_shared<constant_medium>(box1, 0.01, color(0,0,0))); + world.add(make_shared<constant_medium>(box2, 0.01, color(1,1,1))); + + camera cam; + + cam.aspect_ratio = 1.0; + cam.image_width = 600; + cam.samples_per_pixel = 200; + cam.max_depth = 50; + cam.background = color(0,0,0); + + cam.vfov = 40; + cam.lookfrom = point3(278, 278, -800); + cam.lookat = point3(278, 278, 0); + cam.vup = vec3(0,1,0); + + cam.defocus_angle = 0; + + cam.render(world); + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - return objects; + int main() { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + switch (8) { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + case 1: bouncing_spheres(); break; + case 2: checkered_spheres(); break; + case 3: earth(); break; + case 4: perlin_spheres(); break; + case 5: quads(); break; + case 6: simple_light(); break; + case 7: cornell_box(); break; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + case 8: cornell_smoke(); break; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [cornell-smoke]: <kbd>[main.cc]</kbd> Cornell box, with smoke] -</div> <div class='together'> We get: - <div class="render"> - - ![Cornell box with blocks of smoke](../images/img.cornell-smoke.jpg) - - </div> + ![<span class='num'>Image 22:</span> Cornell box with blocks of smoke + ](../images/img-2.22-cornell-smoke.png class='pixel') </div> @@ -2684,19 +4159,20 @@ A Scene Testing All New Features ==================================================================================================== - Let’s put it all together, with a big thin mist covering everything, and a blue subsurface reflection sphere (we didn’t implement that explicitly, but a volume inside a dielectric is what a subsurface material is). The biggest limitation left in the renderer is no shadow rays, but that is why we get caustics and subsurface for free. It’s a double-edged design decision. - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - hittable_list final_scene() { +Also note that we'll parameterize this final scene to support a lower quality render for quick +testing. + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + void final_scene(int image_width, int samples_per_pixel, int max_depth) { hittable_list boxes1; - auto ground = - make_shared<lambertian>(make_shared<constant_texture>(vec3(0.48, 0.83, 0.53))); + auto ground = make_shared<lambertian>(color(0.48, 0.83, 0.53)); - const int boxes_per_side = 20; + int boxes_per_side = 20; for (int i = 0; i < boxes_per_side; i++) { for (int j = 0; j < boxes_per_side; j++) { auto w = 100.0; @@ -2707,87 +4183,196 @@ auto y1 = random_double(1,101); auto z1 = z0 + w; - boxes1.add(make_shared<box>(vec3(x0,y0,z0), vec3(x1,y1,z1), ground)); + boxes1.add(box(point3(x0,y0,z0), point3(x1,y1,z1), ground)); } } - hittable_list objects; + hittable_list world; - objects.add(make_shared<bvh_node>(boxes1, 0, 1)); + world.add(make_shared<bvh_node>(boxes1)); - auto light = make_shared<diffuse_light>(make_shared<constant_texture>(vec3(7, 7, 7))); - objects.add(make_shared<xz_rect>(123, 423, 147, 412, 554, light)); + auto light = make_shared<diffuse_light>(color(7, 7, 7)); + world.add(make_shared<quad>(point3(123,554,147), vec3(300,0,0), vec3(0,0,265), light)); - auto center1 = vec3(400, 400, 200); + auto center1 = point3(400, 400, 200); auto center2 = center1 + vec3(30,0,0); - auto moving_sphere_material = - make_shared<lambertian>(make_shared<constant_texture>(vec3(0.7, 0.3, 0.1))); - objects.add(make_shared<moving_sphere>(center1, center2, 0, 1, 50, moving_sphere_material)); + auto sphere_material = make_shared<lambertian>(color(0.7, 0.3, 0.1)); + world.add(make_shared<sphere>(center1, center2, 50, sphere_material)); - objects.add(make_shared<sphere>(vec3(260, 150, 45), 50, make_shared<dielectric>(1.5))); - objects.add(make_shared<sphere>( - vec3(0, 150, 145), 50, make_shared<metal>(vec3(0.8, 0.8, 0.9), 10.0) + world.add(make_shared<sphere>(point3(260, 150, 45), 50, make_shared<dielectric>(1.5))); + world.add(make_shared<sphere>( + point3(0, 150, 145), 50, make_shared<metal>(color(0.8, 0.8, 0.9), 1.0) )); - auto boundary = make_shared<sphere>(vec3(360, 150, 145), 70, make_shared<dielectric>(1.5)); - objects.add(boundary); - objects.add(make_shared<constant_medium>( - boundary, 0.2, make_shared<constant_texture>(vec3(0.2, 0.4, 0.9)) - )); - boundary = make_shared<sphere>(vec3(0, 0, 0), 5000, make_shared<dielectric>(1.5)); - objects.add(make_shared<constant_medium>( - boundary, .0001, make_shared<constant_texture>(vec3(1,1,1)))); + auto boundary = make_shared<sphere>(point3(360,150,145), 70, make_shared<dielectric>(1.5)); + world.add(boundary); + world.add(make_shared<constant_medium>(boundary, 0.2, color(0.2, 0.4, 0.9))); + boundary = make_shared<sphere>(point3(0,0,0), 5000, make_shared<dielectric>(1.5)); + world.add(make_shared<constant_medium>(boundary, .0001, color(1,1,1))); - int nx, ny, nn; - auto tex_data = stbi_load("earthmap.jpg", &nx, &ny, &nn, 0); - auto emat = make_shared<lambertian>(make_shared<image_texture>(tex_data, nx, ny)); - objects.add(make_shared<sphere>(vec3(400,200, 400), 100, emat)); - auto pertext = make_shared<noise_texture>(0.1); - objects.add(make_shared<sphere>(vec3(220,280, 300), 80, make_shared<lambertian>(pertext))); + auto emat = make_shared<lambertian>(make_shared<image_texture>("earthmap.jpg")); + world.add(make_shared<sphere>(point3(400,200,400), 100, emat)); + auto pertext = make_shared<noise_texture>(0.2); + world.add(make_shared<sphere>(point3(220,280,300), 80, make_shared<lambertian>(pertext))); hittable_list boxes2; - auto white = make_shared<lambertian>(make_shared<constant_texture>(vec3(0.73, 0.73, 0.73))); + auto white = make_shared<lambertian>(color(.73, .73, .73)); int ns = 1000; for (int j = 0; j < ns; j++) { - boxes2.add(make_shared<sphere>(vec3::random(0,165), 10, white)); + boxes2.add(make_shared<sphere>(point3::random(0,165), 10, white)); } - objects.add(make_shared<translate>( + world.add(make_shared<translate>( make_shared<rotate_y>( - make_shared<bvh_node>(boxes2, 0.0, 1.0), 15), + make_shared<bvh_node>(boxes2), 15), vec3(-100,270,395) ) ); - return objects; + camera cam; + + cam.aspect_ratio = 1.0; + cam.image_width = image_width; + cam.samples_per_pixel = samples_per_pixel; + cam.max_depth = max_depth; + cam.background = color(0,0,0); + + cam.vfov = 40; + cam.lookfrom = point3(478, 278, -600); + cam.lookat = point3(278, 278, 0); + cam.vup = vec3(0,1,0); + + cam.defocus_angle = 0; + + cam.render(world); + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + int main() { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + switch (9) { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + case 1: bouncing_spheres(); break; + case 2: checkered_spheres(); break; + case 3: earth(); break; + case 4: perlin_spheres(); break; + case 5: quads(); break; + case 6: simple_light(); break; + case 7: cornell_box(); break; + case 8: cornell_smoke(); break; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + case 9: final_scene(800, 10000, 40); break; + default: final_scene(400, 250, 4); break; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [scene-final]: <kbd>[main.cc]</kbd> Final scene] <div class='together'> -Running it with 10,000 rays per pixel yields: +Running it with 10,000 rays per pixel (sweet dreams) yields: - <div class="render"> + ![<span class='num'>Image 23:</span> Final scene](../images/img-2.23-book2-final.jpg) - ![Final scene](../images/img.book2-final.jpg) - - </div> +Now go off and make a really cool image of your own! +See [our Further Reading wiki page][wiki-further] for additional project related resources. +Feel free to email questions, comments, and cool images to me at ptrshrl@gmail.com. </div> -Now go off and make a really cool image of your own! See https://in1weekend.blogspot.com/ for -pointers to further reading and features, and feel free to email questions, comments, and cool -images to me at ptrshrl@gmail.com. - (insert acknowledgments.md.html here) +Citing This Book +==================================================================================================== +Consistent citations make it easier to identify the source, location and versions of this work. If +you are citing this book, we ask that you try to use one of the following forms if possible. + +Basic Data +----------- + - **Title (series)**: “Ray Tracing in One Weekend Series” + - **Title (book)**: “Ray Tracing: The Next Week” + - **Author**: Peter Shirley, Trevor David Black, Steve Hollasch + - **Version/Edition**: v4.0.2 + - **Date**: 2025-04-25 + - **URL (series)**: <https://raytracing.github.io/> + - **URL (book)**: <https://raytracing.github.io/books/RayTracingTheNextWeek.html> + +Snippets +--------- + + ### Markdown + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [_Ray Tracing: The Next Week_](https://raytracing.github.io/books/RayTracingTheNextWeek.html) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + ### HTML + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + <a href='https://raytracing.github.io/books/RayTracingTheNextWeek.html'> + <cite>Ray Tracing: The Next Week</cite> + </a> + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + ### LaTeX and BibTex + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ~\cite{Shirley2025RTW2} + + @misc{Shirley2025RTW2, + title = {Ray Tracing: The Next Week}, + author = {Peter Shirley, Trevor David Black, Steve Hollasch}, + year = {2025}, + month = {April}, + note = {\small \texttt{https://raytracing.github.io/books/RayTracingTheNextWeek.html}}, + url = {https://raytracing.github.io/books/RayTracingTheNextWeek.html} + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + ### BibLaTeX + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + \usepackage{biblatex} + + ~\cite{Shirley2025RTW2} + + @online{Shirley2025RTW2, + title = {Ray Tracing: The Next Week}, + author = {Peter Shirley, Trevor David Black, Steve Hollasch}, + year = {2025}, + month = {April}, + url = {https://raytracing.github.io/books/RayTracingTheNextWeek.html} + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + ### IEEE + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + “Ray Tracing: The Next Week.” raytracing.github.io/books/RayTracingTheNextWeek.html + (accessed MMM. DD, YYYY) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + ### MLA + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Ray Tracing: The Next Week. raytracing.github.io/books/RayTracingTheNextWeek.html + Accessed DD MMM. YYYY. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + + +[Peter Shirley]: https://github.com/petershirley +[Steve Hollasch]: https://github.com/hollasch +[Trevor David Black]: https://github.com/trevordblack + +[stb_image]: https://github.com/nothings/stb +[readme]: ../README.md +[releases]: https://github.com/RayTracing/raytracing.github.io/releases/ +[wiki-further]: https://github.com/RayTracing/raytracing.github.io/wiki/Further-Readings + + + <!-- Markdeep: https://casual-effects.com/markdeep/ --> <link rel='stylesheet' href='../style/book.css'> <style class="fallback">body{visibility:hidden;white-space:pre;font-family:monospace}</style> <script src="markdeep.min.js"></script> -<script src="https://casual-effects.com/markdeep/latest/markdeep.min.js"></script> +<script src="https://morgan3d.github.io/markdeep/latest/markdeep.min.js"></script> <script>window.alreadyProcessedMarkdeep||(document.body.style.visibility="visible")</script> diff --git a/books/RayTracingTheRestOfYourLife.html b/books/RayTracingTheRestOfYourLife.html index 199ec79f5..214958db6 100644 --- a/books/RayTracingTheRestOfYourLife.html +++ b/books/RayTracingTheRestOfYourLife.html @@ -1,65 +1,114 @@ +<!DOCTYPE html> <meta charset="utf-8"> +<link rel="icon" type="image/png" href="../favicon.png"> <!-- Markdeep: https://casual-effects.com/markdeep/ --> **Ray Tracing: The Rest of Your Life** - Peter Shirley - edited by Steve Hollasch and Trevor David Black + [Peter Shirley][], [Trevor David Black][], [Steve Hollasch][] <br> - Version 3.0.2, 2020-04-11 + Version 4.0.2, 2025-04-25 <br> - Copyright 2018-2020 Peter Shirley. All rights reserved. + Copyright 2018-2024 Peter Shirley. All rights reserved. Overview ==================================================================================================== - In _Ray Tracing in One Weekend_ and _Ray Tracing: the Next Week_, you built a “real” ray tracer. -In this volume, I assume you will be pursuing a career related to ray tracing, and we will dive into -the math of creating a very serious ray tracer. When you are done you should be ready to start -messing with the many serious commercial ray tracers underlying the movie and product design -industries. There are many many things I do not cover in this short volume; I dive into only one of -many ways to write a Monte Carlo rendering program. I don’t do shadow rays (instead I make rays more -likely to go toward lights), bidirectional methods, Metropolis methods, or photon mapping. What I do -is speak in the language of the field that studies those methods. I think of this book as a deep -exposure that can be your first of many, and it will equip you with some of the concepts, math, and -terms you will need to study the others. - -As before, https://in1weekend.blogspot.com/ will have further readings and references. +If you are motivated, you can take the source and information contained in those books to implement +any visual effect you want. The source provides a meaningful and robust foundation upon which to +build out a raytracer for a small hobby project. Most of the visual effects found in commercial ray +tracers rely on the techniques described in these first two books. However, your capacity to add +increasingly complicated visual effects like subsurface scattering or nested dielectrics will be +severely limited by a missing mathematical foundation. In this volume, I assume that you are either +a highly interested student, or are someone who is pursuing a career related to ray tracing. We will +be diving into the math of creating a very serious ray tracer. When you are done, you should be well +equipped to use and modify the various commercial ray tracers found in many popular domains, such as +the movie, television, product design, and architecture industries. + +There are many many things I do not cover in this short volume. For example, there are many ways of +writing Monte Carlo rendering programs--I dive into only one of them. I don’t cover shadow rays +(deciding instead to make rays more likely to go toward lights), nor do I cover bidirectional +methods, Metropolis methods, or photon mapping. You'll find many of these techniques in the +so-called "serious ray tracers", but they are not covered here because it is more important to cover +the concepts, math, and terms of the field. I think of this book as a deep exposure that should be +your first of many, and it will equip you with some of the concepts, math, and terms that you'll +need in order to study these and other interesting techniques. + +I hope that you find the math as fascinating as I do. + +See the [project README][readme] file for information about this project, the repository on GitHub, +directory structure, building & running, and how to make or reference corrections and contributions. + +As before, see [our Further Reading wiki page][wiki-further] for additional project related +resources. + +These books have been formatted to print well directly from your browser. We also include PDFs of +each book [with each release][releases], in the "Assets" section. Thanks to everyone who lent a hand on this project. You can find them in the acknowledgments section at the end of this book. - A Simple Monte Carlo Program ==================================================================================================== +Let’s start with one of the simplest Monte Carlo programs. If you're not familiar with Monte Carlo +programs, then it'll be good to pause and catch you up. There are two kinds of randomized +algorithms: Monte Carlo and Las Vegas. Randomized algorithms can be found everywhere in computer +graphics, so getting a decent foundation isn't a bad idea. A randomized algorithm uses some amount +of randomness in its computation. A Las Vegas random algorithm always produces the correct result, +whereas a Monte Carlo algorithm _may_ produce a correct result--and frequently gets it wrong! But +for especially complicated problems such as ray tracing, we may not place as huge a priority on +being perfectly exact as on getting an answer in a reasonable amount of time. Las Vegas algorithms +will eventually arrive at the correct result, but we can't make too many guarantees on how long it +will take to get there. The classic example of a Las Vegas algorithm is the _quicksort_ sorting +algorithm. The quicksort algorithm will always complete with a fully sorted list, but, the time it +takes to complete is random. Another good example of a Las Vegas algorithm is the code that we use +to pick a random point in a unit disk: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + inline vec3 random_in_unit_disk() { + while (true) { + auto p = vec3(random_double(-1,1), random_double(-1,1), 0); + if (p.length_squared() < 1) + return p; + } + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [las-vegas-algo]: <kbd>[vec3.h]</kbd> A Las Vegas algorithm] -Let’s start with one of the simplest Monte Carlo (MC) programs. MC programs give a statistical -estimate of an answer, and this estimate gets more and more accurate the longer you run it. This -basic characteristic of simple programs producing noisy but ever-better answers is what MC is all -about, and it is especially good for applications like graphics where great accuracy is not needed. +This code will always eventually arrive at a random point in the unit disk, but we can't say +beforehand how long it'll take. It may take only 1 iteration, it may take 2, 3, 4, or even longer. +Whereas, a Monte Carlo program will give a statistical estimate of an answer, and this estimate will +get more and more accurate the longer you run it. Which means that at a certain point, we can just +decide that the answer is accurate _enough_ and call it quits. This basic characteristic of simple +programs producing noisy but ever-better answers is what Monte Carlo is all about, and is especially +good for applications like graphics where great accuracy is not needed. -<div class='together'> -As an example, let’s estimate $\pi$. There are many ways to do this, with the Buffon Needle -problem being a classic case study. We’ll do a variation inspired by that. Suppose you have a circle -inscribed inside a square: - ![Figure [circ-sq]: Estimating π with a circle inside a square](../images/fig.circ-sq.jpg) +Estimating Pi +-------------- +The canonical example of a Monte Carlo algorithm is estimating $\pi$, so let's do that. There are +many ways to estimate $\pi$, with _Buffon's needle problem_ being a classic case study. In Buffon's +needle problem, one is presented with a floor made of parallel strips of floor board, each of the +same width. If a needle is randomly dropped onto the floor, what is the probability that the needle +will lie across two boards? (You can find more information on this problem with a simple Internet +search.) -</div> +We’ll do a variation inspired by this method. Suppose you have a circle inscribed inside a square: + + ![Figure [circ-square]: Estimating $\pi$ with a circle inside a square + ](../images/fig-3.01-circ-square.jpg) -<div class='together'> Now, suppose you pick random points inside the square. The fraction of those random points that end up inside the circle should be proportional to the area of the circle. The exact fraction should in -fact be the ratio of the circle area to the square area. Fraction: +fact be the ratio of the circle area to the square area: $$ \frac{\pi r^2}{(2r)^2} = \frac{\pi}{4} $$ -</div> <div class='together'> Since the $r$ cancels out, we can pick whatever is computationally convenient. Let’s go with $r=1$, @@ -70,29 +119,33 @@ #include <iostream> #include <iomanip> - #include <math.h> - #include <stdlib.h> int main() { - int N = 1000; + std::cout << std::fixed << std::setprecision(12); + int inside_circle = 0; + int N = 100000; + for (int i = 0; i < N; i++) { auto x = random_double(-1,1); auto y = random_double(-1,1); if (x*x + y*y < 1) inside_circle++; } - std::cout << std::fixed << std::setprecision(12); - std::cout << "Estimate of Pi = " << 4*double(inside_circle) / N << '\n'; + + std::cout << "Estimate of Pi = " << (4.0 * inside_circle) / N << '\n'; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [estpi-1]: <kbd>[pi.cc]</kbd> Estimating π] + [Listing [estpi-1]: <kbd>[pi.cc]</kbd> Estimating $\pi$ -- version one] + +The answer of $\pi$ found will vary from computer to computer based on the initial random seed. On +my computer, this gives me the answer `Estimate of Pi = 3.143760000000`. + </div> -The answer of $\pi$ found will vary from computer to computer based on the initial random seed. -On my computer, this gives me the answer `Estimate of Pi = 3.0880000000` -<div class='together'> +Showing Convergence +-------------------- If we change the program to run forever and just print out a running estimate: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ @@ -100,43 +153,50 @@ #include <iostream> #include <iomanip> - #include <math.h> - #include <stdlib.h> int main() { + std::cout << std::fixed << std::setprecision(12); + int inside_circle = 0; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight int runs = 0; - std::cout << std::fixed << std::setprecision(12); while (true) { runs++; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ auto x = random_double(-1,1); auto y = random_double(-1,1); if (x*x + y*y < 1) inside_circle++; + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight if (runs % 100000 == 0) - std::cout << "Estimate of Pi = " - << 4*double(inside_circle) / runs - << '\n'; + std::cout << "\rEstimate of Pi = " << (4.0 * inside_circle) / runs; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ } + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete + std::cout << "Estimate of Pi = " << (4.0 * inside_circle) / N << '\n'; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [estpi-2]: <kbd>[pi.cc]</kbd> Estimating π, v2] -</div> + [Listing [estpi-2]: <kbd>[pi.cc]</kbd> Estimating $\pi$ -- version two] -<div class='together'> -We get very quickly near $\pi$, and then more slowly zero in on it. This is an example of the *Law -of Diminishing Returns*, where each sample helps less than the last. This is the worst part of MC. -We can mitigate this diminishing return by *stratifying* the samples (often called *jittering*), -where instead of taking random samples, we take a grid and take one sample within each: - ![Figure [jitter]: Sampling areas with jittered points](../images/fig.jitter.jpg) +Stratified Samples (Jittering) +------------------------------- +We get very quickly near $\pi$, and then more slowly zero in on it. This is an example of the _Law +of Diminishing Returns_, where each sample helps less than the last. This is the worst part of Monte +Carlo. We can mitigate this diminishing return by _stratifying_ the samples (often called +_jittering_), where instead of taking random samples, we take a grid and take one sample within +each: -</div> + ![Figure [jitter]: Sampling areas with jittered points](../images/fig-3.02-jitter.jpg) <div class='together'> This changes the sample generation, but we need to know how many samples we are taking in advance -because we need to know the grid. Let’s take a hundred million and try it both ways: +because we need to know the grid. Let’s take a million and try it both ways: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ #include "rtweekend.h" @@ -145,15 +205,20 @@ #include <iomanip> int main() { + std::cout << std::fixed << std::setprecision(12); + int inside_circle = 0; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight int inside_circle_stratified = 0; - int sqrt_N = 10000; + int sqrt_N = 1000; + for (int i = 0; i < sqrt_N; i++) { for (int j = 0; j < sqrt_N; j++) { auto x = random_double(-1,1); auto y = random_double(-1,1); if (x*x + y*y < 1) inside_circle++; + x = 2*((i + random_double()) / sqrt_N) - 1; y = 2*((j + random_double()) / sqrt_N) - 1; if (x*x + y*y < 1) @@ -161,752 +226,1701 @@ } } - auto N = static_cast<double>(sqrt_N) * sqrt_N; - std::cout << std::fixed << std::setprecision(12); std::cout << "Regular Estimate of Pi = " - << 4*double(inside_circle) / (sqrt_N*sqrt_N) << '\n' + << (4.0 * inside_circle) / (sqrt_N*sqrt_N) << '\n' << "Stratified Estimate of Pi = " - << 4*double(inside_circle_stratified) / (sqrt_N*sqrt_N) << '\n'; + << (4.0 * inside_circle_stratified) / (sqrt_N*sqrt_N) << '\n'; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [estpi-3]: <kbd>[pi.cc]</kbd> Estimating π, v3] + [Listing [estpi-3]: <kbd>[pi.cc]</kbd> Estimating $\pi$ -- version three] + +</div> +<div class='together'> On my computer, I get: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Regular Estimate of Pi = 3.14151480 - Stratified Estimate of Pi = 3.14158948 + Regular Estimate of Pi = 3.141184000000 + Stratified Estimate of Pi = 3.141460000000 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Where the first 12 decimal places of pi are: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 3.141592653589 + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + </div> Interestingly, the stratified method is not only better, it converges with a better asymptotic rate! Unfortunately, this advantage decreases with the dimension of the problem (so for example, with the -3D sphere volume version the gap would be less). This is called the *Curse of Dimensionality*. We -are going to be very high dimensional (each reflection adds two dimensions), so I won't stratify in -this book, but if you are ever doing single-reflection or shadowing or some strictly 2D problem, you -definitely want to stratify. +3D sphere volume version the gap would be less). This is called the _Curse of Dimensionality_. Ray +tracing is a very high-dimensional algorithm, where each reflection adds two new dimensions: +$\phi_o$ and $\theta_o$. We won't be stratifying the output reflection angle in this book, simply +because it is a little bit too complicated to cover, but there is a lot of interesting research +currently happening in this space. +As an intermediary, we'll be stratifying the locations of the sampling positions around each pixel +location. Let's start with our familiar Cornell box scene. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include "rtweekend.h" -One Dimensional MC Integration -==================================================================================================== + #include "camera.h" + #include "hittable_list.h" + #include "material.h" + #include "quad.h" + #include "sphere.h" -Integration is all about computing areas and volumes, so we could have framed -chapter [A Simple Monte Carlo Program] in an integral form if we wanted to make it maximally -confusing. But sometimes integration is the most natural and clean way to formulate things. -Rendering is often such a problem. Let’s look at a classic integral: + int main() { + hittable_list world; -$$ I = \int_{0}^{2} x^2 dx $$ + auto red = make_shared<lambertian>(color(.65, .05, .05)); + auto white = make_shared<lambertian>(color(.73, .73, .73)); + auto green = make_shared<lambertian>(color(.12, .45, .15)); + auto light = make_shared<diffuse_light>(color(15, 15, 15)); -<div class='together'> -In computer sciency notation, we might write this as: + // Cornell box sides + world.add(make_shared<quad>(point3(555,0,0), vec3(0,0,555), vec3(0,555,0), green)); + world.add(make_shared<quad>(point3(0,0,555), vec3(0,0,-555), vec3(0,555,0), red)); + world.add(make_shared<quad>(point3(0,555,0), vec3(555,0,0), vec3(0,0,555), white)); + world.add(make_shared<quad>(point3(0,0,555), vec3(555,0,0), vec3(0,0,-555), white)); + world.add(make_shared<quad>(point3(555,0,555), vec3(-555,0,0), vec3(0,555,0), white)); -$$ I = \text{area}( x^2, 0, 2 ) $$ + // Light + world.add(make_shared<quad>(point3(213,554,227), vec3(130,0,0), vec3(0,0,105), light)); -We could also write it as: + // Box 1 + shared_ptr<hittable> box1 = box(point3(0,0,0), point3(165,330,165), white); + box1 = make_shared<rotate_y>(box1, 15); + box1 = make_shared<translate>(box1, vec3(265,0,295)); + world.add(box1); -$$ I = 2 \cdot \text{average}(x^2, 0, 2) $$ -</div> + // Box 2 + shared_ptr<hittable> box2 = box(point3(0,0,0), point3(165,165,165), white); + box2 = make_shared<rotate_y>(box2, -18); + box2 = make_shared<translate>(box2, vec3(130,0,65)); + world.add(box2); -<div class='together'> -This suggests a MC approach: + camera cam; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - #include "rtweekend.h" + cam.aspect_ratio = 1.0; + cam.image_width = 600; + cam.samples_per_pixel = 64; + cam.max_depth = 50; + cam.background = color(0,0,0); - #include <iostream> - #include <iomanip> - #include <math.h> - #include <stdlib.h> + cam.vfov = 40; + cam.lookfrom = point3(278, 278, -800); + cam.lookat = point3(278, 278, 0); + cam.vup = vec3(0, 1, 0); - int main() { - int N = 1000000; - auto sum = 0.0; - for (int i = 0; i < N; i++) { - auto x = random_double(0,2); - sum += x*x; - } - std::cout << std::fixed << std::setprecision(12); - std::cout << "I = " << 2 * sum/N << '\n'; + cam.defocus_angle = 0; + + cam.render(world); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [integ-xsq-1]: <kbd>[integrate_x_sq.cc]</kbd> Integrating $x^2$] -</div> + [Listing [estpi-3]: <kbd>[main.cc]</kbd> Cornell box, revisited] -This, as expected, produces approximately the exact answer we get with algebra, $I = 8/3$. We could -also do it for functions that we can’t analytically integrate like $\log(\sin(x))$. In graphics, we -often have functions we can evaluate but can’t write down explicitly, or functions we can only -probabilistically evaluate. That is in fact what the ray tracing `ray_color()` function of the last -two books is -- we don’t know what color is seen in every direction, but we can statistically -estimate it in any given dimension. - -One problem with the random program we wrote in the first two books is that small light sources -create too much noise. This is because our uniform sampling doesn’t sample these light sources often -enough. Light sources are only sampled if a ray scatters toward them, but this can be unlikely for a -small light, or a light that is far away. We could lessen this problem if we sent more random -samples toward this light, but this will cause the scene to be inaccurately bright. We can remove -this inaccuracy by downweighting these samples to adjust for the over-sampling. How we do that -adjustment? To do that, we will need the concept of a _probability density function_. +Run this program to generate an un-stratified render and save for comparison. -<div class='together'> -First, what is a _density function_? It’s just a continuous form of a histogram. Here’s an example -from the histogram Wikipedia page: +Now make the following changes to implement a stratified sampling procedure: - ![Figure [histogram]: Histogram example](../images/fig.histogram.jpg) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class camera { + public: + ... -</div> + void render(const hittable& world) { + initialize(); -<div class='together'> -If we added data for more trees, the histogram would get taller. If we divided the data into more -bins, it would get shorter. A discrete density function differs from a histogram in that it -normalizes the frequency y-axis to a fraction or percentage (just a fraction times 100). A -continuous histogram, where we take the number of bins to infinity, can’t be a fraction because the -height of all the bins would drop to zero. A density function is one where we take the bins and -adjust them so they don’t get shorter as we add more bins. For the case of the tree histogram above -we might try: - - $$ \text{bin-height} = \frac{(\text{Fraction of trees between height }H\text{ and }H’)}{(H-H’)} $$ -</div> + std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n"; -<div class='together'> -That would work! We could interpret that as a statistical predictor of a tree’s height: + for (int j = 0; j < image_height; j++) { + std::clog << "\rScanlines remaining: " << (image_height - j) << ' ' << std::flush; + for (int i = 0; i < image_width; i++) { + color pixel_color(0,0,0); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + for (int s_j = 0; s_j < sqrt_spp; s_j++) { + for (int s_i = 0; s_i < sqrt_spp; s_i++) { + ray r = get_ray(i, j, s_i, s_j); + pixel_color += ray_color(r, max_depth, world); + } + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + write_color(std::cout, pixel_samples_scale * pixel_color); + } + } - $$ \text{Probability a random tree is between } H \text{ and } H’ = \text{bin-height}\cdot(H-H’)$$ -</div> + std::clog << "\rDone. \n"; + } -If we wanted to know about the chances of being in a span of multiple bins, we would sum. + private: + int image_height; // Rendered image height + double pixel_samples_scale; // Color scale factor for a sum of pixel samples + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + int sqrt_spp; // Square root of number of samples per pixel + double recip_sqrt_spp; // 1 / sqrt_spp + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + point3 center; // Camera center + ... -A _probability density function_, henceforth _PDF_, is that fractional histogram made continuous. + void initialize() { + image_height = int(image_width / aspect_ratio); + image_height = (image_height < 1) ? 1 : image_height; -Let’s make a _PDF_ and use it a bit to understand it more. Suppose I want a random number $r$ -between 0 and 2 whose probability is proportional to itself: $r$. We would expect the PDF $p(r)$ to -look something like the figure below, but how high should it be? - ![Figure [linear-pdf]: A linear PDF](../images/fig.linear-pdf.jpg) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + sqrt_spp = int(std::sqrt(samples_per_pixel)); + pixel_samples_scale = 1.0 / (sqrt_spp * sqrt_spp); + recip_sqrt_spp = 1.0 / sqrt_spp; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ -<div class='together'> -The height is just $p(2)$. What should that be? We could reasonably make it anything by -convention, and we should pick something that is convenient. Just as with histograms we can sum up -(integrate) the region to figure out the probability that $r$ is in some interval $(x_0,x_1)$: + center = lookfrom; + ... + } - $$ \text{Probability } x_0 < r < x_1 = C \cdot \text{area}(p(r), x_0, x_1) $$ -</div> -<div class='together'> -where $C$ is a scaling constant. We may as well make $C = 1$ for cleanliness, and that is exactly -what is done in probability. We also know the probability $r$ has the value 1 somewhere, so for this -case + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + ray get_ray(int i, int j, int s_i, int s_j) const { + // Construct a camera ray originating from the defocus disk and directed at a randomly + // sampled point around the pixel location i, j for stratified sample square s_i, s_j. - $$ \text{area}(p(r), 0, 2) = 1 $$ -</div> + auto offset = sample_square_stratified(s_i, s_j); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + auto pixel_sample = pixel00_loc + + ((i + offset.x()) * pixel_delta_u) + + ((j + offset.y()) * pixel_delta_v); -<div class='together'> -Since $p(r)$ is proportional to $r$, _i.e._, $p = C' \cdot r$ for some other constant $C'$ + auto ray_origin = (defocus_angle <= 0) ? center : defocus_disk_sample(); + auto ray_direction = pixel_sample - ray_origin; + auto ray_time = random_double(); - $$ - area(C'r, 0, 2) = \int_{0}^{2} C' r dr - = \frac{C'r^2}{2} \biggr|_{r=2}^{r=0} - = \frac{C' \cdot 2^2}{2} - \frac{C' \cdot 0^2}{2} - = 2C' - $$ + return ray(ray_origin, ray_direction, ray_time); + } -So $p(r) = r/2$. -</div> -How do we generate a random number with that PDF $p(r)$? For that we will need some more machinery. -Don’t worry this doesn’t go on forever! + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + vec3 sample_square_stratified(int s_i, int s_j) const { + // Returns the vector to a random point in the square sub-pixel specified by grid + // indices s_i and s_j, for an idealized unit square pixel [-.5,-.5] to [+.5,+.5]. -<div class='together'> -Given a random number from `d = random_double()` that is uniform and between 0 and 1, we should be -able to find some function $f(d)$ that gives us what we want. Suppose $e = f(d) = d^2$. This is no -longer a uniform PDF. The PDF of $e$ will be bigger near 0 than it is near 1 (squaring a number -between 0 and 1 makes it smaller). To convert this general observation to a function, we need the -cumulative probability distribution function $P(x)$: + auto px = ((s_i + random_double()) * recip_sqrt_spp) - 0.5; + auto py = ((s_j + random_double()) * recip_sqrt_spp) - 0.5; - $$ P(x) = \text{area}(p, -\infty, x) $$ -</div> + return vec3(px, py, 0); + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ -<div class='together'> -Note that for $x$ where we didn’t define $p(x)$, $p(x) = 0$, _i.e._, the probability of an $x$ there -is zero. For our example PDF $p(r) = r/2$, the $P(x)$ is: + vec3 sample_square() const { + ... + } - $$ P(x) = 0 : x < 0 $$ - $$ P(x) = \frac{x^2}{4} : 0 < x < 2 $$ - $$ P(x) = 1 : x > 2 $$ -</div> + ... + }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [render-estpi-3]: <kbd>[camera.h]</kbd> Stratifying the samples inside pixels] <div class='together'> -One question is, what’s up with $x$ versus $r$? They are dummy variables -- analogous to the -function arguments in a program. If we evaluate $P$ at $x = 1.0$, we get: +If we compare the results from without stratification: - $$ P(1.0) = \frac{1}{4} $$ -</div> + ![<span class='num'>Image 1:</span> Cornell box, no stratification + ](../images/img-3.01-cornell-no-strat.png class='pixel') -<div class='together'> -This says _the probability that a random variable with our PDF is less than one is 25%_. This gives -rise to a clever observation that underlies many methods to generate non-uniform random numbers. We -want a function `f()` that when we call it as `f(random_double())` we get a return value with a PDF -$\frac{x^2}{4}$. We don’t know what that is, but we do know that 25% of what it returns should be -less than 1.0, and 75% should be above 1.0. If $f()$ is increasing, then we would expect $f(0.25) = -1.0$. This can be generalized to figure out $f()$ for every possible input: - - $$ f(P(x)) = x $$ </div> <div class='together'> -That means $f$ just undoes whatever $P$ does. So, +To after, with stratification: - $$ f(x) = P^{-1}(x) $$ -</div> + ![<span class='num'>Image 2:</span> Cornell box, with stratification + ](../images/img-3.02-cornell-strat.png class='pixel') -<div class='together'> -The -1 means “inverse function”. Ugly notation, but standard. For our purposes, if we have PDF $p()$ -and cumulative distribution function $P()$, we can use this "inverse function" with a random number -to get what we want: +You should, if you squint, be able to see sharper contrast at the edges of planes and at the edges +of boxes. The effect will be more pronounced at locations that have a higher frequency of change. +High frequency change can also be thought of as high information density. For our cornell box scene, +all of our materials are matte, with a soft area light overhead, so the only locations of high +information density are at the edges of objects. The effect will be more obvious with textures and +reflective materials. + +If you are ever doing single-reflection or shadowing or some strictly 2D problem, you definitely +want to stratify. - $$ e = P^{-1} (\text{random_double}()) $$ </div> -<div class='together'> -For our PDF $p(x) = x/2$, and corresponding $P(x)$, we need to compute the inverse of $P$. If we -have - $$ y = \frac{x^2}{4} $$ -we get the inverse by solving for $x$ in terms of $y$: +One Dimensional Monte Carlo Integration +==================================================================================================== +Our variation of Buffon's needle problem is a way of calculating $\pi$ by solving for the ratio of +the area of the circle and the area of the circumscribed square: + + $$ \frac{\operatorname{area}(\mathit{circle})}{\operatorname{area}(\mathit{square})} + = \frac{\pi}{4} + $$ - $$ x = \sqrt{4y} $$ +We picked a bunch of random points in the circumscribed square and counted the fraction of them that +were also in the unit circle. This fraction was an estimate that tended toward $\frac{\pi}{4}$ as +more points were added. If we didn't know the area of a circle, we could still solve for it using +the above ratio. We know that the ratio of areas of the unit circle and the circumscribed square is +$\frac{\pi}{4}$, and we know that the area of a circumscribed square is $4r^2$, so we could then use +those two quantities to get the area of a circle: -Thus our random number with density $p$ is found with: + $$ \frac{\operatorname{area}(\mathit{circle})}{\operatorname{area}(\mathit{square})} + = \frac{\pi}{4} + $$ - $$ e = \sqrt{4\cdot\text{random_double}()} $$ -</div> + $$ \frac{\operatorname{area}(\mathit{circle})}{(2r)^2} = \frac{\pi}{4} $$ -Note that this ranges from 0 to 2 as hoped, and if we check our work by replacing `random_double()` -with $\frac{1}{4}$ we get 1 as expected. + $$ \operatorname{area}(\mathit{circle}) = \frac{\pi}{4} 4r^2 $$ -<div class='together'> -We can now sample our old integral + $$ \operatorname{area}(\mathit{circle}) = \pi r^2 $$ - $$ I = \int_{0}^{2} x^2 $$ -</div> +We choose a circle with radius $r = 1$ and get: -<div class='together'> -We need to account for the non-uniformity of the PDF of $x$. Where we sample too much we should -down-weight. The PDF is a perfect measure of how much or little sampling is being done. So the -weighting function should be proportional to $1/pdf$. In fact it is exactly $1/pdf$: + $$ \operatorname{area}(\mathit{circle}) = \pi $$ - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - inline double pdf(double x) { - return 0.5*x; - } - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ +<div class='together'> +Our work above is equally valid as a means to solve for $\pi$ as it is a means to solve for the area +of a circle. So we could make the following substitution in one of the first versions of our pi +program: - int main() { - int N = 1000000; - auto sum = 0.0; - for (int i = 0; i < N; i++) { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete + std::cout << "Estimate of Pi = " << (4.0 * inside_circle) / N << '\n'; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - auto x = sqrt(random_double(0,4)); - sum += x*x / pdf(x); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - } - std::cout << std::fixed << std::setprecision(12); - std::cout << "I = " << sum/N << '\n'; - } + std::cout << "Estimated area of unit circle = " << (4.0 * inside_circle) / N << '\n'; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [integ-xsq-2]: <kbd>[integrate_x_sq.cc]</kbd> Integrating $x^2$ with PDF] + [Listing [estunitcircle]: <kbd>[pi.cc]</kbd> Estimating area of unit circle] + </div> -Since we are sampling more where the integrand is big, we might expect less noise and thus faster -convergence. In effect, we are steering our samples toward the parts of the distribution that are -more _important_. This is why using a carefully chosen non-uniform PDF is usually called _importance -sampling_. -<div class='together'> -If we take that same code with uniform samples so the PDF = $1/2$ over the range [0,2] we can use -the machinery to get `x = random_double(0,2)`, and the code is: +Expected Value +-------------- +Let's take a step back and think about our Monte Carlo algorithm a little bit more generally. - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - inline double pdf(double x) { - return 0.5; - } - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ +If we assume that we have all of the following: - int main() { - int N = 1000000; - auto sum = 0.0; - for (int i = 0; i < N; i++) { - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - auto x = random_double(0,2); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - sum += x*x / pdf(x); - } - std::cout << std::fixed << std::setprecision(12); - std::cout << "I = " << sum/N << '\n'; - } - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [integ-xsq-3]: <kbd>[integrate_x_sq.cc]</kbd> Integrating $x^2$, v3] -</div> +1. A list of values $X$ that contains members $x_i$: -<div class='together'> -Note that we don’t need that 2 in the `2*sum/N` anymore -- that is handled by the PDF, which is 2 -when you divide by it. You’ll note that importance sampling helps a little, but not a ton. We could -make the PDF follow the integrand exactly: + $$ X = (x_0, x_1, ..., x_{N-1}) $$ - $$ p(x) = \frac{3}{8}x^2 $$ +2. A continuous function $f(x)$ that takes members from the list: -And we get the corresponding + $$ y_i = f(x_i) $$ - $$ P(x) = \frac{x^3}{8} $$ +3. A function $F(X)$ that takes the list $X$ as input and produces the list $Y$ as output: -and + $$ Y = F(X) $$ - $$ P^{-1}(x) = 8x^\frac{1}{3} $$ -</div> +4. Where output list $Y$ has members $y_i$: -<div class='together'> -This perfect importance sampling is only possible when we already know the answer (we got $P$ by -integrating $p$ analytically), but it’s a good exercise to make sure our code works. For just 1 -sample we get: + $$ Y = (y_0, y_1, ..., y_{N-1}) = (f(x_0), f(x_1), ..., f(x_{N-1})) $$ - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - inline double pdf(double x) { - return 3*x*x/8; - } - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ +If we assume all of the above, then we could solve for the arithmetic mean--the average--of the list +$Y$ with the following: - int main() { - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - int N = 1; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - auto sum = 0.0; - for (int i = 0; i < N; i++) { - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - auto x = pow(random_double(0,8), 1./3.); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - sum += x*x / pdf(x); - } - std::cout << std::fixed << std::setprecision(12); - std::cout << "I = " << sum/N << '\n'; - } - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [integ-xsq-4]: <kbd>[integrate_x_sq.cc]</kbd> Integrating $x^2$, final version] -</div> + $$ \operatorname{average}(Y_i) = E[Y] = \frac{1}{N} \sum_{i=0}^{N-1} y_i $$ + $$ = \frac{1}{N} \sum_{i=0}^{N-1} f(x_i) $$ + $$ = E[F(X)] $$ -Which always returns the exact answer. +Where $E[Y]$ is referred to as the _expected value of_ $Y$. -<div class='together'> -Let’s review now because that was most of the concepts that underlie MC ray tracers. +Note the subtle difference between _average value_ and _expected value_ here: - 1. You have an integral of $f(x)$ over some domain $[a,b]$ - 2. You pick a PDF $p$ that is non-zero over $[a,b]$ - 3. You average a whole ton of $\frac{f(r)}{p(r)}$ where $r$ is a random number with PDF $p$. +- A set may have many different subsets of selections from that set. Each subset has an _average + value_, which is the sum of all selections divided by the count of selections. Note that a given + item may occur zero times, one times, or multiple times in a subset. -Any choice of PDF $p$ will always converge to the right answer, but the closer that $p$ -approximates $f$, the faster that it will converge. -</div> +- A set has only one _expected value_: the sum of _all_ members of a set, divided by the total + number of items in that set. Put another way, the _expected value_ is the _average value_ of _all_ + members of a set. +It is important to note that as the number of random samples from a set increases, the average value +of a set will converge to the expected value. +If the values of $x_i$ are chosen randomly from a continuous interval $[a,b]$ such that $ a \leq x_i +\leq b $ for all values of $i$, then $E[F(X)]$ will approximate the average of the continuous +function $f(x')$ over the the same interval $ a \leq x' \leq b $. -MC Integration on the Sphere of Directions -==================================================================================================== + $$ E[f(x') | a \leq x' \leq b] \approx E[F(X) | X = + \{\small x_i | a \leq x_i \leq b \normalsize \} ] $$ + $$ \approx E[Y = \{\small y_i = f(x_i) | a \leq x_i \leq b \normalsize \} ] $$ -In our ray tracer we pick random directions, and directions can be represented as points on the -unit sphere. The same methodology as before applies, but now we need to have a PDF defined over 2D. -Suppose we have this integral over all directions: + $$ \approx \frac{1}{N} \sum_{i=0}^{N-1} f(x_i) $$ - $$ \int cos^2(\theta) $$ +If we take the number of samples $N$ and take the limit as $N$ goes to $\infty$, then we get the +following: -<div class='together'> -By MC integration, we should just be able to sample $\cos^2(\theta) / p(\text{direction})$, but what -is direction in that context? We could make it based on polar coordinates, so $p$ would be in terms -of $(\theta, \phi)$. However you do it, remember that a PDF has to integrate to 1 and represent the -relative probability of that direction being sampled. We have a method from the last books to take -uniform random samples in or on a unit sphere: + $$ E[f(x') | a \leq x' \leq b] = \lim_{N \to \infty} \frac{1}{N} \sum_{i=0}^{N-1} f(x_i) $$ - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - vec3 random_in_unit_sphere() { - while (true) { - auto p = vec3::random(-1,1); - if (p.length_squared() >= 1) continue; - return p; - } - } +Within the continuous interval $[a,b]$, the expected value of continuous function $f(x')$ can be +perfectly represented by summing an infinite number of random points within the interval. As this +number of points approaches $\infty$ the average of the outputs tends to the exact answer. This is a +Monte Carlo algorithm. - vec3 random_unit_vector() { - auto a = random_double(0, 2*pi); - auto z = random_double(-1, 1); - auto r = sqrt(1 - z*z); - return vec3(r*cos(a), r*sin(a), z); - } - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [random-unit-vector]: <kbd>[vec3.h]</kbd> Random vector of unit length] -</div> +Sampling random points isn't our only way to solve for the expected value over an interval. We can +also choose where we place our sampling points. If we had $N$ samples over an interval $[a,b]$ then +we could choose to equally space points throughout: -<div class='together'> -Now what is the PDF of these uniform points? As a density on the unit sphere, it is $1/\text{area}$ -of the sphere or $1/(4\pi)$. If the integrand is $\cos^2(\theta)$, and $\theta$ is the angle with -the z axis: + $$ x_i = a + i \Delta x $$ + $$ \Delta x = \frac{b - a}{N} $$ - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - inline double pdf(const vec3& p) { - return 1 / (4*pi); - } +Then solving for their expected value: - int main() { - int N = 1000000; - auto sum = 0.0; - for (int i = 0; i < N; i++) { - vec3 d = random_unit_vector(); - auto cosine_squared = d.z()*d.z(); - sum += cosine_squared / pdf(d); - } - std::cout << std::fixed << std::setprecision(12); - std::cout << "I = " << sum/N << '\n'; - } - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [main-sphereimp]: <kbd>[sphere_importance.cc]</kbd> - Generating importance-sampled points on the unit sphere] -</div> + $$ E[f(x') | a \leq x' \leq b] \approx \frac{1}{N} \sum_{i=0}^{N-1} f(x_i) + \Big|_{x_i = a + i \Delta x} $$ + $$ E[f(x') | a \leq x' \leq b] \approx \frac{\Delta x}{b - a} \sum_{i=0}^{N-1} f(x_i) + \Big|_{x_i = a + i \Delta x} $$ + $$ E[f(x') | a \leq x' \leq b] \approx \frac{1}{b - a} \sum_{i=0}^{N-1} f(x_i) \Delta x + \Big|_{x_i = a + i \Delta x} $$ -The analytic answer (if you remember enough advanced calc, check me!) is $\frac{4}{3} \pi$, and the -code above produces that. Next, we are ready to apply that in ray tracing! +Take the limit as $N$ approaches $\infty$ -The key point here is that all the integrals and probability and all that are over the unit sphere. -The area on the unit sphere is how you measure the directions. Call it direction, solid angle, or -area -- it’s all the same thing. Solid angle is the term usually used. If you are comfortable with -that, great! If not, do what I do and imagine the area on the unit sphere that a set of directions -goes through. The solid angle $\omega$ and the projected area $A$ on the unit sphere are the same -thing. + $$ E[f(x') | a \leq x' \leq b] = \lim_{N \to \infty} \frac{1}{b - a} \sum_{i=0}^{N-1} + f(x_i) \Delta x \Big|_{x_i = a + i \Delta x} $$ - ![Figure [solid-angle]: Solid angle / projected area of a sphere](../images/fig.solid-angle.jpg) +This is, of course, just a regular integral: -Now let’s go on to the light transport equation we are solving. + $$ E[f(x') | a \leq x' \leq b] = \frac{1}{b - a} \int_{a}^{b} f(x) dx $$ +If you recall your introductory calculus class, the integral of a function is the area under the +curve over that interval: + $$ \operatorname{area}(f(x), a, b) = \int_{a}^{b} f(x) dx$$ -Light Scattering -==================================================================================================== +Therefore, the average over an interval is intrinsically linked with the area under the curve in +that interval. -In this chapter we won't actually program anything. We will set up for a big lighting change in the -next chapter. + $$ E[f(x) | a \leq x \leq b] = \frac{1}{b - a} \cdot \operatorname{area}(f(x), a, b) $$ -Our program from the last books already scatters rays from a surface or volume. This is the commonly -used model for light interacting with a surface. One natural way to model this is with probability. -First, is the light absorbed? +Both the integral of a function and a Monte Carlo sampling of that function can be used to solve for +the average over a specific interval. While integration solves for the average with the sum of +infinitely many infinitesimally small slices of the interval, a Monte Carlo algorithm will +approximate the same average by solving the sum of ever increasing random sample points within the +interval. Counting the number of points that fall inside of an object isn't the only way to measure +its average or area. Integration is also a common mathematical tool for this purpose. If a closed +form exists for a problem, integration is frequently the most natural and clean way to formulate +things. -Probability of light scattering: $A$ +I think a couple of examples will help. -Probability of light being absorbed: $1-A$ -Here $A$ stands for _albedo_ (latin for _whiteness_). Albedo is a precise technical term in some -disciplines, but in all cases it is used to define some form of _fractional reflectance_. This -_fractional reflectance_ (or albedo) will vary with color and (as we implemented for our glass in -book one) can vary with incident direction. +Integrating x² +--------------- +Let’s look at a classic integral: -In most physically based renderers, we would use a set of wavelengths for the light color rather -than RGB. We can extend our intuition by thinking of R, G, and B as specific algebraic mixtures of -long, medium, and short wavelengths. + $$ I = \int_{0}^{2} x^2 dx $$ -If the light does scatter, it will have a directional distribution that we can describe as a PDF -over solid angle. I will refer to this as its _scattering PDF_: $s(direction)$. The scattering PDF -can also vary with _incident direction_, which is the direction of the incoming ray. You can see -this varying with incident direction when you look at reflections off a road -- they become -mirror-like as your viewing angle (incident angle) approaches grazing. +We could solve this using integration: -<div class='together'> -The color of a surface in terms of these quantities is: + $$ I = \frac{1}{3} x^3 \Big|_{0}^{2} $$ + $$ I = \frac{1}{3} (2^3 - 0^3) $$ + $$ I = \frac{8}{3} $$ - $$ Color = \int A \cdot s(direction) \cdot \text{color}(direction) $$ -</div> +Or, we could solve the integral using a Monte Carlo approach. In computer sciency notation, we might +write this as: -Note that $A$ and $s()$ may depend on the view direction or the scattering position (position on a -surface or position within a volume). Therefore, the output color may also vary with view direction -or scattering position. + $$ I = \operatorname{area}( x^2, 0, 2 ) $$ -<div class='together'> -If we apply the MC basic formula we get the following statistical estimate: +We could also write it as: - $$ Color = \frac{A \cdot s(direction) \cdot \text{color}(direction)}{p(direction)} $$ + $$ E[f(x) | a \leq x \leq b] = \frac{1}{b - a} \cdot \operatorname{area}(f(x), a, b) $$ + $$ \operatorname{average}(x^2, 0, 2) = \frac{1}{2 - 0} \cdot \operatorname{area}( x^2, 0, 2 ) $$ + $$ \operatorname{average}(x^2, 0, 2) = \frac{1}{2 - 0} \cdot I $$ + $$ I = 2 \cdot \operatorname{average}(x^2, 0, 2) $$ -where $p(direction)$ is the PDF of whatever direction we randomly generate. -</div> +<div class='together'> +The Monte Carlo approach: -For a Lambertian surface we already implicitly implemented this formula for the special case where -$p()$ is a cosine density. The $s()$ of a Lambertian surface is proportional to $\cos(\theta)$, -where $\theta$ is the angle relative to the surface normal. Remember that all PDF need to integrate -to one. For $\cos(\theta) < 0$ we have $s(direction) = 0$, and the integral of cos over the -hemisphere is $\pi$. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include "rtweekend.h" -<div class='together'> -To see that, remember that in spherical coordinates: + #include <iostream> + #include <iomanip> + + int main() { + int a = 0; + int b = 2; + int N = 1000000; + auto sum = 0.0; - $$ dA = \sin(\theta) d\theta d\phi $$ + for (int i = 0; i < N; i++) { + auto x = random_double(a, b); + sum += x*x; + } -So: + std::cout << std::fixed << std::setprecision(12); + std::cout << "I = " << (b - a) * (sum / N) << '\n'; + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [integ-xsq-1]: <kbd>[integrate_x_sq.cc]</kbd> Integrating $x^2$] - $$ Area = \int_{0}^{2 \pi} \int_{0}^{\pi / 2} cos(\theta) sin(\theta) d\theta d\phi = - 2 \pi \frac{1}{2} = \pi $$ </div> <div class='together'> -So for a Lambertian surface the scattering PDF is: +This, as expected, produces approximately the exact answer we get with integration, _i.e._ +$I = 2.666… = \frac{8}{3}$. You could rightly point to this example and say that the integration is +actually a lot less work than the Monte Carlo. That might be true in the case where the function is +$f(x) = x^2$, but there exist many functions where it might be simpler to solve for the Monte Carlo +than for the integration, like $f(x) = \sin^5(x)$. + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + for (int i = 0; i < N; i++) { + auto x = random_double(a, b); + sum += std::pow(std::sin(x), 5.0); + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [integ-sin5]: Integrating $\sin^5$] - $$ s(direction) = \frac{\cos(\theta)}{\pi} $$ </div> <div class='together'> -If we sample using a PDF that equals the scattering PDF: +We could also use the Monte Carlo algorithm for functions where an analytical integration does not +exist, like $f(x) = \ln(\sin(x))$. - $$ p(direction) = s(direction) = \frac{\cos(\theta)}{\pi} $$ + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + for (int i = 0; i < N; i++) { + auto x = random_double(a, b); + sum += std::log(std::sin(x)); + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [integ-ln-sin]: Integrating $\ln(\sin)$] -The numerator and denominator cancel out, and we get: +</div> - $$ Color = A \cdot color(direction) $$ +In graphics, we often have functions that we can write down explicitly but that have a complicated +analytic integration, or, just as often, we have functions that _can_ be evaluated but that _can't_ +be written down explicitly, and we will frequently find ourselves with a function that can _only_ be +evaluated probabilistically. The function `ray_color` from the first two books is an example of a +function that can only be determined probabilistically. We can’t know what color can be seen from +any given place in all directions, but we can statistically estimate which color can be seen from +one particular place, for a single particular direction. + + +Density Functions +------------------ +The `ray_color` function that we wrote in the first two books, while elegant in its simplicity, has +a fairly _major_ problem. Small light sources create too much noise. This is because our uniform +sampling doesn’t sample these light sources often enough. Light sources are only sampled if a ray +scatters toward them, but this can be unlikely for a small light, or a light that is far away. If +the background color is black, then the only real sources of light in the scene are from the lights +that are actually placed about the scene. There might be two rays that intersect at nearby points on +a surface, one that is randomly reflected toward the light and one that is not. The ray that is +reflected toward the light will appear a very bright color. The ray that is reflected to somewhere +else will appear a very dark color. The two intensities should really be somewhere in the middle. We +could lessen this problem if we steered both of these rays toward the light, but this would cause +the scene to be inaccurately bright. + +For any given ray, we usually trace from the camera, through the scene, and terminate at a light. +But imagine if we traced this same ray from the light source, through the scene, and terminated at +the camera. This ray would start with a bright intensity and would lose energy with each successive +bounce around the scene. It would ultimately arrive at the camera, having been dimmed and colored by +its reflections off various surfaces. Now, imagine if this ray was forced to bounce toward the +camera as soon as it could. It would appear inaccurately bright because it hadn't been dimmed by +successive bounces. This is analogous to sending more random samples toward the light. It would go a +long way toward solving our problem of having a bright pixel next to a dark pixel, but it would then +just make _all_ of our pixels bright. + +We can remove this inaccuracy by downweighting those samples to adjust for the over-sampling. How do +we do this adjustment? Well, we'll first need to understand the concept of a _probability density +function_. But to understand the concept of a _probability density function_, we'll first need to +know what a _density function_ is. + +A _density function_ is just the continuous version of a histogram. Here’s an example of a histogram +from the histogram Wikipedia page: -This is exactly what we had in our original `ray_color()` function! However, we need to generalize -so we can send extra rays in important directions, such as toward the lights. + ![Figure [histogram]: Histogram example](../images/fig-3.03-histogram.jpg) -The treatment above is slightly non-standard because I want the same math to work for surfaces and -volumes. To do otherwise will make some ugly code. -</div> +If we had more items in our data source, the number of bins would stay the same, but each bin would +have a higher frequency of each item. If we divided the data into more bins, we'd have more bins, +but each bin would have a lower frequency of each item. If we took the number of bins and raised it +to infinity, we'd have an infinite number of zero-frequency bins. To solve for this, we'll replace +our histogram, which is a _discrete function_, with a _discrete density function_. A _discrete +density function_ differs from a _discrete function_ in that it normalizes the y-axis to a fraction +or percentage of the total, _i.e_ its density, instead of a total count for each bin. Converting +from a _discrete function_ to a _discrete density function_ is trivial: -<div class='together'> -If you read the literature, you’ll see reflection described by the bidirectional reflectance -distribution function (BRDF). It relates pretty simply to our terms: + $$ \text{Density of Bin i} = \frac{\text{Number of items in Bin i}} + {\text{Number of items total}} $$ - $$ BRDF = \frac{A \cdot s(direction)}{\cos(\theta)} $$ +Once we have a _discrete density function_, we can then convert it into a _density function_ by +changing our discrete values into continuous values. -So for a Lambertian surface for example, $BRDF = A / \pi$. Translation between our terms and BRDF is -easy. + $$ \text{Bin Density} = \frac{(\text{Fraction of trees between height }H\text{ and }H’)} + {(H-H’)} $$ + +So a _density function_ is a continuous histogram where all of the values are normalized against a +total. If we had a specific tree we wanted to know the height of, we could create a _probability +function_ that would tell us how likely it is for our tree to fall within a specific bin. + + $$ \text{Probability of Bin i} = \frac{\text{Number of items in Bin i}} + {\text{Number of items total}} $$ + +If we combined our _probability function_ and our (continuous) _density function_, we could +interpret that as a statistical predictor of a tree’s height: + + $$ \text{Probability a random tree is between } H \text{ and } H’ = + \text{Bin Density}\cdot(H-H’)$$ + +Indeed, with this continuous probability function, we can now say the likelihood that any given tree +has a height that places it within any arbitrary span of multiple bins. This is a _probability +density function_ (henceforth _PDF_). In short, a PDF is a continuous function that can be +integrated over to determine how likely a result is over an integral. + + +Constructing a PDF +------------------- +Let’s make a PDF and play around with it to build up an intuition. We'll use the following function: + + ![Figure [linear-pdf]: A linear PDF](../images/fig-3.04-linear-pdf.jpg) + +What does this function do? Well, we know that a PDF is just a continuous function that defines the +likelihood of an arbitrary range of values. This function $p(r)$ is constrained between $0$ and $2$ +and linearly increases along that interval. So, if we used this function as a PDF to generate a +random number then the _probability_ of getting a number near zero would be less than the +probability of getting a number near two. + +The PDF $p(r)$ is a linear function that starts with $0$ at $r=0$ and monotonically increases to its +highest point at $p(2)$ for $r=2$. What is the value of $p(2)$? What is the value of $p(r)$? Maybe +$p(2)$ is 2? The PDF increases linearly from 0 to 2, so guessing that the value of $p(2)$ is 2 seems +reasonable. At least it looks like it can't be 0. + +Remember that the PDF is a probability function. We are constraining the PDF so that it lies in the +range [0,2]. The PDF represents the continuous density function for a probabilistic list. If we know +that everything in that list is contained within 0 and 2, we can say that the probability of getting +a value between 0 and 2 is 100%. Therefore, the area under the curve must sum to 1: + + $$ \operatorname{area}(p(r), 0, 2) = 1 $$ + +All linear functions can be represented as a constant term multiplied by a variable. + + $$ p(r) = C \cdot r $$ + +We need to solve for the value of $C$. We can use integration to work backwards. + + $$ 1 = \operatorname{area}(p(r), 0, 2) $$ + $$ = \int_{0}^{2} C \cdot r dr $$ + $$ = C \cdot \int_{0}^{2} r dr $$ + $$ = C \cdot \frac{r^2}{2} \Big|_{0}^{2} $$ + $$ = C ( \frac{2^2}{2} - \frac{0}{2} ) $$ + $$ C = \frac{1}{2} $$ + +That gives us the PDF of $p(r) = r/2$. Just as with histograms we can sum up (integrate) the region +to figure out the probability that $r$ is in some interval $[x_0,x_1]$: + + $$ \operatorname{Probability} (r | x_0 \leq r \leq x_1 ) + = \operatorname{area}(p(r), x_0, x_1) + $$ + + $$ \operatorname{Probability} (r | x_0 \leq r \leq x_1 ) = \int_{x_0}^{x_1} \frac{r}{2} dr $$ + +To confirm your understanding, you should integrate over the region $r=0$ to $r=2$, you should get a +probability of 1. + +After spending enough time with PDFs you might start referring to a PDF as the probability that a +variable $r$ is value $x$, _i.e._ $p(r=x)$. Don't do this. For a continuous function, the +probability that a variable is a specific value is always zero. A PDF can only tell you the +probability that a variable will fall within a given interval. If the interval you're checking +against is a single value, then the PDF will always return a zero probability because its "bin" is +infinitely thin (has zero width). Here's a simple mathematical proof of this fact: + + $$ \operatorname{Probability} (r = x) = \int_{x}^{x} p(r) dr $$ + $$ = P(r) \Big|_{x}^{x} $$ + $$ = P(x) - P(x) $$ + $$ = 0 $$ + +Finding the probability of a region surrounding x may not be zero: + + $$ \operatorname{Probability} (r | x - \Delta x < r < x + \Delta x ) = + \operatorname{area}(p(r), x - \Delta x, x + \Delta x) $$ + $$ = P(x + \Delta x) - P(x - \Delta x) $$ + + +Choosing our Samples +-------------------- +If we have a PDF for the function that we care about, then we have the probability that the function +will return a value within an arbitrary interval. We can use this to determine where we should +sample. Remember that this started as a quest to determine the best way to sample a scene so that we +wouldn't get very bright pixels next to very dark pixels. If we have a PDF for the scene, then we +can probabilistically steer our samples toward the light without making the image inaccurately +bright. We already said that if we steer our samples toward the light then we _will_ make the image +inaccurately bright. We need to figure out how to steer our samples without introducing this +inaccuracy, this will be explained a little bit later, but for now we'll focus on generating samples +if we have a PDF. How do we generate a random number with a PDF? For that we will need some more +machinery. Don’t worry -- this doesn’t go on forever! + +<div class='together'> +Our random number generator `random_double()` produces a random double between 0 and 1. The number +generator is uniform between 0 and 1, so any number between 0 and 1 has equal likelihood. If our PDF +is uniform over a domain, say $[0,10]$, then we can trivially produce perfect samples for this +uniform PDF with + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 10.0 * random_double() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +</div> + +That's an easy case, but the vast majority of cases that we're going to care about are nonuniform. +We need to figure out a way to convert a uniform random number generator into a nonuniform random +number generator, where the distribution is defined by the PDF. We'll just _assume_ that there +exists a function $f(d)$ that takes uniform input and produces a nonuniform distribution weighted by +PDF. We just need to figure out a way to solve for $f(d)$. + +For the PDF given above, where $p(r) = \frac{r}{2}$, the probability of a random sample is higher +toward 2 than it is toward 0. There is a greater probability of getting a number between 1.8 and 2.0 +than between 0.0 and 0.2. If we put aside our mathematics hat for a second and put on our computer +science hat, maybe we can figure out a smart way of partitioning the PDF. We know that there is a +higher probability near 2 than near 0, but what is the value that splits the probability in half? +What is the value that a random number has a 50% chance of being higher than and a 50% chance of +being lower than? What is the $x$ that solves: + + $$ 50\% = \int_{0}^{x} \frac{r}{2} dr = \int_{x}^{2} \frac{r}{2} dr $$ + +Solving gives us: + + $$ 0.5 = \frac{r^2}{4} \Big|_{0}^{x} $$ + $$ 0.5 = \frac{x^2}{4} $$ + $$ x^2 = 2$$ + $$ x = \sqrt{2}$$ + +As a crude approximation we could create a function `f(d)` that takes as input `double d = +random_double()`. If `d` is less than (or equal to) 0.5, it produces a uniform number in +$[0,\sqrt{2}]$, if `d` is greater than 0.5, it produces a uniform number in $[\sqrt{2}, 2]$. + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + double f(double d) + { + if (d <= 0.5) + return std::sqrt(2.0) * random_double(); + else + return std::sqrt(2.0) + (2 - std::sqrt(2.0)) * random_double(); + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [crude-approx]: A crude, first-order approximation to nonuniform PDF] + +<div class='together'> +While our initial random number generator was uniform from 0 to 1: + + ![Figure [uniform-dist]: A uniform distribution](../images/fig-3.05-uniform-dist.jpg) + +</div> + +<div class='together'> +Our, new, crude approximation for $\frac{r}{2}$ is nonuniform (but only just): + + ![Figure [approx-f]: A nonuniform distribution for $r/2$ + ](../images/fig-3.06-nonuniform-dist.jpg) + +</div> + +We had the analytical solution to the integration above, so we could very easily solve for the 50% +value. But we could also solve for this 50% value experimentally. There will be functions that we +either can't or don't want to solve for the integration. In these cases, we can get an experimental +result close to the real value. Let's take the function: + + $$ p(x) = e^{\frac{-x}{2 \pi}} \sin^2(x) $$ + +<div class='together'> +Which looks a little something like this: + + ![Figure [exp-sin2]: A function that we don't want to solve analytically + ](../images/fig-3.07-exp-sin2.jpg) + +</div> + +<div class='together'> +At this point you should be familiar with how to experimentally solve for the area under a curve. +We'll take our existing code and modify it slightly to get an estimate for the 50% value. We want to +solve for the $x$ value that gives us half of the total area under the curve. As we go along and +solve for the rolling sum over N samples, we're also going to store each individual sample alongside +its `p(x)` value. After we solve for the total sum, we'll sort our samples and add them up until we +have an area that is half of the total. From $0$ to $2\pi$ for example: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include "rtweekend.h" + + #include <algorithm> + #include <vector> + #include <iostream> + #include <iomanip> + + struct sample { + double x; + double p_x; + }; + + bool compare_by_x(const sample& a, const sample& b) { + return a.x < b.x; + } + + int main() { + const unsigned int N = 10000; + sample samples[N]; + double sum = 0.0; + + // Iterate through all of our samples. + + for (unsigned int i = 0; i < N; i++) { + // Get the area under the curve. + auto x = random_double(0, 2*pi); + auto sin_x = std::sin(x); + auto p_x = exp(-x / (2*pi)) * sin_x * sin_x; + sum += p_x; + + // Store this sample. + sample this_sample = {x, p_x}; + samples[i] = this_sample; + } + + // Sort the samples by x. + std::sort(std::begin(samples), std::end(samples), compare_by_x); + + // Find out the sample at which we have half of our area. + double half_sum = sum / 2.0; + double halfway_point = 0.0; + double accum = 0.0; + for (unsigned int i = 0; i < N; i++){ + accum += samples[i].p_x; + if (accum >= half_sum) { + halfway_point = samples[i].x; + break; + } + } + + std::cout << std::fixed << std::setprecision(12); + std::cout << "Average = " << sum / N << '\n'; + std::cout << "Area under curve = " << 2 * pi * sum / N << '\n'; + std::cout << "Halfway = " << halfway_point << '\n'; + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [est-halfway]: <kbd>[estimate_halfway.cc]</kbd> Estimating the 50% point of a function] + +</div> + +<div class='together'> +This code snippet isn't too different from what we had before. We're still solving for the sum over +an interval (0 to $2\pi$). Only this time, we're also storing and sorting all of our samples by +their input and output. We use this to determine the point at which they subtotal half of the sum +across the entire interval. Once we know that our first $j$ samples sum up to half of the total sum, +we know that the $j\text{th}$ $x$ roughly corresponds to our halfway point: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Average = 0.314686555791 + Area under curve = 1.977233943713 + Halfway = 2.016002314977 + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +</div> + +If you solve for the integral from $0$ to $2.016$ and from $2.016$ to $2\pi$ you should get almost +exactly the same result for both. + +We have a method of solving for the halfway point that splits a PDF in half. If we wanted to, we +could use this to create a nested binary partition of the PDF: + + 1. Solve for halfway point of a PDF + 2. Recurse into lower half, repeat step 1 + 3. Recurse into upper half, repeat step 1 + +Stopping at a reasonable depth, say 6–10. As you can imagine, this could be quite computationally +expensive. The computational bottleneck for the code above is probably sorting the samples. A naive +sorting algorithm can have an algorithmic complexity of $\mathcal{O}(\mathbf{n^2})$ time, which is +tremendously expensive. Fortunately, the sorting algorithm included in the standard library is +usually much closer to $\mathcal{O}(\mathbf{n\log{}n})$ time, but this can still be quite expensive, +especially for millions or billions of samples. But this will produce decent nonuniform +distributions of nonuniform numbers. This divide and conquer method of producing nonuniform +distributions is used somewhat commonly in practice, although there are much more efficient means of +doing so than a simple binary partition. If you have an arbitrary function that you wish to use as +the PDF for a distribution, you'll want to research the _Metropolis-Hastings Algorithm_. + + +Approximating Distributions +--------------------------- +This was a lot of math and work to build up a couple of notions. Let's return to our initial PDF. +For the intervals without an explicit probability, we assume the PDF to be zero. So for our example +from the beginning of the chapter, $p(r) = 0$, for $r \notin [0,2]$. We can rewrite our $p(r)$ in +piecewise fashion: + + $$ p(r)=\begin{cases} + 0 & r < 0 \\ + \frac{r}{2} & 0 \leq r \leq 2 \\ + 0 & 2 < r \\ + \end{cases} + $$ + +If you consider what we were trying to do in the previous section, a lot of math revolved around the +_accumulated_ area (or _accumulated_ probability) from zero. In the case of the function + + $$ f(x) = e^{\frac{-x}{2 \pi}} \sin^2(x) $$ + +we cared about the accumulated probability from $0$ to $2\pi$ (100%) and the accumulated probability +from $0$ to $2.016$ (50%). We can generalize this to an important term, the _Cumulative Distribution +Function_ $P(x)$ is defined as: + + $$ P(x) = \int_{-\infty}^{x} p(x') dx' $$ + +Or, + + $$ P(x) = \operatorname{area}(p(x'), -\infty, x) $$ + +Which is the amount of _cumulative_ probability from $-\infty$. We rewrote the integral in terms of +$x'$ instead of $x$ because of calculus rules, if you're not sure what it means, don't worry about +it, you can just treat it like it's the same. If we take the integration outlined above, we get the +piecewise $P(r)$: + + $$ P(r)=\begin{cases} + 0 & r < 0 \\ + \frac{r^2}{4} & 0 \leq r \leq 2 \\ + 1 & 2 < r \\ + \end{cases} + $$ + +The _Probability Density Function_ (PDF) is the probability function that explains how likely an +interval of numbers is to be chosen. The _Cumulative Distribution Function_ (CDF) is the +distribution function that explains how likely all numbers smaller than its input is to be chosen. +To go from the PDF to the CDF, you need to integrate from $-\infty$ to $x$, but to go from the CDF +to the PDF, all you need to do is take the derivative: + + $$ p(x) = \frac{d}{dx}P(x) $$ + +If we evaluate the CDF, $P(r)$, at $r = 1.0$, we get: + + $$ P(1.0) = \frac{1}{4} $$ + +This says _a random variable plucked from our PDF has a 25% chance of being 1 or lower_. We want a +function $f(d)$ that takes a uniform distribution between 0 and 1 (_i.e_ `f(random_double())`), and +returns a random value according to a distribution that has the CDF $P(x) = \frac{x^2}{4}$. We don’t +know yet know what the function $f(d)$ is analytically, but we do know that 25% of what it returns +should be less than 1.0, and 75% should be above 1.0. Likewise, we know that 50% of what it returns +should be less than $\sqrt{2}$, and 50% should be above $\sqrt{2}$. If $f(d)$ monotonically +increases, then we would expect $f(0.25) = 1.0$ and $f(0.5) = \sqrt{2}$. This can be generalized to +figure out $f(d)$ for every possible input: + + $$ f(P(x)) = x $$ + +Let's take some more samples: + + $$ P(0.0) = 0 $$ + $$ P(0.5) = \frac{1}{16} $$ + $$ P(1.0) = \frac{1}{4} $$ + $$ P(1.5) = \frac{9}{16} $$ + $$ P(2.0) = 1 $$ + +so, the function $f()$ has values + + $$ f(P(0.0)) = f(0) = 0 $$ + $$ f(P(0.5)) = f(\frac{1}{16}) = 0.5 $$ + $$ f(P(1.0)) = f(\frac{1}{4}) = 1.0 $$ + $$ f(P(1.5)) = f(\frac{9}{16}) = 1.5 $$ + $$ f(P(2.0)) = f(1) = 2.0 $$ + +We could use these intermediate values and interpolate between them to approximate $f(d)$: + + ![Figure [approx f]: Approximating the nonuniform f()](../images/fig-3.08-approx-f.jpg) + +If you can't solve for the PDF analytically, then you can't solve for the CDF analytically. After +all, the CDF is just the integral of the PDF. However, you can still create a distribution that +approximates the PDF. If you take a bunch of samples from the random function you want the PDF from, +you can approximate the PDF by getting a histogram of the samples and then converting to a PDF. +Alternatively, you can do as we did above and sort all of your samples. + +Looking closer at the equality: + + $$ f(P(x)) = x $$ + +That just means that $f()$ just undoes whatever $P()$ does. So, $f()$ is the inverse function: + + $$ f(d) = P^{-1}(x) $$ + +For our purposes, if we have PDF $p()$ and cumulative distribution function $P()$, we can use this +"inverse function" with a random number to get what we want: + + $$ f(d) = P^{-1} (\operatorname{random\_double}()) $$ + +For our PDF $p(r) = r/2$, and corresponding $P(r)$, we need to compute the inverse of $P(r)$. If we +have + + $$ y = \frac{r^2}{4} $$ + +we get the inverse by solving for $r$ in terms of $y$: + + $$ r = \sqrt{4y} $$ + +Which means the inverse of our CDF (which we'll call $ICD(x)$) is defined as + + $$ P^{-1}(r) = \operatorname{ICD}(r) = \sqrt{4y} $$ + +Thus our random number generator with density $p(r)$ can be created with: + + $$ \operatorname{ICD}(d) = \sqrt{4 \cdot \operatorname{random\_double}()} $$ + +Note that this ranges from 0 to 2 as we hoped, and if we check our work, we replace +`random_double()` with $1/4$ to get 1, and also replace with $1/2$ to get $\sqrt{2}$, just as +expected. + + +Importance Sampling +-------------------- +You should now have a decent understanding of how to take an analytical PDF and generate a function +that produces random numbers with that distribution. We return to our original integral and try it +with a few different PDFs to get a better understanding: + + $$ I = \int_{0}^{2} x^2 dx $$ + +<div class='together'> +The last time that we tried to solve for the integral we used a Monte Carlo approach, uniformly +sampling from the interval $[0, 2]$. We didn't know it at the time, but we were implicitly using a +uniform PDF between 0 and 2. This means that we're using a PDF = $1/2$ over the range $[0,2]$, which +means the CDF is $P(x) = x/2$, so $\operatorname{ICD}(d) = 2d$. Knowing this, we can make this +uniform PDF explicit: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include "rtweekend.h" + + #include <iostream> + #include <iomanip> + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + double icd(double d) { + return 2.0 * d; + } + + double pdf(double x) { + return 0.5; + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + int main() { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete + int a = 0; + int b = 2; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + int N = 1000000; + auto sum = 0.0; + + for (int i = 0; i < N; i++) { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + auto x = icd(random_double()); + sum += x*x / pdf(x); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } + std::cout << std::fixed << std::setprecision(12); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + std::cout << "I = " << (sum / N) << '\n'; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [integ-xsq-2]: <kbd>[integrate_x_sq.cc]</kbd> Explicit uniform PDF for $x^2$] + +</div> + +There are a couple of important things to emphasize. Every value of $x$ represents one sample of the +function $x^2$ within the distribution $[0, 2]$. We use a function $\operatorname{ICD}$ to randomly +select samples from within this distribution. We were previously multiplying the average over the +interval (`sum / N`) times the length of the interval (`b - a`) to arrive at the final answer. Here, +we don't need to multiply by the interval length--that is, we no longer need to multiply the average +by $2$. + +We need to account for the nonuniformity of the PDF of $x$. Failing to account for this +nonuniformity will introduce bias in our scene. Indeed, this bias is the source of our inaccurately +bright image. Accounting for the nonuniformity will yield accurate results. The PDF will "steer" +samples toward specific parts of the distribution, which will cause us to converge faster, but at +the cost of introducing bias. To remove this bias, we need to down-weight where we sample more +frequently, and to up-weight where we sample less frequently. For our new nonuniform random number +generator, the PDF defines how much or how little we sample a specific portion. So the weighting +function should be proportional to $1/\mathit{pdf}$. In fact it is _exactly_ $1/\mathit{pdf}$. This +is why we divide `x*x` by `pdf(x)`. + +We can try to solve for the integral using the linear PDF, $p(r) = \frac{r}{2}$, for which we were +able to solve for the CDF and its inverse, ICD. To do that, all we need to do is replace the +functions $\operatorname{ICD}(d) = \sqrt{4d}$ and $p(x) = x/2$. + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + double icd(double d) { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + return std::sqrt(4.0 * d); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } + + double pdf(double x) { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + return x / 2.0; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } + + int main() { + int N = 1000000; + auto sum = 0.0; + + for (int i = 0; i < N; i++) { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + auto z = random_double(); + if (z == 0.0) // Ignore zero to avoid NaNs + continue; + + auto x = icd(z); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + sum += x*x / pdf(x); + } + + std::cout << std::fixed << std::setprecision(12); + std::cout << "I = " << sum / N << '\n'; + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [integ-xsq-3]: <kbd>[integrate_x_sq.cc]</kbd> Integrating $x^2$ with linear PDF] + +If you compared the runs from the uniform PDF and the linear PDF, you would have probably found that +the linear PDF converged faster. If you think about it, a linear PDF is probably a better +approximation for a quadratic function than a uniform PDF, so you would expect it to converge +faster. If that's the case, then we should just try to make the PDF match the integrand by turning +the PDF into a quadratic function: + + $$ p(r)=\begin{cases} + 0 & r < 0 \\ + C \cdot r^2 & 0 \leq r \leq 2 \\ + 0 & 2 < r \\ + \end{cases} + $$ + +Like the linear PDF, we'll solve for the constant $C$ by integrating to 1 over the interval: + + $$ 1 = \int_{0}^{2} C \cdot r^2 dr $$ + $$ = C \cdot \int_{0}^{2} r^2 dr $$ + $$ = C \cdot \frac{r^3}{3} \Big|_{0}^{2} $$ + $$ = C ( \frac{2^3}{3} - \frac{0}{3} ) $$ + $$ C = \frac{3}{8} $$ + +Which gives us: + + $$ p(r)=\begin{cases} + 0 & r < 0 \\ + \frac{3}{8} r^2 & 0 \leq r \leq 2 \\ + 0 & 2 < r \\ + \end{cases} + $$ + +And we get the corresponding CDF: + + $$ P(r) = \frac{r^3}{8} $$ + +and + + $$ P^{-1}(x) = \operatorname{ICD}(d) = 8d^\frac{1}{3} $$ + +<div class='together'> +For just one sample we get: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + double icd(double d) { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + return 8.0 * std::pow(d, 1.0/3.0); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } + + double pdf(double x) { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + return (3.0/8.0) * x*x; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } + + int main() { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + int N = 1; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + auto sum = 0.0; + + for (int i = 0; i < N; i++) { + auto z = random_double(); + if (z == 0.0) // Ignore zero to avoid NaNs + continue; + + auto x = icd(z); + sum += x*x / pdf(x); + } + std::cout << std::fixed << std::setprecision(12); + std::cout << "I = " << sum / N << '\n'; + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [integ-xsq-5]: <kbd>[integrate_x_sq.cc]</kbd> Integrating $x^2$, final version] + +This always returns the exact answer. + +</div> + +A nonuniform PDF "steers" more samples to where the PDF is big, and fewer samples to where the PDF +is small. By this sampling, we would expect less noise in the places where the PDF is big and more +noise where the PDF is small. If we choose a PDF that is higher in the parts of the scene that have +higher noise, and is smaller in the parts of the scene that have lower noise, we'll be able to +reduce the total noise of the scene with fewer samples. This means that we will be able to converge +to the correct scene _faster_ than with a uniform PDF. In effect, we are steering our samples toward +the parts of the distribution that are more _important_. This is why using a carefully chosen +nonuniform PDF is usually called _importance sampling_. + +In all of the examples given, we always converged to the correct answer of $8/3$. We got the same +answer when we used both a uniform PDF and the "correct" PDF (that is, $\operatorname{ICD}(d) = +8d^{\frac{1}{3}}$). While they both converged to the same answer, the uniform PDF took much longer. +After all, we only needed a single sample from the PDF that perfectly matched the integral. This +should make sense, as we were choosing to sample the important parts of the distribution more often, +whereas the uniform PDF just sampled the whole distribution equally, without taking importance into +account. + +Indeed, this is the case for any PDF that you create--they will all converge eventually. This is +just another part of the power of the Monte Carlo algorithm. Even the naive PDF where we solved for +the 50% value and split the distribution into two halves: $[0, \sqrt{2}]$ and $[\sqrt{2}, 2]$. That +PDF will converge. Hopefully you should have an intuition as to why that PDF will converge faster +than a pure uniform PDF, but slower than the linear PDF (that is, $\operatorname{ICD}(d) = +\sqrt{4d}$). + +The perfect importance sampling is only possible when we already know the answer (we got $P$ by +integrating $p$ analytically), but it’s a good exercise to make sure our code works. + +Let's review the main concepts that underlie Monte Carlo ray tracers: + + 1. You have an integral of $f(x)$ over some domain $[a,b]$ + 2. You pick a PDF $p$ that is non-zero and non-negative over $[a,b]$ + 3. You average a whole ton of $\frac{f(r)}{p(r)}$ where $r$ is a random number with PDF $p$. + +Any choice of PDF $p$ will always converge to the right answer, but the closer that $p$ approximates +$f$, the faster that it will converge. + + + +Monte Carlo Integration on the Sphere of Directions +==================================================================================================== +In chapter One Dimensional Monte Carlo Integration we started with uniform random numbers and +slowly, over the course of a chapter, built up more and more complicated ways of producing random +numbers, before ultimately arriving at the intuition of PDFs, and how to use them to generate random +numbers of arbitrary distribution. + +All of the concepts covered in that chapter continue to work as we extend beyond a single dimension. +Moving forward, we might need to be able to select a point from a two, three, or even higher +dimensional space and then weight that selection by an arbitrary PDF. An important case of this--at +least for ray tracing--is producing a random direction. In the first two books we generated a random +direction by creating a random vector and rejecting it if it fell outside of the unit sphere. We +repeated this process until we found a random vector that fell inside the unit sphere. Normalizing +this vector produced points that lay exactly on the unit sphere and thereby represent a random +direction. This process of generating samples and rejecting them if they are not inside a desired +space is called _the rejection method_, and is found all over the literature. The method covered +in the last chapter is referred to as _the inversion method_ because we invert a PDF. + +Every direction in 3D space has an associated point on the unit sphere and can be generated by +solving for the vector that travels from the origin to that associated point. You can think of +choosing a random direction as choosing a random point in a constrained two dimensional plane: the +plane created by mapping the unit sphere to Cartesian coordinates. The same methodology as before +applies, but now we might have a PDF defined over two dimensions. Suppose we want to integrate this +function over the surface of the unit sphere: + + $$ f(\theta, \phi) = \cos^2(\theta) $$ + +Using Monte Carlo integration, we should just be able to sample $\cos^2(\theta) / p(r)$, where the +$p(r)$ is now just $p(direction)$. But what is _direction_ in that context? We could make it based +on polar coordinates, so $p$ would be in terms of $\theta$ and $\phi$ for $p(\theta, \phi)$. It +doesn't matter which coordinate system you choose to use. Although, however you choose to do it, +remember that a PDF must integrate to one over the whole surface and that the PDF represents the +_relative probability_ of that direction being sampled. Recall that we have a `vec3` function to +generate uniform random samples on the unit sphere $d$ (`random_unit_vector()`). What is the PDF +of these uniform samples? As a uniform density on the unit sphere, it is $1/\mathit{area}$ of the +sphere, which is $1/(4\pi)$. If the integrand is $\cos^2(\theta)$, and $\theta$ is the angle with +the $z$ axis, we can use scalar projection to re-write $\cos^2(\theta)$ in terms of the $d_z$: + + $$ d_z = \lVert d \rVert \cos \theta = 1 \cdot \cos \theta $$ + +We can then substitute $1 \cdot \cos \theta$ with $d_z$ giving us: + + $$ f(\theta, \phi) = \cos^2 (\theta) = {d_z}^2 $$ + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include "rtweekend.h" + + #include <iostream> + #include <iomanip> + + double f(const vec3& d) { + auto cosine_squared = d.z()*d.z(); + return cosine_squared; + } + + double pdf(const vec3& d) { + return 1 / (4*pi); + } + + int main() { + int N = 1000000; + auto sum = 0.0; + for (int i = 0; i < N; i++) { + vec3 d = random_unit_vector(); + auto f_d = f(d); + sum += f_d / pdf(d); + } + std::cout << std::fixed << std::setprecision(12); + std::cout << "I = " << sum / N << '\n'; + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [main-sphereimp]: <kbd>[sphere_importance.cc]</kbd> Generating importance-sampled points on the unit sphere] + +The analytic answer is $\frac{4}{3} \pi = 4.188790204786391$ -- if you remember enough advanced +calc, check me! And the code above produces that. The key point here is that all of the integrals +and the probability and everything else is over the unit sphere. The way to represent a single +direction in 3D is its associated point on the unit sphere. The way to represent a range of +directions in 3D is the amount of area on the unit sphere that those directions travel through. Call +it direction, area, or _solid angle_ -- it’s all the same thing. Solid angle is the term that you'll +usually find in the literature. You have radians (r) in $\theta$ over one dimension, and you have +_steradians_ (sr) in $\theta$ _and_ $\phi$ over two dimensions (the unit sphere is a three +dimensional object, but its surface is only two dimensional). Solid Angle is just the two +dimensional extension of angles. If you are comfortable with a two dimensional angle, great! If not, +do what I do and imagine the area on the unit sphere that a set of directions goes through. The +solid angle $\omega$ and the projected area $A$ on the unit sphere are the same thing. + + ![Figure [solid-angle]: Solid angle / projected area of a sphere + ](../images/fig-3.09-solid-angle.jpg) + +Now let’s go on to the light transport equation we are solving. + + + +Light Scattering +==================================================================================================== +In this chapter we won't actually program anything. We'll just be setting up for a big lighting +change in the next chapter. Our ray tracing program from the first two books scatters a ray when it +interacts with a surface or a volume. Ray scattering is the most commonly used model for simulating +light propagation through a scene. This can naturally be modeled probabilistically. There are many +things to consider when modeling the probabilistic scattering of rays. -For participation media (volumes), our albedo is usually called _scattering albedo_, and our -scattering PDF is usually called _phase function_. -</div> +Albedo +------- +First, is the light absorbed? +Probability of light being scattered: $A$ -Importance Sampling Materials -==================================================================================================== +Probability of light being absorbed: $1-A$ + +Where here $A$ stands for _albedo_. As covered in our first book, recall that albedo is a form of +fractional reflectance. It can help to stop and remember that when we simulate light propagation, +all we're doing is simulating the movement of photons through a space. If you remember your high +school Physics then you should recall that every photon has a unique energy and wavelength +associated by the Planck constant: + + $$ E = \frac{hc}{\lambda} $$ + +Each individual photon has a _tiny_ amount of energy, but when you add enough of them up you get all +of the illumination in your rendering. The absorption or scattering of a photon with a surface or a +volume (or really anything that a photon can interact with) is probabilistically determined by the +albedo of the object. Albedo can depend on color because some objects are more likely to absorb some +wavelengths. + +In most physically based renderers, we would use a predefined set of specific wavelengths for the +light color rather than RGB. As an example, we would replace our _tristimulus_ RGB renderer with +something that specifically samples at 300nm, 350nm, 400nm, ..., 700nm. We can extend our intuition +by thinking of R, G, and B as specific algebraic mixtures of wavelengths where R is _mostly_ red +wavelengths, G is _mostly_ green wavelengths, and B is _mostly_ blue wavelengths. This is an +approximation of the human visual system which has 3 unique sets of color receptors, called _cones_, +that are each sensitive to different algebraic mixtures of wavelengths, roughly RGB, but are +referred to as long, medium, and short cones (the names are in reference to the wavelengths that +each cone is sensitive to, not the length of the cone). Just as colors can be represented by their +strength in the RGB color space, colors can also be represented by how excited each set of cones is +in the _LMS color space_ (long, medium, short). + +Scattering +----------- +If the light does scatter, it will have a directional distribution that we can describe as a PDF +over solid angle. I will refer to this as its _scattering PDF_: $\operatorname{pScatter}()$. The +scattering PDF will vary with outgoing direction: $\operatorname{pScatter}(\omega_o)$. The +scattering PDF can also vary with _incident direction_: +$\operatorname{pScatter}(\omega_i, \omega_o)$. You can see this varying with incident direction when +you look at reflections off a road -- they become mirror-like as your viewing angle (incident angle) +approaches grazing. The scattering PDF can vary with the wavelength of the light: +$\operatorname{pScatter}(\omega_i, \omega_o, \lambda)$. A good example of this is a prism refracting +white light into a rainbow. Lastly, the scattering PDF can also depend on the scattering position: +$\operatorname{pScatter}(\mathbf{x}, \omega_i, \omega_o, \lambda)$. The $\mathbf{x}$ is just math +notation for the scattering position: $\mathbf{x} = (x, y, z)$. The albedo of an object can also +depend on these quantities: $A(\mathbf{x}, \omega_i, \omega_o, \lambda)$. + +The color of a surface is found by integrating these terms over the unit hemisphere by the incident +direction: + + $$ \operatorname{Color}_o(\mathbf{x}, \omega_o, \lambda) = \int_{\omega_i} + A(\mathbf{x}, \omega_i, \omega_o, \lambda) \cdot + \operatorname{pScatter}(\mathbf{x}, \omega_i, \omega_o, \lambda) \cdot + \operatorname{Color}_i(\mathbf{x}, \omega_i, \lambda) $$ + +We've added a $\operatorname{Color}_i$ term. The scattering PDF and the albedo at the surface of an +object are acting as filters to the light that is shining on that point. So we need to solve for the +light that is shining on that point. This is a recursive algorithm, and is the reason our +`ray_color` function returns the color of the current object multiplied by the color of the next +ray. + + +The Scattering PDF +------------------- +If we apply the Monte Carlo basic formula we get the following statistical estimate: + + $$ \operatorname{Color}_o(\mathbf{x}, \omega_o, \lambda) \approx \sum + \frac{A(\, \ldots \,) \cdot + \operatorname{pScatter}(\, \ldots \,) \cdot + \operatorname{Color}_i(\, \ldots \,)} + {p(\mathbf{x}, \omega_i, \omega_o, \lambda)} $$ + +where $p(\mathbf{x}, \omega_i, \omega_o, \lambda)$ is the PDF of whatever outgoing direction we +randomly generate. + +For a Lambertian surface we already implicitly implemented this formula for the special case where +$pScatter(\, \ldots \,)$ is a cosine density. The $\operatorname{pScatter}(\, \ldots \,)$ of a +Lambertian surface is proportional to $\cos(\theta_o)$, where $\theta_o$ is the angle relative to +the surface normal ($\theta_o \in [0,\pi]$). An angle of $0$ indicates an outgoing direction in the +same direction as the surface normal, and an angle of $\pi$ indicates an outgoing direction exactly +opposite the normal vector. + +Let's solve for $C$ once more: + + $$ \operatorname{pScatter}(\mathbf{x}, \omega_i, \omega_o, \lambda) = C \cdot \cos(\theta_o) $$ + +All two dimensional PDFs need to integrate to one over the whole surface (remember that +$\operatorname{pScatter}$ is a PDF). We set +$\operatorname{pScatter}(\frac{\pi}{2} < \theta_o \le \pi) = 0$ so that we don't scatter below the +horizon. Given this, we only need to integrate $\theta \in [0, \frac{\pi}{2}]$. + + $$ 1 = \int_{\phi = 0}^{2 \pi} \int_{\theta = 0}^\frac{\pi}{2} C \cdot \cos(\theta) dA $$ + +To integrate over the hemisphere, remember that in spherical coordinates: + + $$ dA = \sin(\theta) d\theta d\phi $$ + +So: + + $$ 1 = C \cdot \int_0^{2 \pi} \int_0^\frac{\pi}{2} + \cos(\theta) \sin(\theta) d\theta d\phi $$ + $$ 1 = C \cdot 2 \pi \frac{1}{2} $$ + $$ 1 = C \cdot \pi $$ + $$ C = \frac{1}{\pi} $$ + +The integral of $\cos(\theta_o)$ over the hemisphere is $\pi$, so we need to normalize by +$\frac{1}{\pi}$. The PDF $\operatorname{pScatter}$ is only dependent on outgoing direction +($\omega_o$), so we'll simplify its representation to just $\operatorname{pScatter}(\omega_o)$. Put +all of this together and you get the scattering PDF for a Lambertian surface: + + $$ \operatorname{pScatter}(\omega_o) = \frac{\cos(\theta_o)}{\pi} $$ + +We'll assume that the $p(\mathbf{x}, \omega_i, \omega_o, \lambda)$ is equal to the scattering PDF: + + $$ p(\omega_o) = \operatorname{pScatter}(\omega_o) = \frac{\cos(\theta_o)}{\pi} $$ + +The numerator and denominator cancel out, and we get: + + $$ \operatorname{Color}_o(\mathbf{x}, \omega_o, \lambda) \approx \sum + A(\, \ldots \,) \cdot + \operatorname{Color}_i(\, \ldots \,) $$ <div class='together'> -Our goal over the next two chapters is to instrument our program to send a bunch of extra rays -toward light sources so that our picture is less noisy. Let’s assume we can send a bunch of rays -toward the light source using a PDF $pLight(direction)$. Let’s also assume we have a PDF related to -$s$, and let’s call that $pSurface(direction)$. A great thing about PDFs is that you can just use -linear mixtures of them to form mixture densities that are also PDFs. For example, the simplest -would be: +This is exactly what we had in our original `ray_color()` function! + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + return attenuation * ray_color(scattered, depth-1, world); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - $$ p(direction) = \frac{1}{2}\cdotp Light(direction) + \frac{1}{2}\cdot pSurface(direction) $$ </div> -As long as the weights are positive and add up to one, any such mixture of PDFs is a PDF. Remember, -we can use any PDF: _all PDF eventually converge to the correct answer_. So, the game is to figure -out how to make the PDF larger where the product $s(direction) \cdot color(direction)$ is large. For -diffuse surfaces, this is mainly a matter of guessing where $color(direction)$ is high. +The treatment above is slightly non-standard because I want the same math to work for surfaces and +volumes. If you read the literature, you’ll see reflection defined by the _Bidirectional Reflectance +Distribution Function_ (BRDF). It relates pretty simply to our terms: -For a mirror, $s()$ is huge only near one direction, so it matters a lot more. Most renderers in -fact make mirrors a special case, and just make the $s/p$ implicit -- our code currently does that. + $$ BRDF(\omega_i, \omega_o, \lambda) = \frac{A(\mathbf{x}, \omega_i, \omega_o, \lambda) \cdot + \operatorname{pScatter}(\mathbf{x}, \omega_i, \omega_o, \lambda)}{\cos(\theta_o)} $$ -<div class='together'> -Let’s do a simple refactoring and temporarily remove all materials that aren’t Lambertian. We can -use our Cornell Box scene again, and let’s generate the camera in the function that generates the -model: +So for a Lambertian surface for example, $BRDF = A / \pi$. Translation between our terms and BRDF is +easy. For participating media (volumes), our albedo is usually called the _scattering albedo_, and +our scattering PDF is usually called the _phase function_. - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - hittable_list cornell_box(camera& cam, double aspect) { - hittable_list world; +All that we've done here is outline the PDF for the Lambertian scattering of a material. However, +we'll need to generalize so that we can send extra rays in important directions, such as toward the +lights. - auto red = make_shared<lambertian>(make_shared<constant_texture>(vec3(0.65, 0.05, 0.05))); - auto white = make_shared<lambertian>(make_shared<constant_texture>(vec3(0.73, 0.73, 0.73))); - auto green = make_shared<lambertian>(make_shared<constant_texture>(vec3(0.12, 0.45, 0.15))); - auto light = make_shared<diffuse_light>(make_shared<constant_texture>(vec3(15, 15, 15))); - world.add(make_shared<flip_face>(make_shared<yz_rect>(0, 555, 0, 555, 555, green))); - world.add(make_shared<yz_rect>(0, 555, 0, 555, 0, red)); - world.add(make_shared<flip_face>(make_shared<xz_rect>(213, 343, 227, 332, 554, light))); - world.add(make_shared<flip_face>(make_shared<xz_rect>(0, 555, 0, 555, 555, white))); - world.add(make_shared<xz_rect>(0, 555, 0, 555, 0, white)); - world.add(make_shared<flip_face>(make_shared<xy_rect>(0, 555, 0, 555, 555, white))); - shared_ptr<hittable> box1 = make_shared<box>(vec3(0,0,0), vec3(165,330,165), white); - box1 = make_shared<rotate_y>(box1, 15); - box1 = make_shared<translate>(box1, vec3(265,0,295)); - world.add(box1); +Playing with Importance Sampling +==================================================================================================== +Our goal over the next several chapters is to instrument our program to send a bunch of extra rays +toward light sources so that our picture is less noisy. Let’s assume we can send a bunch of rays +toward the light source using a PDF $\operatorname{pLight}(\omega_o)$. Let’s also assume we have a +PDF related to $\operatorname{pScatter}$, and let’s call that $\operatorname{pSurface}(\omega_o)$. A +great thing about PDFs is that you can just use linear mixtures of them to form mixture densities +that are also PDFs. For example, the simplest would be: - shared_ptr<hittable> box2 = make_shared<box>(vec3(0,0,0), vec3(165,165,165), white); - box2 = make_shared<rotate_y>(box2, -18); - box2 = make_shared<translate>(box2, vec3(130,0,65); - world.add(box2); + $$ p(\omega_o) = \frac{1}{2} \operatorname{pSurface}(\omega_o) + \frac{1}{2} + \operatorname{pLight}(\omega_o)$$ + +As long as the weights are positive and add up to one, any such mixture of PDFs is a PDF. Remember, +we can use any PDF: _all PDFs eventually converge to the correct answer_. So, the game is to figure +out how to make the PDF larger where the product - vec3 lookfrom(278, 278, -800); - vec3 lookat(278, 278, 0); - vec3 vup(0, 1, 0); - auto dist_to_focus = 10.0; - auto aperture = 0.0; - auto vfov = 40.0; - auto t0 = 0.0; - auto t1 = 1.0; + $$ \operatorname{pScatter}(\mathbf{x}, \omega_i, \omega_o) \cdot + \operatorname{Color}_i(\mathbf{x}, \omega_i) $$ - cam = camera(lookfrom, lookat, vup, vfov, aspect, aperture, dist_to_focus, t0, t1); +is largest. For diffuse surfaces, this is mainly a matter of guessing where +$\operatorname{Color}_i(\mathbf{x}, \omega_i)$ is largest. Which is equivalent to guessing where the +most light is coming from. - return world; +For a mirror, $\operatorname{pScatter}()$ is huge only near one direction, so +$\operatorname{pScatter}()$ matters a lot more. In fact, most renderers just make mirrors a special +case, and make the $\operatorname{pScatter}()/p()$ implicit -- our code currently does that. + + +Returning to the Cornell Box +----------------------------- +Let’s adjust some parameters for the Cornell box: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + int main() { + ... + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + cam.samples_per_pixel = 1000; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + ... } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [cornell-box]: <kbd>[main.cc]</kbd> Cornell box, refactored] -</div> <div class='together'> -At 500×500 my code produces this image in 10min on 1 core of my Macbook: +At 600×600 my code produces this image in 15min on 1 core of my Macbook: - <div class="render"> + ![<span class='num'>Image 3:</span> Cornell box, refactored + ](../images/img-3.03-cornell-refactor1.jpg class='pixel') - ![Cornell box, refactored](../images/img.cornell-refactor1.jpg) - - </div> +</div> Reducing that noise is our goal. We’ll do that by constructing a PDF that sends more rays to the light. -</div> First, let’s instrument the code so that it explicitly samples some PDF and then normalizes for -that. Remember MC basics: $\int f(x) \approx f(r)/p(r)$. For the Lambertian material, let’s sample -like we do now: $p(direction) = \cos(\theta) / \pi$. +that. Remember Monte Carlo basics: $\int f(x) \approx \sum f(r)/p(r)$. For the Lambertian material, +let’s sample like we do now: $p(\omega_o) = \cos(\theta_o) / \pi$. <div class='together'> -We modify the base-class `material` to enable this importance sampling: +We modify the base-class `material` to enable this importance sampling, and define the scattering +PDF function for Lambertian materials: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class material { - public: + public: + ... - virtual bool scatter( - const ray& r_in, const hit_record& rec, vec3& albedo, ray& scattered, double& pdf - ) const { - return false; - } - virtual double scattering_pdf( - const ray& r_in, const hit_record& rec, const ray& scattered - ) const { - return 0; - } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + virtual double scattering_pdf(const ray& r_in, const hit_record& rec, const ray& scattered) + const { + return 0; + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + }; - virtual vec3 emitted(double u, double v, const vec3& p) const { - return vec3(0,0,0); - } + class lambertian : public material { + public: + lambertian(const color& albedo) : tex(make_shared<solid_color>(albedo)) {} + lambertian(shared_ptr<texture> tex) : tex(tex) {} + + bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) + const override { + ... + } + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + double scattering_pdf(const ray& r_in, const hit_record& rec, const ray& scattered) + const override { + auto cos_theta = dot(rec.normal, unit_vector(scattered.direction())); + return cos_theta < 0 ? 0 : cos_theta/pi; + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + private: + shared_ptr<texture> tex; }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [class-material]: <kbd>[material.h]</kbd> - The material class, adding importance sampling] + [Listing [class-lambertian-impsample]: <kbd>[material.h]</kbd> Lambertian material, modified for importance sampling] + </div> <div class='together'> -And _Lambertian_ material becomes: +And the `camera::ray_color` function gets a minor modification: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - class lambertian : public material { - public: - lambertian(shared_ptr<texture> a) : albedo(a) {} - - bool scatter( - const ray& r_in, const hit_record& rec, vec3& alb, ray& scattered, double& pdf - ) const { - vec3 target = rec.p + rec.normal + random_unit_vector(); - scattered = ray(rec.p, unit_vector(target-rec.p), r_in.time()); - alb = albedo->value(rec.u, rec.v, rec.p); - pdf = dot(rec.normal, scattered.direction()) / pi; - return true; - } + class camera { + ... + private: + ... + color ray_color(const ray& r, int depth, const hittable& world) const { + // If we've exceeded the ray bounce limit, no more light is gathered. + if (depth <= 0) + return color(0,0,0); - double scattering_pdf( - const ray& r_in, const hit_record& rec, const ray& scattered - ) const { - auto cosine = dot(rec.normal, unit_vector(scattered.direction())); - return cosine < 0 ? 0 : cosine/pi; - } + hit_record rec; + + // If the ray hits nothing, return the background color. + if (!world.hit(r, interval(0.001, infinity), rec)) + return background; + + ray scattered; + color attenuation; + color color_from_emission = rec.mat->emitted(rec.u, rec.v, rec.p); + + if (!rec.mat->scatter(r, rec, attenuation, scattered)) + return color_from_emission; - public: - shared_ptr<texture> albedo; + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + double scattering_pdf = rec.mat->scattering_pdf(r, rec, scattered); + double pdf_value = scattering_pdf; + + color color_from_scatter = + (attenuation * scattering_pdf * ray_color(scattered, depth-1, world)) / pdf_value; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + return color_from_emission + color_from_scatter; + } }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [class-lambertian-impsample]: <kbd>[material.h]</kbd> - Lambertian material, modified for importance sampling] + [Listing [ray-color-impsample]: <kbd>[camera.h]</kbd> The ray_color function, modified for importance sampling] + </div> -<div class='together'> -And the color function gets a minor modification: +You should get exactly the same picture. Which _should make sense_, as the scattered part of +`ray_color` is getting multiplied by `scattering_pdf / pdf_value`, and as `pdf_value` is equal to +`scattering_pdf` is just the same as multiplying by one. + + +Using a Uniform PDF Instead of a Perfect Match +---------------------------------------------- +Now, just for the experience, let's try using a different sampling PDF. We'll continue to have our +reflected rays weighted by Lambertian, so $\cos(\theta_o)$, and we'll keep the scattering PDF as is, +but we'll use a different PDF in the denominator. We will sample using a uniform PDF about the +hemisphere, so we'll set the denominator to $1/2\pi$. This will still converge on the correct +answer, as all we've done is change the PDF, but since the PDF is now less of a perfect match for +the real distribution, it will take longer to converge. Which, for the same number of samples means +a noisier image: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - vec3 ray_color(const ray& r, const vec3& background, const hittable& world, int depth) { - hit_record rec; + class camera { + ... + private: + ... + color ray_color(const ray& r, int depth, const hittable& world) const { + hit_record rec; - // If we've exceeded the ray bounce limit, no more light is gathered. - if (depth <= 0) - return vec3(0,0,0); + // If we've exceeded the ray bounce limit, no more light is gathered. + if (depth <= 0) + return color(0,0,0); - // If the ray hits nothing, return the background color. - if (!world.hit(r, 0.001, infinity, rec)) - return background; + // If the ray hits nothing, return the background color. + if (!world.hit(r, interval(0.001, infinity), rec)) + return background; - ray scattered; - vec3 attenuation; - vec3 emitted = rec.mat_ptr->emitted(rec.u, rec.v, rec.p); + ray scattered; + color attenuation; + color color_from_emission = rec.mat->emitted(rec.u, rec.v, rec.p); + if (!rec.mat->scatter(r, rec, attenuation, scattered)) + return color_from_emission; + + double scattering_pdf = rec.mat->scattering_pdf(r, rec, scattered); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - double pdf; - vec3 albedo; + double pdf_value = 1 / (2*pi); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - if (!rec.mat_ptr->scatter(r, rec, albedo, scattered, pdf)) - return emitted; + color color_from_scatter = + (attenuation * scattering_pdf * ray_color(scattered, depth-1, world)) / pdf_value; - return emitted - + albedo * rec.mat_ptr->scattering_pdf(r, rec, scattered) - * ray_color(scattered, background, world, depth-1) / pdf; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - } + return color_from_emission + color_from_scatter; + } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [ray-color-impsample]: <kbd>[main.cc]</kbd> - The ray_color function, modified for importance sampling] -</div> + [Listing [ray-color-uniform]: <kbd>[camera.h]</kbd> The ray_color function, now with a uniform PDF in the denominator] -You should get exactly the same picture. +You should get a very similar result to before, only with slightly more noise, it may be hard to +see. -<div class='together'> -Now, just for the experience, try a different sampling strategy. As in the first book, Let’s choose -randomly from the hemisphere above the surface. This would be $p(direction) = \frac{1}{2\pi}$. - - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - bool scatter( - const ray& r_in, const hit_record& rec, vec3& alb, ray& scattered, double& pdf - ) const { - vec3 direction = random_in_hemisphere(rec.normal); - scattered = ray(rec.p, unit_vector(direction), r_in.time()); - alb = albedo->value(rec.u, rec.v, rec.p); - pdf = 0.5 / pi; - return true; - } - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [scatter-mod]: <kbd>[material.h]</kbd> Modified scatter function] -</div> + ![<span class='num'>Image 4:</span> Cornell box, with imperfect PDF + ](../images/img-3.04-cornell-imperfect.jpg class='pixel') -<div class='together'> -And again I _should_ get the same picture except with different variance, but I don’t! +Make sure to return the PDF to the scattering PDF. + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + ... + + double scattering_pdf = rec.mat->scattering_pdf(r, rec, scattered); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + double pdf_value = scattering_pdf; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - <div class="render"> + ... + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [ray-color-return]: <kbd>[camera.h]</kbd> Return the PDF to the same as scattering PDF] + + +Random Hemispherical Sampling +--------------------------- +To confirm our understanding, let's try a different scattering distribution. For this one, we'll +attempt to repeat the uniform hemispherical scattering from the first book. There's nothing wrong +with this technique, but we are no longer treating our objects as Lambertian. Lambertian is a +specific type of diffuse material that requires a $\cos(\theta_o)$ scattering distribution. Uniform +hemispherical scattering is a different diffuse material. If we keep the material the same but +change the PDF, as we did in last section, we will still converge on the same answer, but our +convergence may take more or less samples. However, if we change the material, we will have +fundamentally changed the render and the algorithm will converge on a different answer. So when we +replace Lambertian diffuse with uniform hemispherical diffuse we should expect the outcome of our +render to be _materially_ different. We're going to adjust our scattering direction and scattering +PDF: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class lambertian : public material { + public: + lambertian(const color& albedo) : tex(make_shared<solid_color>(albedo)) {} + lambertian(shared_ptr<texture> tex) : tex(tex) {} - ![Cornell box, with different sampling strategy](../images/img.cornell-refactor2.jpg) + bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) + const override { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + auto scatter_direction = random_on_hemisphere(rec.normal); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - </div> + // Catch degenerate scatter direction + if (scatter_direction.near_zero()) + scatter_direction = rec.normal; -</div> + scattered = ray(rec.p, scatter_direction, r_in.time()); + attenuation = tex->value(rec.u, rec.v, rec.p); + return true; + } + + double scattering_pdf(const ray& r_in, const hit_record& rec, const ray& scattered) + const override { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + return 1 / (2*pi); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } + + ... + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [scatter-mod]: <kbd>[material.h]</kbd> Modified PDF and scatter function] + +This new diffuse material is actually just $p(\omega_o) = \frac{1}{2\pi}$ for the scattering PDF. So +our uniform PDF that was an imperfect match for Lambertian diffuse is actually a perfect match for +our uniform hemispherical diffuse. When rendering, we should get a slightly different image. -It’s pretty close to our old picture, but there are differences that are not noise. The front of the -tall box is much more uniform in color. So I have the most difficult kind of bug to find in a Monte -Carlo program -- a bug that produces a reasonable looking image. I also don’t know if the bug is the -first version of the program, or the second, or both! + ![<span class='num'>Image 5:</span> Cornell box, with uniform hemispherical sampling + ](../images/img-3.05-cornell-uniform-hemi.jpg class='pixel') + +It’s pretty close to our old picture, but there are differences that are not just noise. The front +of the tall box is much more uniform in color. If you aren't sure what the best sampling pattern for +your material is, it's pretty reasonable to just go ahead and assume a uniform PDF, and while that +might converge slowly, it's not going to ruin your render. That said, if you're not sure what the +correct sampling pattern for your material is, your choice of PDF is not going to be your biggest +concern, as incorrectly choosing your scattering function _will_ ruin your render. At the very least +it will produce an incorrect result. You may find yourself with the most difficult kind of bug to +find in a Monte Carlo program -- a bug that produces a reasonable looking image! You won’t know if +the bug is in the first version of the program, or the second, or both! Let’s build some infrastructure to address this. @@ -914,485 +1928,764 @@ Generating Random Directions ==================================================================================================== +In this and the next two chapters, we'll harden our understanding and our tools. -In this and the next two chapters, let’s harden our understanding and tools and figure out which -Cornell Box is right. Let’s first figure out how to generate random directions, but to simplify -things let’s assume the z-axis is the surface normal and $\theta$ is the angle from the normal. -We’ll get them oriented to the surface normal vector in the next chapter. We will only deal with -distributions that are rotationally symmetric about $z$. So $p(direction) = f(\theta)$. If you have -had advanced calculus, you may recall that on the sphere in spherical coordinates $dA = \sin(\theta) -\cdot d\theta \cdot d\phi$. If you haven’t, you’ll have to take my word for the next step, but -you’ll get it when you take advanced calc. -<div class='together'> -Given a directional PDF, $p(direction) = f(\theta)$ on the sphere, the 1D PDFs on $\theta$ and -$\phi$ are: +Random Directions Relative to the Z Axis +----------------------------------------- +Let’s first figure out how to generate random directions. We already have a method to generate +random directions using the rejection method, so let's create one using the inversion method. To +simplify things, assume the $z$ axis is the surface normal, and $\theta$ is the angle from the +normal. We'll set everything up in terms of the $z$ axis this chapter. Next chapter we’ll get them +oriented to the surface normal vector. We will only deal with distributions that are rotationally +symmetric about $z$. So $p(\omega) = f(\theta)$. - $$ a(\phi) = \frac{1}{2\pi} $$ -(uniform) - $$ b(\theta) = 2\pi f(\theta)\sin(\theta) $$ -</div> +Given a directional PDF on the sphere (where $p(\omega) = f(\theta)$), the one dimensional PDFs on +$\theta$ and $\phi$ are: -<div class='together'> -For uniform random numbers $r_1$ and $r_2$, the material presented in the -One Dimensional MC Integration chapter leads to: + $$ a(\phi) = \frac{1}{2\pi} $$ + $$ b(\theta) = 2\pi f(\theta)\sin(\theta) $$ - $$ r_1 = \int_{0}^{\phi} \frac{1}{2\pi} dt = \frac{\phi}{2\pi} $$ +For uniform random numbers $r_1$ and $r_2$, we solve for the CDF of $\theta$ and $\phi$ so that we +can invert the CDF to derive the random number generator. -Solving for $\phi$ we get: + $$ r_1 = \int_{0}^{\phi} a(\phi') d\phi' $$ + $$ = \int_{0}^{\phi} \frac{1}{2\pi} d\phi' $$ + $$ = \frac{\phi}{2\pi} $$ - $$ \phi = 2 \pi \cdot r_1 $$ +Invert to solve for $\phi$: -For $\theta$ we have: + $$ \phi = 2 \pi \cdot r_1 $$ - $$ r_2 = \int_{0}^{\theta} 2 \pi f(t) \sin(t) dt $$ -</div> +This should match with your intuition. To solve for a random $\phi$ you can take a uniform random +number in the interval [0,1] and multiply by $2\pi$ to cover the full range of all possible $\phi$ +values, which is just [0,$2\pi$]. You may not have a fully formed intuition for how to solve for a +random value of $\theta$, so let's walk through the math to help you get set up. We rewrite $\phi$ +as $\phi'$ and $\theta$ as $\theta'$ just like before, as a formality. For $\theta$ we have: -<div class='together'> -Here, $t$ is a dummy variable. Let’s try some different functions for $f()$. Let’s first try a -uniform density on the sphere. The area of the unit sphere is $4\pi$, so a uniform $p(direction) = -\frac{1}{4\pi}$ on the unit sphere. + $$ r_2 = \int_{0}^{\theta} b(\theta') d\theta' $$ + $$ = \int_{0}^{\theta} 2 \pi f(\theta') \sin(\theta') d\theta' $$ - $$ r_2 = \int_{0}^{\theta} 2 \pi \frac{1}{4\pi} \sin(t) dt $$ - $$ = \int_{0}^{\theta} \frac{1}{2} \sin(t) dt $$ - $$ = \frac{-\cos(\theta)}{2} - \frac{-\cos(0)}{2} $$ - $$ = \frac{1 - \cos(\theta)}{2} $$ +Let’s try some different functions for $f()$. Let’s first try a uniform density on the sphere. The +area of the unit sphere is $4\pi$, so a uniform $p(\omega) = \frac{1}{4\pi}$ on the unit sphere. + + $$ r_2 = \int_{0}^{\theta} 2 \pi \frac{1}{4\pi} \sin(\theta') d\theta' $$ + $$ = \int_{0}^{\theta} \frac{1}{2} \sin(\theta') d\theta' $$ + $$ = \frac{-\cos(\theta)}{2} - \frac{-\cos(0)}{2} $$ + $$ = \frac{1 - \cos(\theta)}{2} $$ Solving for $\cos(\theta)$ gives: - $$ \cos(\theta) = 1 - 2 r_2 $$ + $$ \cos(\theta) = 1 - 2 r_2 $$ We don’t solve for theta because we probably only need to know $\cos(\theta)$ anyway, and don’t want needless $\arccos()$ calls running around. -</div> -<div class='together'> To generate a unit vector direction toward $(\theta,\phi)$ we convert to Cartesian coordinates: - $$ x = \cos(\phi) \cdot \sin(\theta) $$ - $$ y = \sin(\phi) \cdot \sin(\theta) $$ - $$ z = \cos(\theta) $$ + $$ x = \cos(\phi) \cdot \sin(\theta) $$ + $$ y = \sin(\phi) \cdot \sin(\theta) $$ + $$ z = \cos(\theta) $$ -And using the identity that $\cos^2 + \sin^2 = 1$, we get the following in terms of random -$(r_1,r_2)$: +And using the identity $\cos^2 + \sin^2 = 1$, we get the following in terms of random $(r_1,r_2)$: - $$ x = \cos(2\pi \cdot r_1)\sqrt{1 - (1-2 r_2)^2} $$ - $$ y = \sin(2\pi \cdot r_1)\sqrt{1 - (1-2 r_2)^2} $$ - $$ z = 1 - 2 r_2 $$ + $$ x = \cos(2\pi \cdot r_1)\sqrt{1 - (1-2 r_2)^2} $$ + $$ y = \sin(2\pi \cdot r_1)\sqrt{1 - (1-2 r_2)^2} $$ + $$ z = 1 - 2 r_2 $$ Simplifying a little, $(1 - 2 r_2)^2 = 1 - 4r_2 + 4r_2^2$, so: - $$ x = \cos(2 \pi r_1) \cdot 2 \sqrt{r_2(1 - r_2)} $$ - $$ y = \sin(2 \pi r_1) \cdot 2 \sqrt{r_2(1 - r_2)} $$ - $$ z = 1 - 2 r_2 $$ -</div> + $$ x = \cos(2 \pi r_1) \cdot 2 \sqrt{r_2(1 - r_2)} $$ + $$ y = \sin(2 \pi r_1) \cdot 2 \sqrt{r_2(1 - r_2)} $$ + $$ z = 1 - 2 r_2 $$ <div class='together'> We can output some of these: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include "rtweekend.h" + + #include <iostream> + #include <math.h> + int main() { for (int i = 0; i < 200; i++) { auto r1 = random_double(); auto r2 = random_double(); - auto x = cos(2*pi*r1)*2*sqrt(r2*(1-r2)); - auto y = sin(2*pi*r1)*2*sqrt(r2*(1-r2)); + auto x = std::cos(2*pi*r1) * 2 * std::sqrt(r2*(1-r2)); + auto y = std::sin(2*pi*r1) * 2 * std::sqrt(r2*(1-r2)); auto z = 1 - 2*r2; std::cout << x << " " << y << " " << z << '\n'; } } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [rand-unit-sphere-plot]: <kbd>[sphere_plot.cc]</kbd> Random points on the unit sphere] + </div> <div class='together'> And plot them for free on plot.ly (a great site with 3D scatterplot support): - ![Figure [pt-uni-sphere]: Random points on the unit sphere](../images/fig.pt-uni-sphere.jpg) + ![Figure [rand-pts-sphere]: Random points on the unit sphere + ](../images/fig-3.10-rand-pts-sphere.jpg) + +On the plot.ly website you can rotate that around and see that it appears uniform. </div> -On the plot.ly website you can rotate that around and see that it appears uniform. -<div class='together'> +Uniform Sampling a Hemisphere +------------------------------ Now let’s derive uniform on the hemisphere. The density being uniform on the hemisphere means -$p(direction) = \frac{1}{2\pi}$. Just changing the constant in the theta equations yields: +$p(\omega) = f(\theta) = \frac{1}{2\pi}$. Just changing the constant in the theta equations yields: - $$ \cos(\theta) = 1 - r_2 $$ -</div> + $$ r_2 = \int_{0}^{\theta} b(\theta') d\theta' $$ + $$ = \int_{0}^{\theta} 2 \pi f(\theta') \sin(\theta') d\theta' $$ + $$ = \int_{0}^{\theta} 2 \pi \frac{1}{2\pi} \sin(\theta') d\theta' $$ + $$ \ldots $$ + $$ \cos(\theta) = 1 - r_2 $$ -<div class='together'> -It is comforting that $\cos(\theta)$ will vary from 1 to 0, and thus theta will vary from 0 to -$\pi/2$. Rather than plot it, let’s do a 2D integral with a known solution. Let’s integrate cosine -cubed over the hemisphere (just picking something arbitrary with a known solution). First let’s do -it by hand: - - $$ \int \cos^3(\theta) dA $$ - $$ = \int_{0}^{2 \pi} \int_{0}^{\pi /2} \cos^3(\theta) \sin(\theta) d\theta d\phi $$ - $$ = 2 \pi \int_{0}^{\pi/2} \cos^3(\theta) \sin(\theta) = \frac{\pi}{2} $$ -</div> +This means that $\cos(\theta)$ will vary from 1 to 0, so $\theta$ will vary from 0 to $\pi/2$, which +means that nothing will go below the horizon. Rather than plot it, we'll solve for a 2D integral +with a known solution. Let’s integrate cosine cubed over the hemisphere (just picking something +arbitrary with a known solution). First we'll solve the integral by hand: + + $$ \int_\omega \cos^3(\theta) dA $$ + $$ = \int_{0}^{2 \pi} \int_{0}^{\pi /2} \cos^3(\theta) \sin(\theta) d\theta d\phi $$ + $$ = 2 \pi \int_{0}^{\pi/2} \cos^3(\theta) \sin(\theta) d\theta = \frac{\pi}{2} $$ <div class='together'> -Now for integration with importance sampling. $p(direction) = \frac{1}{2\pi}$, so we average $f/p$ -which is $\cos^3(\theta) / (1/(2 \pi))$, and we can test this: +Now for integration with importance sampling. $p(\omega) = \frac{1}{2\pi}$, so we average +$f()/p() = \cos^3(\theta) / \frac{1}{2\pi}$, and we can test this: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include "rtweekend.h" + + #include <iostream> + #include <iomanip> + + double f(double r2) { + // auto x = std::cos(2*pi*r1) * 2 * std::sqrt(r2*(1-r2)); + // auto y = std::sin(2*pi*r1) * 2 * std::sqrt(r2*(1-r2)); + auto z = 1 - r2; + double cos_theta = z; + return cos_theta*cos_theta*cos_theta; + } + + double pdf() { + return 1.0 / (2.0*pi); + } + int main() { int N = 1000000; + auto sum = 0.0; for (int i = 0; i < N; i++) { - auto r1 = random_double(); auto r2 = random_double(); - auto x = cos(2*pi*r1)*2*sqrt(r2*(1-r2)); - auto y = sin(2*pi*r1)*2*sqrt(r2*(1-r2)); - auto z = 1 - r2; - sum += z*z*z / (1.0/(2.0*pi)); + sum += f(r2) / pdf(); } + std::cout << std::fixed << std::setprecision(12); - std::cout << "Pi/2 = " << pi/2 << '\n'; - std::cout << "Estimate = " << sum/N << '\n'; + std::cout << "PI/2 = " << pi / 2.0 << '\n'; + std::cout << "Estimate = " << sum / N << '\n'; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [cos-cubed]: <kbd>[cos_cubed.cc]</kbd> Integration using $cos^3(x)$] + [Listing [cos-cubed]: <kbd>[cos_cubed.cc]</kbd> Integration using $\cos^3(x)$] + </div> -<div class='together'> -Now let’s generate directions with $p(directions) = \cos(\theta) / \pi$. - $$ r_2 = \int_{0}^{\theta} 2 \pi \frac{\cos(t)}{\pi} \sin(t) = 1 - \cos^2(\theta) $$ +Cosine Sampling a Hemisphere +------------------------------ +We'll now continue trying to solve for cosine cubed over the horizon, but we'll change our PDF to +generate directions with $p(\omega) = f(\theta) = \cos(\theta) / \pi$. + + $$ r_2 = \int_{0}^{\theta} b(\theta') d\theta' $$ + $$ = \int_{0}^{\theta} 2 \pi f(\theta') \sin(\theta') d\theta' $$ + $$ = \int_{0}^{\theta} 2 \pi \frac{\cos(\theta')}{\pi} \sin(\theta') d\theta' $$ + $$ = 1 - \cos^2(\theta) $$ So, - $$ \cos(\theta) = \sqrt{1 - r_2} $$ + $$ \cos(\theta) = \sqrt{1 - r_2} $$ We can save a little algebra on specific cases by noting - $$ z = \cos(\theta) = \sqrt{1 - r_2} $$ - $$ x = \cos(\phi) \sin(\theta) = \cos(2 \pi r_1) \sqrt{1 - z^2} = \cos(2 \pi r_1) \sqrt{r_2} $$ - $$ y = \sin(\phi) \sin(\theta) = \sin(2 \pi r_1) \sqrt{1 - z^2} = \sin(2 \pi r_1) \sqrt{r_2} $$ -</div> + $$ z = \cos(\theta) = \sqrt{1 - r_2} $$ + $$ x = \cos(\phi) \sin(\theta) = \cos(2 \pi r_1) \sqrt{1 - z^2} = \cos(2 \pi r_1) \sqrt{r_2} $$ + $$ y = \sin(\phi) \sin(\theta) = \sin(2 \pi r_1) \sqrt{1 - z^2} = \sin(2 \pi r_1) \sqrt{r_2} $$ <div class='together'> -Let’s also start generating them as random vectors: +Here's a function that generates random vectors weighted by this PDF: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - #include "rtweekend.h" - - #include <iostream> - #include <math.h> - inline vec3 random_cosine_direction() { auto r1 = random_double(); auto r2 = random_double(); - auto z = sqrt(1-r2); auto phi = 2*pi*r1; - auto x = cos(phi)*sqrt(r2); - auto y = sin(phi)*sqrt(r2); + auto x = std::cos(phi) * std::sqrt(r2); + auto y = std::sin(phi) * std::sqrt(r2); + auto z = std::sqrt(1-r2); return vec3(x, y, z); } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [random-cosine-direction]: <kbd>[vec3.h]</kbd> Random cosine direction utility function] + +</div> + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include "rtweekend.h" + + #include <iostream> + #include <iomanip> + + double f(const vec3& d) { + auto cos_theta = d.z(); + return cos_theta*cos_theta*cos_theta; + } + + double pdf(const vec3& d) { + return d.z() / pi; + } int main() { int N = 1000000; auto sum = 0.0; for (int i = 0; i < N; i++) { - vec3 v = random_cosine_direction(); - sum += v.z()*v.z()*v.z() / (v.z()/pi); + vec3 d = random_cosine_direction(); + sum += f(d) / pdf(d); } std::cout << std::fixed << std::setprecision(12); - std::cout << "Pi/2 = " << pi/2 << '\n'; - std::cout << "Estimate = " << sum/N << '\n'; + std::cout << "PI/2 = " << pi / 2.0 << '\n'; + std::cout << "Estimate = " << sum / N << '\n'; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [cos-density]: <kbd>[cos_density.cc]</kbd> Integration with cosine density function] -</div> -We can generate other densities later as we need them. In the next chapter we’ll get them aligned to -the surface normal vector. +We can generate other densities later as we need them. This `random_cosine_direction()` function +produces a random direction weighted by $\cos(\theta)$ where $\theta$ is the angle from the $z$ +axis. -Ortho-normal Bases +Orthonormal Bases ==================================================================================================== - -In the last chapter we developed methods to generate random directions relative to the Z-axis. We’d -like to be able to do that relative to a surface normal vector. - -An ortho-normal basis (ONB) is a collection of -three mutually orthogonal unit vectors. The Cartesian XYZ axes are one such ONB, and I sometimes -forget that it has to sit in some real place with real orientation to have meaning in the real -world, and some virtual place and orientation in the virtual world. A picture is a result of the -relative positions/orientations of the camera and scene, so as long as the camera and scene are -described in the same coordinate system, all is well. +In the last chapter we developed methods to generate random directions relative to the $z$ axis. If +we want to be able to produce reflections off of any surface, we are going to need to make this more +general: Not all normals are going to be perfectly aligned with the $z$ axis. So in this chapter we +are going to generalize our methods so that they support arbitrary surface normal vectors. + + +Relative Coordinates +--------------------- +An _orthonormal basis_ (ONB) is a collection of three mutually orthogonal unit vectors. It is a +strict subtype of coordinate system. The Cartesian $xyz$ axes are one example of an orthonormal +basis. All of our renders are the result of the relative positions and orientations of the objects +in a scene projected onto the image plane of the camera. The camera and objects must be described in +the same coordinate system, so that the projection onto the image plane is logically defined, +otherwise the camera has no definitive means of correctly rendering the objects. Either the camera +must be redefined in the objects' coordinate system, or the objects must be redefined in the +camera's coordinate system. It's best to start with both in the same coordinate system, so no +redefinition is necessary. So long as the camera and scene are described in the same coordinate +system, all is well. The orthonormal basis defines how distances and orientations are represented in +the space, but an orthonormal basis alone is not enough. The objects and the camera need to +described by their displacement from a mutually defined location. This is just the origin +$\mathbf{O}$ of the scene; it represents the center of the universe for everything to displace from. + +Suppose we have an origin $\mathbf{O}$ and Cartesian unit vectors $\mathbf{x}$, $\mathbf{y}$, and +$\mathbf{z}$. When we say a location is (3,-2,7), we really are saying: + + $$ \text{Location is } \mathbf{O} + 3\mathbf{x} - 2\mathbf{y} + 7\mathbf{z} $$ + +If we want to measure coordinates in another coordinate system with origin $\mathbf{O}'$ and basis +vectors $\mathbf{u}$, $\mathbf{v}$, and $\mathbf{w}$, we can just find the numbers $(u,v,w)$ such +that: + + $$ \text{Location is } \mathbf{O}' + u\mathbf{u} + v\mathbf{v} + w\mathbf{w} $$ + + +Generating an Orthonormal Basis +-------------------------------- +If you take an intro to graphics course, there will be a lot of time spent on coordinate systems and +4×4 coordinate transformation matrices. Pay attention, it’s really important stuff! But we won’t be +needing it for this book and we'll make do without it. What we do need is to generate random +directions with a set distribution relative to the surface normal vector $\mathbf{n}$. We won’t be +needing an origin for this because a direction is relative and has no specific origin. To start off +with, we need two cotangent vectors that are each perpendicular to $\mathbf{n}$ and that are also +perpendicular to each other. + +Some 3D object models will come with one or more cotangent vectors for each vertex. If our model has +only one cotangent vector, then the process of making an ONB is a nontrivial one. Suppose we have +any vector $\mathbf{a}$ that is of nonzero length and nonparallel with $\mathbf{n}$. We can get +vectors $\mathbf{s}$ and $\mathbf{t}$ perpendicular to $\mathbf{n}$ by using the property of the +cross product that $\mathbf{n} \times \mathbf{a}$ is perpendicular to both $\mathbf{n}$ and +$\mathbf{a}$: + + $$ \mathbf{s} = \operatorname{unit\_vector}(\mathbf{n} \times \mathbf{a}) $$ + + $$ \mathbf{t} = \mathbf{n} \times \mathbf{s} $$ <div class='together'> -Suppose we have an origin $\mathbf{o}$ and cartesian unit vectors $\vec{\mathbf{x}}$, -$\vec{\mathbf{y}}$, and $\vec{\mathbf{z}}$. When we say a location is (3, -2, 7), we really are -saying: +This is all well and good, but the catch is that we may not be given an $\mathbf{a}$ when we load a +model, and our current program doesn't have a way to generate one. If we went ahead and picked an +arbitrary $\mathbf{a}$ to use as an initial vector we may get an $\mathbf{a}$ that is parallel to +$\mathbf{n}$. So a common method is to pick an arbitrary axis and check to see if it's parallel to +$\mathbf{n}$ (which we assume to be of unit length), if it is, just use another axis: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + if (std::fabs(n.x()) > 0.9) + a = vec3(0, 1, 0) + else + a = vec3(1, 0, 0) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - $$ \text{Location is } \mathbf{o} + 3\vec{\mathbf{x}} - 2\vec{\mathbf{y}} + 7\vec{\mathbf{z}} $$ </div> <div class='together'> -If we want to measure coordinates in another coordinate system with origin $\mathbf{o}'$ and basis -vectors $\vec{\mathbf{u}}$, $\vec{\mathbf{v}}$, and $\vec{\mathbf{w}}$, we can just find the numbers -$(u, v, w)$ such that: +We then take the cross product to get $\mathbf{s}$ and $\mathbf{t}$ + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + vec3 s = unit_vector(cross(n, a)); + vec3 t = cross(n, s); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - $$ \text{Location is } \mathbf{o}' + u\vec{\mathbf{u}} + v\vec{\mathbf{v}} + w\vec{\mathbf{w}} $$ </div> -If you take an intro graphics course, there will be a lot of time spent on coordinate systems and -4×4 coordinate transformation matrices. Pay attention, it’s important stuff in graphics! But we -won’t need it. What we need to is generate random directions with a set distribution relative to -$\vec{\mathbf{n}}$. We don’t need an origin because a direction is relative to no specified origin. -We do need two cotangent vectors that are mutually perpendicular to $\vec{\mathbf{n}}$, and to each -other. +Note that we don't need to take the unit vector for $\mathbf{t}$. Since $\mathbf{n}$ and +$\mathbf{s}$ are both unit vectors, their cross product $\mathbf{t}$ will be also. Once we have an +ONB of $\mathbf{s}$, $\mathbf{t}$, and $\mathbf{n}$, and we have a random $(x,y,z)$ relative to the +$z$ axis, we can get the vector relative to $\mathbf{n}$ with: -<div class='together'> -Some models will come with one or more cotangent vectors. If our model has only one cotangent -vector, then the process of making an ONB is a nontrivial one. Suppose we have any vector -$\vec{\mathbf{a}}$ that is of nonzero length and not parallel to $\vec{\mathbf{n}}$. We can get -vectors $\vec{\mathbf{s}}$ and $\vec{\mathbf{t}}$ perpendicular to $\vec{\mathbf{n}}$ by using the -property of the cross product that $\vec{\mathbf{c}} \times \vec{\mathbf{d}}$ is perpendicular to -both $\vec{\mathbf{c}}$ and $\vec{\mathbf{d}}$: + $$ \text{random vector} = x \mathbf{s} + y \mathbf{t} + z \mathbf{n} $$ - $$ \vec{\mathbf{t}} = \text{unit_vector}(\vec{\mathbf{a}} \times \vec{\mathbf{n}}) $$ +If you remember, we used similar math to produce rays from a camera. You can think of that as a +change to the camera’s natural coordinate system. - $$ \vec{\mathbf{s}} = \vec{\mathbf{t}} \times \vec{\mathbf{n}} $$ -</div> -<div class='together'> -This is all well and good, but the catch is that we may not be given an $\vec{\mathbf{a}}$ when we -load a model, and we don't have an $\vec{\mathbf{a}}$ with our existing program. If we went ahead -and picked an arbitrary $\vec{\mathbf{a}}$ to use as our initial vector we may get an -$\vec{\mathbf{a}}$ that is parallel to $\vec{\mathbf{n}}$. A common method is to use an -if-statement to determine whether $\vec{\mathbf{n}}$ is a particular axis, and if not, use that -axis. +The ONB Class +-------------- +Should we make a class for ONBs, or are utility functions enough? I’m not sure, but let’s make a +class because it won't really be more complicated than utility functions: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #ifndef ONB_H + #define ONB_H + + class onb { + public: + onb(const vec3& n) { + axis[2] = unit_vector(n); + vec3 a = (std::fabs(axis[2].x()) > 0.9) ? vec3(0,1,0) : vec3(1,0,0); + axis[1] = unit_vector(cross(axis[2], a)); + axis[0] = cross(axis[2], axis[1]); + } + + const vec3& u() const { return axis[0]; } + const vec3& v() const { return axis[1]; } + const vec3& w() const { return axis[2]; } + + vec3 transform(const vec3& v) const { + // Transform from basis coordinates to local space. + return (v[0] * axis[0]) + (v[1] * axis[1]) + (v[2] * axis[2]); + } + + private: + vec3 axis[3]; + }; + + #endif ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - if absolute(n.x > 0.9) - a ← (0, 1, 0) - else - a ← (1, 0, 0) + [Listing [class-onb]: <kbd>[onb.h]</kbd> Orthonormal basis class] + +<div class='together'> +We can rewrite our Lambertian material using this to get: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include "hittable.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + #include "onb.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include "texture.h" + + class material { + public: + ... + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + virtual bool scatter( + const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered, double& pdf + ) const { + return false; + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + ... + }; + + class lambertian : public material { + public: + ... + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + bool scatter( + const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered, double& pdf + ) const override { + onb uvw(rec.normal); + auto scatter_direction = uvw.transform(random_cosine_direction()); + + scattered = ray(rec.p, unit_vector(scatter_direction), r_in.time()); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + attenuation = tex->value(rec.u, rec.v, rec.p); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + pdf = dot(uvw.w(), scattered.direction()) / pi; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + return true; + } + + ... + }; + + class metal : public material { + public: + ... + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + bool scatter( + const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered, double& pdf + ) const override { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + ... + } + }; + + class dielectric : public material { + public: + ... + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + bool scatter( + const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered, double& pdf + ) const override { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + ... + } + }; + + class diffuse_light : public material { + ... + }; + + class isotropic : public material { + public: + ... + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + bool scatter( + const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered, double& pdf + ) const override { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + ... + } + }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -</div> - -<div class='together'> -Once we have an ONB of $\vec{\mathbf{s}}$, $\vec{\mathbf{t}}$, and $\vec{\mathbf{n}}$, and we have a -$random(x,y,z)$ relative to the Z-axis, we can get the vector relative to $\vec{n}$ as: + [Listing [scatter-onb]: <kbd>[material.h]</kbd> Scatter function, with orthonormal basis] - $$ \text{Random vector} = x \vec{\mathbf{s}} + y \vec{\mathbf{t}} + z \vec{\mathbf{n}} $$ </div> -<div class='together'> -You may notice we used similar math to get rays from a camera. That could be viewed as a change to -the camera’s natural coordinate system. Should we make a class for ONBs, or are utility functions -enough? I’m not sure, but let’s make a class because it won't really be more complicated than -utility functions: +And here we add the accompanying changes to the camera class: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - class onb { - public: - onb() {} + class camera { + ... + private: + ... - inline vec3 operator[](int i) const { return axis[i]; } + color ray_color(const ray& r, int depth, const hittable& world) const { + ... + + ray scattered; + color attenuation; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + double pdf_value; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + color color_from_emission = rec.mat->emitted(rec.u, rec.v, rec.p); - vec3 u() const { return axis[0]; } - vec3 v() const { return axis[1]; } - vec3 w() const { return axis[2]; } - vec3 local(double a, double b, double c) const { - return a*u() + b*v() + c*w(); - } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + if (!rec.mat->scatter(r, rec, attenuation, scattered, pdf_value)) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + return color_from_emission; - vec3 local(const vec3& a) const { - return a.x()*u() + a.y()*v() + a.z()*w(); - } + double scattering_pdf = rec.mat->scattering_pdf(r, rec, scattered); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + pdf_value = scattering_pdf; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - void build_from_w(const vec3&); + ... + } - public: - vec3 axis[3]; + ... }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [scatter-ray-color]: <kbd>[camera.h]</kbd> Updated ray_color function with returned PDF value] +<div class='together'> +Which produces: + + ![<span class='num'>Image 6:</span> Cornell box, with orthonormal basis scatter function + ](../images/img-3.06-cornell-ortho.jpg class='pixel') - void onb::build_from_w(const vec3& n) { - axis[2] = unit_vector(n); - vec3 a = (fabs(w().x()) > 0.9) ? vec3(0,1,0) : vec3(1,0,0); - axis[1] = unit_vector(cross(w(), a)); - axis[0] = cross(w(), v()); - } - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [class-onb]: <kbd>[onb.h]</kbd> Ortho-normal basis class] </div> <div class='together'> -We can rewrite our Lambertian material using this to get: +Let’s get rid of some of that noise. + +But first, let's quickly update the `isotropic` material: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - bool scatter( - const ray& r_in, const hit_record& rec, vec3& alb, ray& scattered, double& pdf - ) const { - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - onb uvw; - uvw.build_from_w(rec.normal); - vec3 direction = uvw.local(random_cosine_direction()); - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - scattered = ray(rec.p, unit_vector(direction), r_in.time()); - alb = albedo->value(rec.u, rec.v, rec.p); + class isotropic : public material { + public: + isotropic(const color& albedo) : tex(make_shared<solid_color>(albedo)) {} + isotropic(shared_ptr<texture> tex) : tex(tex) {} + + bool scatter( + const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered, double& pdf + ) const override { + scattered = ray(rec.p, random_unit_vector(), r_in.time()); + attenuation = tex->value(rec.u, rec.v, rec.p); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - pdf = dot(uvw.w(), scattered.direction()) / pi; + pdf = 1 / (4 * pi); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - return true; - } - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [scatter-onb]: <kbd>[material.h]</kbd> Scatter function, with ortho-normal basis] -</div> - -<div class='together'> -Which produces: + return true; + } - <div class="render"> - ![Cornell box, with orthonormal basis scatter function](../images/img.cornell-ortho.jpg) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + double scattering_pdf(const ray& r_in, const hit_record& rec, const ray& scattered) + const override { + return 1 / (4 * pi); + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - </div> + private: + shared_ptr<texture> tex; + }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [class-isotropic-impsample]: <kbd>[material.h]</kbd> Isotropic material, modified for importance sampling] -Is that right? We still don’t know for sure. Tracking down bugs is hard in the absence of reliable -reference solutions. Let’s table that for now and get rid of some of that noise. </div> + Sampling Lights Directly ==================================================================================================== +The problem with sampling uniformly over all directions is that lights are no more likely to be +sampled than any arbitrary or unimportant direction. We could use shadow rays to solve for the +direct lighting at any given point. Instead, I’ll just use a PDF that sends more rays to the light. +We can then turn around and change that PDF to send more rays in whatever direction we want. -The problem with sampling almost uniformly over directions is that lights are not sampled any more -than unimportant directions. We could use shadow rays and separate out direct lighting. Instead, -I’ll just send more rays to the light. We can then use that later to send more rays in whatever -direction we want. - -<div class='together'> It’s really easy to pick a random direction toward the light; just pick a random point on the light -and send a ray in that direction. We also need to know the PDF, $p(direction)$. What is that? +and send a ray in that direction. But we'll need to know the PDF, $p(\omega)$, so that we're not +biasing our render. But what is that? -For a light of area $A$, if we sample uniformly on that light, the PDF on the surface of the light -is $\frac{1}{A}$. What is it on the area of the unit sphere that defines directions? Fortunately, -there is a simple correspondence, as outlined in the diagram: - ![Figure [light-pdf]: Projection of light shape onto PDF](../images/fig.light-pdf.jpg) +Getting the PDF of a Light +--------------------------- +For a light with a surface area of $A$, if we sample uniformly on that light, the PDF on the surface +is just $\frac{1}{A}$. How much area does the entire surface of the light take up if its projected +back onto the unit sphere? Fortunately, there is a simple correspondence, as outlined in this +diagram: -</div> + ![Figure [shape-onto-pdf]: Projection of light shape onto PDF + ](../images/fig-3.11-shape-onto-pdf.jpg) -<div class='together'> -If we look at a small area $dA$ on the light, the probability of sampling it is $p_q(q) \cdot dA$. -On the sphere, the probability of sampling the small area $dw$ on the sphere is $p(direction) \cdot -dw$. There is a geometric relationship between $dw$ and $dA$: +If we look at a small area $dA$ on the light, the probability of sampling it is +$\operatorname{p_q}(q) \cdot dA$. On the sphere, the probability of sampling the small area +$d\omega$ on the sphere is $\operatorname{p}(\omega) \cdot d\omega$. There is a geometric +relationship between $d\omega$ and $dA$: - $$ dw = \frac{dA \cdot \cos(alpha)}{distance^2(p,q)} $$ + $$ d\omega = \frac{dA \cdot \cos(\theta)}{\operatorname{distance}^2(p,q)} $$ -Since the probability of sampling dw and dA must be the same, we have +Since the probability of sampling $d\omega$ and $dA$ must be the same, then - $$ p(direction) \cdot \frac{dA \cdot \cos(alpha)}{distance^2(p,q)} - = p_q(q) \cdot dA - = \frac{dA}{A} - $$ + $$ \operatorname{p}(\omega) \cdot d\omega = \operatorname{p_q}(q) \cdot dA $$ + $$ \operatorname{p}(\omega) + \cdot \frac{dA \cdot \cos(\theta)}{\operatorname{distance}^2(p,q)} + = \operatorname{p_q}(q) \cdot dA $$ + +We know that if we sample uniformly on the light the PDF on the surface is $\frac{1}{A}$: + + $$ \operatorname{p_q}(q) = \frac{1}{A} $$ + $$ \operatorname{p}(\omega) \cdot \frac{dA \cdot \cos(\theta)}{\operatorname{distance}^2(p,q)} + = \frac{dA}{A} $$ So - $$ p(direction) = \frac{distance^2(p,q)}{\cos(alpha) \cdot A} $$ -</div> + $$ \operatorname{p}(\omega) = \frac{\operatorname{distance}^2(p,q)}{\cos(\theta) \cdot A} $$ -<div class='together'> -If we hack our `ray_color()` function to sample the light in a very hard-coded fashion just to check -that math and get the concept, we can add it (see the highlighted region): + +Light Sampling +--------------- +We can hack our `ray_color()` function to sample the light in a very hard-coded fashion just to +check that we got the math and concept right: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - vec3 ray_color(const ray& r, const vec3& background, const hittable& world, int depth) { - hit_record rec; + class camera { + ... + private: + ... + + color ray_color(const ray& r, int depth, const hittable& world) const { + // If we've exceeded the ray bounce limit, no more light is gathered. + if (depth <= 0) + return color(0,0,0); + + hit_record rec; - // If we've exceeded the ray bounce limit, no more light is gathered. - if (depth <= 0) - return vec3(0,0,0); + // If the ray hits nothing, return the background color. + if (!world.hit(r, interval(0.001, infinity), rec)) + return background; - // If the ray hits nothing, return the background color. - if (!world.hit(r, 0.001, infinity, rec)) - return background; + ray scattered; + color attenuation; + double pdf_value; + color color_from_emission = rec.mat->emitted(rec.u, rec.v, rec.p); + + if (!rec.mat->scatter(r, rec, attenuation, scattered, pdf_value)) + return color_from_emission; - ray scattered; - vec3 attenuation; - vec3 emitted = rec.mat_ptr->emitted(rec.u, rec.v, rec.p); - double pdf; - vec3 albedo; - if (!rec.mat_ptr->scatter(r, rec, albedo, scattered, pdf)) - return emitted; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - vec3 on_light = vec3(random_double(213,343), 554, random_double(227,332)); - vec3 to_light = on_light - rec.p; - auto distance_squared = to_light.length_squared(); - to_light.make_unit_vector(); + auto on_light = point3(random_double(213,343), 554, random_double(227,332)); + auto to_light = on_light - rec.p; + auto distance_squared = to_light.length_squared(); + to_light = unit_vector(to_light); + + if (dot(to_light, rec.normal) < 0) + return color_from_emission; - if (dot(to_light, rec.normal) < 0) - return emitted; + double light_area = (343-213)*(332-227); + auto light_cosine = std::fabs(to_light.y()); + if (light_cosine < 0.000001) + return color_from_emission; - double light_area = (343-213)*(332-227); - auto light_cosine = fabs(to_light.y()); - if (light_cosine < 0.000001) - return emitted; + pdf_value = distance_squared / (light_cosine * light_area); + scattered = ray(rec.p, to_light, r.time()); - pdf = distance_squared / (light_cosine * light_area); - scattered = ray(rec.p, to_light, r.time()); + double scattering_pdf = rec.mat->scattering_pdf(r, rec, scattered); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - return emitted - + albedo * rec.mat_ptr->scattering_pdf(r, rec, scattered) - * ray_color(scattered, background, world, depth-1) / pdf; + color color_from_scatter = + (attenuation * scattering_pdf * ray_color(scattered, depth-1, world)) / pdf_value; + + return color_from_emission + color_from_scatter; + } + }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [ray-color-lights]: <kbd>[camera.h]</kbd> Ray color with light sampling] + +We'll test this scene with just ten samples per pixel: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + int main() { + ... + cam.aspect_ratio = 1.0; + cam.image_width = 600; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + cam.samples_per_pixel = 10; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + cam.max_depth = 50; + cam.background = color(0,0,0); + ... } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [ray-color-lights]: <kbd>[main.cc]</kbd> Ray color with light sampling] -</div> + [Listing [ray-color-lights-10spp]: <kbd>[main.cc]</kbd> Ray color with light sampling at 10spp] -<div class='together'> With 10 samples per pixel this yields: - <div class="render"> + ![<span class='num'>Image 7:</span> Cornell box, sampling only the light, 10 samples per pixel + ](../images/img-3.07-cornell-sample-light.jpg class='pixel') - ![Cornell box, sampling only the light, 10 samples per pixel](../images/img.cornell-samplight.jpg) +This is about what we would expect from something that samples only the light sources, so this +appears to work. - </div> -</div> +Switching to Unidirectional Light +---------------------------------- +The noisy pops around the light on the ceiling are because the light is two-sided and there is a +small space between light and ceiling. We probably want to have the light just emit down. We can do +that by letting the `hittable::emitted()` function take extra information: -<div class='together'> -This is about what we would expect from something that samples only the light sources, so this -appears to work. The noisy pops around the light on the ceiling are because the light is two-sided -and there is a small space between light and ceiling. We probably want to have the light just emit -down. We can do that by letting the emitted member function of hittable take extra information: + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class material { + public: + ... + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + virtual color emitted( + const ray& r_in, const hit_record& rec, double u, double v, const point3& p + ) const { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - virtual vec3 emitted(const ray& r_in, const hit_record& rec, double u, double v, - const vec3& p) const { + return color(0,0,0); + } + ... + }; - if (rec.front_face) - return emit->value(u, v, p); - else - return vec3(0,0,0); - } + class diffuse_light : public material { + public: + ... + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + color emitted(const ray& r_in, const hit_record& rec, double u, double v, const point3& p) + const override { + if (!rec.front_face) + return color(0,0,0); + return tex->value(u, v, p); + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + ... + }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [emitted-directional]: <kbd>[material.h]</kbd> Material emission, directional] -</div> -<div class='together'> -We also need to flip the light so its normals point in the -y direction. This gives us: - <div class="render"> + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class camera { + ... + private: + color ray_color(const ray& r, int depth, const hittable& world) const { + ... + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + color color_from_emission = rec.mat->emitted(r, rec, rec.u, rec.v, rec.p); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + ... + } + }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [emitted-ray-color]: <kbd>[camera.h]</kbd> Material emission, camera::ray_color() changes] + - ![Cornell box, light emitted only in the downward direction](../images/img.cornell-lightdown.jpg) +<div class='together'> +This gives us: - </div> + ![<span class='num'>Image 8:</span> Cornell box, light emitted only in the downward direction + ](../images/img-3.08-cornell-lightdown.jpg class='pixel') </div> @@ -1400,964 +2693,1275 @@ Mixture Densities ==================================================================================================== - -<div class='together'> We have used a PDF related to $\cos(\theta)$, and a PDF related to sampling the light. We would like -a PDF that combines these. A common tool in probability is to mix the densities to form a mixture -density. Any weighted average of PDFs is a PDF. For example, we could just average the two -densities: +a PDF that combines these. - $$ pdf\_mixture(direction) = \frac{1}{2} pdf\_reflection(direction) + - \frac{1}{2}pdf\_light(direction) - $$ -</div> -<div class='together'> -How would we instrument our code to do that? There is a very important detail that makes this not -quite as easy as one might expect. Choosing the random direction is simple: +The PDF Class +------------- +We've worked with PDFs in quite a lot of code already. I think that now is a good time to figure out +how we want to standardize our usage of PDFs. We already know that we are going to have a PDF for +the surface and a PDF for the light, so let's create a `pdf` base class. So far, we've had a `pdf()` +function that took a direction and returned the PDF's distribution value for that direction. This +value has so far been one of $1/4\pi$, $1/2\pi$, and $\cos(\theta)/\pi$. In a couple of our examples +we generated the random direction using a different distribution than the distribution of the PDF. +We covered this quite a lot in the chapter Playing with Importance Sampling. In general, if we know +the distribution of our random directions, we should use a PDF with the same distribution. This will +lead to the fastest convergence. With that in mind, we'll create a `pdf` class that is responsible +for generating random directions and determining the value of the PDF. - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - if (random_double() < 0.5) - Pick direction according to pdf_reflection - else - Pick direction according to pdf_light - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -</div> +From all of this, any `pdf` class should be responsible for -But evaluating $pdf\_mixture$ is slightly more subtle. We need to evaluate both $pdf\_reflection$ -and $pdf\_light$ because there are some directions where either PDF could have generated the -direction. For example, we might generate a direction toward the light using $pdf\_reflection$. + 1. returning a random direction weighted by the internal PDF distribution, and + 2. returning the corresponding PDF distribution value in that direction. <div class='together'> -If we step back a bit, we see that there are two functions a PDF needs to support: +The details of how this is done under the hood varies for $\operatorname{pSurface}$ and +$\operatorname{pLight}$, but that is exactly what class hierarchies were invented for! It’s never +obvious what goes in an abstract class, so my approach is to be greedy and hope a minimal interface +works, and for `pdf` this implies: -1. What is your value at this location? -2. Return a random number that is distributed appropriately. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #ifndef PDF_H + #define PDF_H -</div> + #include "onb.h" -<div class='together'> -The details of how this is done under the hood varies for the $pdf\_reflection$ and the $pdf\_light$ -and the mixture density of the two of them, but that is exactly what class hierarchies were invented -for! It’s never obvious what goes in an abstract class, so my approach is to be greedy and hope a -minimal interface works, and for the PDF this implies: - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class pdf { - public: - virtual ~pdf() {} + public: + virtual ~pdf() {} - virtual double value(const vec3& direction) const = 0; - virtual vec3 generate() const = 0; + virtual double value(const vec3& direction) const = 0; + virtual vec3 generate() const = 0; }; + + #endif ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [class-pdf]: <kbd>[pdf.h]</kbd> The `pdf` class] -</div> + [Listing [class-pdf]: <kbd>[pdf.h]</kbd> The abstract PDF class] -We’ll see if that works by fleshing out the subclasses. For sampling the light, we will need -`hittable` to answer some queries that it doesn’t have an interface for. We’ll probably need to mess -with it too, but we can start by seeing if we can put something in `hittable` involving sampling the -bounding box that works with all its subclasses. +</div> <div class='together'> -First, let’s try a cosine density: +We’ll see if we need to add anything else to `pdf` by fleshing out the subclasses. First, we'll +create a uniform density over the unit sphere: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - inline vec3 random_cosine_direction() { - auto r1 = random_double(); - auto r2 = random_double(); - auto z = sqrt(1-r2); + class sphere_pdf : public pdf { + public: + sphere_pdf() {} - auto phi = 2*pi*r1; - auto x = cos(phi)*sqrt(r2); - auto y = sin(phi)*sqrt(r2); + double value(const vec3& direction) const override { + return 1/ (4 * pi); + } - return vec3(x, y, z); - } + vec3 generate() const override { + return random_unit_vector(); + } + }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [class-uni-pdf]: <kbd>[pdf.h]</kbd> The sphere_pdf class] + +</div> +<div class='together'> +Next, let’s try a cosine density: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class cosine_pdf : public pdf { - public: - cosine_pdf(const vec3& w) { uvw.build_from_w(w); } + public: + cosine_pdf(const vec3& w) : uvw(w) {} - virtual double value(const vec3& direction) const { - auto cosine = dot(unit_vector(direction), uvw.w()); - return (cosine <= 0) ? 0 : cosine/pi; - } + double value(const vec3& direction) const override { + auto cosine_theta = dot(unit_vector(direction), uvw.w()); + return std::fmax(0, cosine_theta/pi); + } - virtual vec3 generate() const { - return uvw.local(random_cosine_direction()); - } + vec3 generate() const override { + return uvw.transform(random_cosine_direction()); + } - public: - onb uvw; + private: + onb uvw; }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [class-cos-pdf]: <kbd>[pdf.h]</kbd> The cosine_pdf class] + </div> <div class='together'> -We can try this in the `ray_color()` function, with the main changes highlighted. We also need to -change variable `pdf` to some other variable name to avoid a name conflict with the new `pdf` class. +We can try this cosine PDF in the `ray_color()` function: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - vec3 ray_color(const ray& r, const vec3& background, const hittable& world, int depth) { - hit_record rec; + #include "hittable.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + #include "pdf.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include "material.h" + + class camera { + ... + private: + ... + color ray_color(const ray& r, int depth, const hittable& world) const { + // If we've exceeded the ray bounce limit, no more light is gathered. + if (depth <= 0) + return color(0,0,0); - // If we've exceeded the ray bounce limit, no more light is gathered. - if (depth <= 0) - return vec3(0,0,0); + hit_record rec; - // If the ray hits nothing, return the background color. - if (!world.hit(r, 0.001, infinity, rec)) - return background; + // If the ray hits nothing, return the background color. + if (!world.hit(r, interval(0.001, infinity), rec)) + return background; + + ray scattered; + color attenuation; + double pdf_value; + color color_from_emission = rec.mat->emitted(r, rec, rec.u, rec.v, rec.p); + + if (!rec.mat->scatter(r, rec, attenuation, scattered, pdf_value)) + return color_from_emission; - ray scattered; - vec3 attenuation; - vec3 emitted = rec.mat_ptr->emitted(r, rec, rec.u, rec.v, rec.p); - double pdf_val; - vec3 albedo; - if (!rec.mat_ptr->scatter(r, rec, albedo, scattered, pdf_val)) - return emitted; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - cosine_pdf p(rec.normal); - scattered = ray(rec.p, p.generate(), r.time()); - pdf_val = p.value(scattered.direction()); + cosine_pdf surface_pdf(rec.normal); + scattered = ray(rec.p, surface_pdf.generate(), r.time()); + pdf_value = surface_pdf.value(scattered.direction()); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - return emitted - + albedo * rec.mat_ptr->scattering_pdf(r, rec, scattered) - * ray_color(scattered, background, world, depth-1) - / pdf_val; - } + double scattering_pdf = rec.mat->scattering_pdf(r, rec, scattered); + + color color_from_scatter = + (attenuation * scattering_pdf * ray_color(scattered, depth-1, world)) / pdf_value; + + return color_from_emission + color_from_scatter; + } + }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [ray-color-cos-pdf]: <kbd>[main.cc]</kbd> The ray_color function, using cosine pdf] + [Listing [ray-color-cos-pdf]: <kbd>[camera.h]</kbd> The ray_color function, using cosine PDF] + </div> <div class='together'> -This yields an apparently matching result so all we’ve done so far is refactor where `pdf` is -computed: +And set the render back to 1000 samples per pixel: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + int main() { + ... + cam.aspect_ratio = 1.0; + cam.image_width = 600; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + cam.samples_per_pixel = 1000; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + cam.max_depth = 50; + cam.background = color(0,0,0); + ... + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [cosine-density-1000spp]: <kbd>[main.cc]</kbd> Reset sampling back to 1000spp] - <div class="render"> +</div> - ![Cornell box with a cosine density _pdf_](../images/img.cornell-cospdf.jpg) +<div class='together'> +This yields an exactly matching result so all we’ve done so far is move some computation up into the +`cosine_pdf` class: - </div> + ![<span class='num'>Image 9:</span> Cornell box with a cosine density PDF + ](../images/img-3.09-cornell-cos-pdf.jpg class='pixel') </div> -<div class='together'> -Now we can try sampling directions toward a `hittable` like the light. +Sampling Directions towards a Hittable +--------------------------------------- +Now we can try sampling directions toward a `hittable`, like the light. + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + #include "hittable_list.h" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include "onb.h" + + ... + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight class hittable_pdf : public pdf { - public: - hittable_pdf(shared_ptr<hittable> p, const vec3& origin) : ptr(p), o(origin) {} + public: + hittable_pdf(const hittable& objects, const point3& origin) + : objects(objects), origin(origin) + {} - virtual double value(const vec3& direction) const { - return ptr->pdf_value(o, direction); - } + double value(const vec3& direction) const override { + return objects.pdf_value(origin, direction); + } - virtual vec3 generate() const { - return ptr->random(o); - } + vec3 generate() const override { + return objects.random(origin); + } - public: - vec3 o; - shared_ptr<hittable> ptr; + private: + const hittable& objects; + point3 origin; }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [class-hittable-pdf]: <kbd>[pdf.h]</kbd> The hittable_pdf class] -</div> -<div class='together'> -This assumes two as-yet not implemented functions in the `hittable` class. To avoid having to add -instrumentation to all `hittable` subclasses, we’ll add two dummy functions to the `hittable` class: +If we want to sample the light, we will need `hittable` to answer some queries that it doesn’t yet +have an interface for. The above code assumes the existence of two as-of-yet unimplemented functions +in the `hittable` class: `pdf_value()` and `random()`. We need to add these functions for the +program to compile. We could go through all of the `hittable` subclasses and add these functions, +but that would be a hassle, so we’ll just add two trivial functions to the `hittable` base class. +This breaks our previously pure abstract implementation, but it saves work. Feel free to write these +functions through to subclasses if you want a purely abstract `hittable` interface class. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class hittable { - public: - virtual bool hit(const ray& r, double t_min, double t_max, hit_record& rec) const = 0; - virtual bool bounding_box(double t0, double t1, aabb& output_box) const = 0; + public: + virtual ~hittable() = default; + + virtual bool hit(const ray& r, interval ray_t, hit_record& rec) const = 0; + + virtual aabb bounding_box() const = 0; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - virtual double pdf_value(const vec3& o, const vec3& v) const { - return 0.0; - } + virtual double pdf_value(const point3& origin, const vec3& direction) const { + return 0.0; + } - virtual vec3 random(const vec3& o) const { - return vec3(1, 0, 0); - } + virtual vec3 random(const point3& origin) const { + return vec3(1,0,0); + } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [hittable-plus2]: <kbd>[hittable.h]</kbd> The hittable class, with two new methods] + +<div class='together'> +And then we change `quad` to implement those functions: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + class quad : public hittable { + public: + quad(const point3& Q, const vec3& u, const vec3& v, shared_ptr<material> mat) + : Q(Q), u(u), v(v), mat(mat) + { + auto n = cross(u, v); + normal = unit_vector(n); + D = dot(normal, Q); + w = n / dot(n,n); + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + area = n.length(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + set_bounding_box(); + } + + ... + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + double pdf_value(const point3& origin, const vec3& direction) const override { + hit_record rec; + if (!this->hit(ray(origin, direction), interval(0.001, infinity), rec)) + return 0; + + auto distance_squared = rec.t * rec.t * direction.length_squared(); + auto cosine = std::fabs(dot(direction, rec.normal) / direction.length()); + + return distance_squared / (cosine * area); + } + + vec3 random(const point3& origin) const override { + auto p = Q + (random_double() * u) + (random_double() * v); + return p - origin; + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + private: + point3 Q; + vec3 u, v; + vec3 w; + shared_ptr<material> mat; + aabb bbox; + vec3 normal; + double D; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + double area; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [quad-pdf]: <kbd>[quad.h]</kbd> quad with PDF] + </div> +We only need to add `pdf_value()` and `random()` to `quad` because we're using this to importance +sample the light, and the only light we have in our scene is a `quad`. if you want other light +geometries, or want to use a PDF with other objects, you'll need to implement the above functions +for the corresponding classes. + <div class='together'> -And we change `xz_rect` to implement those functions: +Add a `lights` parameter to the camera `render()` function: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - class xz_rect: public hittable { - public: - xz_rect() {} + class camera { + public: + ... + - xz_rect( - double _x0, double _x1, double _z0, double _z1, double _k, shared_ptr<material> mat) - : x0(_x0), x1(_x1), z0(_z0), z1(_z1), k(_k), mp(mat) {}; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + void render(const hittable& world, const hittable& lights) { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + initialize(); - virtual bool hit(const ray& r, double t0, double t1, hit_record& rec) const; + std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n"; - virtual bool bounding_box(double t0, double t1, aabb& output_box) const { - output_box = aabb(vec3(x0,k-0.0001,z0), vec3(x1, k+0.0001, z1)); - return true; + for (int j = 0; j < image_height; j++) { + std::clog << "\rScanlines remaining: " << (image_height - j) << ' ' << std::flush; + for (int i = 0; i < image_width; i++) { + color pixel_color(0,0,0); + for (int s_j = 0; s_j < sqrt_spp; s_j++) { + for (int s_i = 0; s_i < sqrt_spp; s_i++) { + ray r = get_ray(i, j, s_i, s_j); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + pixel_color += ray_color(r, max_depth, world, lights); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } + } + write_color(std::cout, pixel_samples_scale * pixel_color); + } } + std::clog << "\rDone. \n"; + } + + ... + private: + ... + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - virtual double pdf_value(const vec3& origin, const vec3& v) const { - hit_record rec; - if (!this->hit(ray(origin, v), 0.001, infinity, rec)) - return 0; + color ray_color(const ray& r, int depth, const hittable& world, const hittable& lights) + const { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + ... - auto area = (x1-x0)*(z1-z0); - auto distance_squared = rec.t * rec.t * v.length_squared(); - auto cosine = fabs(dot(v, rec.normal) / v.length()); + ray scattered; + color attenuation; + double pdf_value; + color color_from_emission = rec.mat->emitted(r, rec, rec.u, rec.v, rec.p); - return distance_squared / (cosine * area); - } + if (!rec.mat->scatter(r, rec, attenuation, scattered, pdf_value)) + return color_from_emission; - virtual vec3 random(const vec3& origin) const { - vec3 random_point = vec3(random_double(x0,x1), k, random_double(z0,z1)); - return random_point - origin; - } + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + hittable_pdf light_pdf(lights, rec.p); + scattered = ray(rec.p, light_pdf.generate(), r.time()); + pdf_value = light_pdf.value(scattered.direction()); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + double scattering_pdf = rec.mat->scattering_pdf(r, rec, scattered); + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + color sample_color = ray_color(scattered, depth-1, world, lights); + color color_from_scatter = (attenuation * scattering_pdf * sample_color) / pdf_value; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - public: - shared_ptr<material> mp; - double x0, x1, z0, z1, k; + return color_from_emission + color_from_scatter; + } }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [xz-rect-pdf]: <kbd>[aarect.h]</kbd> XZ rect with pdf] + [Listing [ray-color-lights]: <kbd>[camera.h]</kbd> ray_color function with light PDF] + </div> <div class='together'> -And then change `ray_color()`: +Create a light in the middle of the ceiling: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - vec3 ray_color(const ray& r, const vec3& background, const hittable& world, int depth) { - hit_record rec; + int main() { + ... - // If we've exceeded the ray bounce limit, no more light is gathered. - if (depth <= 0) - return vec3(0,0,0); + // Box 2 + shared_ptr<hittable> box2 = box(point3(0,0,0), point3(165,165,165), white); + box2 = make_shared<rotate_y>(box2, -18); + box2 = make_shared<translate>(box2, vec3(130,0,65)); + world.add(box2); - // If the ray hits nothing, return the background color. - if (!world.hit(r, 0.001, infinity, rec)) - return background; - ray scattered; - vec3 attenuation; - vec3 emitted = rec.mat_ptr->emitted(r, rec, rec.u, rec.v, rec.p); - double pdf_val; - vec3 albedo; - if (!rec.mat_ptr->scatter(r, rec, albedo, scattered, pdf_val)) - return emitted; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + // Light Sources + auto empty_material = shared_ptr<material>(); + quad lights(point3(343,554,332), vec3(-130,0,0), vec3(0,0,-105), empty_material); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + camera cam; + cam.aspect_ratio = 1.0; + cam.image_width = 600; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - shared_ptr<hittable> light_shape = make_shared<xz_rect>(213, 343, 227, 332, 554, 0); - hittable_pdf p(light_shape, rec.p); + cam.samples_per_pixel = 10; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + cam.max_depth = 50; + cam.background = color(0,0,0); - scattered = ray(rec.p, p.generate(), r.time()); - pdf_val = p.value(scattered.direction()); + ... - return emitted - + albedo * rec.mat_ptr->scattering_pdf(r, rec, scattered) - * ray_color(scattered, background, world, depth-1) - / pdf_val; + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + cam.render(world, lights); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [ray-color-hittable-pdf]: <kbd>[main.cc]</kbd> ray_color function with hittable PDF] + [Listing [ray-color-hittable-pdf]: <kbd>[main.cc]</kbd> Adding a light to the Cornell box] + </div> <div class='together'> At 10 samples per pixel we get: - <div class="render"> + ![<span class='num'>Image 10:</span> Cornell box, sampling a hittable light, 10 samples per pixel + ](../images/img-3.10-hittable-light.jpg class='pixel') + +</div> + + +The Mixture PDF Class +---------------------- +As was briefly mentioned in the chapter Playing with Importance Sampling, we can create linear +mixtures of any PDFs to form mixture densities that are also PDFs. Any weighted average of PDFs is +also a PDF. As long as the weights are positive and add up to any one, we have a new PDF. + + $$ \operatorname{pMixture}() = w_0 p_0() + w_1 p_1() + w_2 p_2() + \ldots + w_{n-1} p_{n-1}() $$ + + $$ 1 = w_0 + w_1 + w_2 + \ldots + w_{n-1} $$ - ![Cornell box, sampling a hittable light, 10 samples per pixel](../images/img.cornell-samphit.jpg) +For example, we could just average the two densities: - </div> + $$ \operatorname{pMixture}(\omega_o) + = \frac{1}{2} \operatorname{pSurface}(\omega_o) + \frac{1}{2} \operatorname{pLight}(\omega_o) + $$ + +<div class='together'> +How would we instrument our code to do that? There is a very important detail that makes this not +quite as easy as one might expect. Generating the random direction for a mixture PDF is simple: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + if (random_double() < 0.5) + pick direction according to pSurface + else + pick direction according to pLight + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +</div> + +But solving for the PDF value of $\operatorname{pMixture}$ is slightly more subtle. We can't just + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + if (direction is from pSurface) + get PDF value of pSurface + else + get PDF value of pLight + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ </div> <div class='together'> -Now we would like to do a mixture density of the cosine and light sampling. The mixture density -class is straightforward: +For one, figuring out which PDF the random direction came from is probably not trivial. We don't +have any plumbing for `generate()` to tell `value()` what the original `random_double()` was, so we +can't trivially say which PDF the random direction comes from. If we thought that the above was +correct, we would have to solve backwards to figure which PDF the direction could come from. Which +honestly sounds like a nightmare, but fortunately we don't need to do that. There are some +directions that both PDFs could have generated. For example, a direction toward the light could have +been generated by either $\operatorname{pLight}$ _or_ $\operatorname{pSurface}$. It is sufficient +for us to solve for the PDF value of $\operatorname{pSurface}$ and of $\operatorname{pLight}$ for a +random direction and then take the PDF mixture weights to solve for the total PDF value for that +direction. The mixture density class is actually pretty straightforward: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class mixture_pdf : public pdf { - public: - mixture_pdf(shared_ptr<pdf> p0, shared_ptr<pdf> p1) { - p[0] = p0; - p[1] = p1; - } + public: + mixture_pdf(shared_ptr<pdf> p0, shared_ptr<pdf> p1) { + p[0] = p0; + p[1] = p1; + } - virtual double value(const vec3& direction) const { - return 0.5 * p[0]->value(direction) + 0.5 *p[1]->value(direction); - } + double value(const vec3& direction) const override { + return 0.5 * p[0]->value(direction) + 0.5 * p[1]->value(direction); + } - virtual vec3 generate() const { - if (random_double() < 0.5) - return p[0]->generate(); - else - return p[1]->generate(); - } + vec3 generate() const override { + if (random_double() < 0.5) + return p[0]->generate(); + else + return p[1]->generate(); + } - public: - shared_ptr<pdf> p[2]; + private: + shared_ptr<pdf> p[2]; }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [class-mixturep-df]: <kbd>[pdf.h]</kbd> The `mixture_pdf` class] + [Listing [class-mixturep-df]: <kbd>[pdf.h]</kbd> The mixture_pdf class] + </div> <div class='together'> -And plugging it into `ray_color()`: +Now we would like to do a mixture density of the cosine sampling and of the light sampling. We can +plug it into `ray_color()`: - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - vec3 ray_color( - const ray& r, - const vec3& background, - const hittable& world, - shared_ptr<hittable> lights, - int depth - ) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - hit_record rec; + class camera { + ... + private: + ... - // If we've exceeded the ray bounce limit, no more light is gathered. - if (depth <= 0) - return vec3(0,0,0); + color ray_color(const ray& r, int depth, const hittable& world, const hittable& lights) + const { + ... - // If the ray hits nothing, return the background color. - if (!world.hit(r, 0.001, infinity, rec)) - return background; - - ray scattered; - vec3 attenuation; - vec3 emitted = rec.mat_ptr->emitted(r, rec, rec.u, rec.v, rec.p); - double pdf_val; - vec3 albedo; - if (!rec.mat_ptr->scatter(r, rec, albedo, scattered, pdf_val)) - return emitted; + if (!rec.mat->scatter(r, rec, attenuation, scattered, pdf_value)) + return color_from_emission; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - shared_ptr<hittable> light_ptr = make_shared<xz_rect>(213, 343, 227, 332, 554, 0); - hittable_pdf p0(light_ptr, rec.p); + auto p0 = make_shared<hittable_pdf>(lights, rec.p); + auto p1 = make_shared<cosine_pdf>(rec.normal); + mixture_pdf mixed_pdf(p0, p1); - cosine_pdf p1(rec.normal); - mixture_pdf p(&p0, &p1); + scattered = ray(rec.p, mixed_pdf.generate(), r.time()); + pdf_value = mixed_pdf.value(scattered.direction()); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - scattered = ray(rec.p, p.generate(), r.time()); - pdf_val = p.value(scattered.direction()); + double scattering_pdf = rec.mat->scattering_pdf(r, rec, scattered); - return emitted - + albedo * rec.mat_ptr->scattering_pdf(r, rec, scattered) - * ray_color(scattered, background, world, lights, depth-1) - / pdf_val; - } + color sample_color = ray_color(scattered, depth-1, world, lights); + color color_from_scatter = (attenuation * scattering_pdf * sample_color) / pdf_value; + + return color_from_emission + color_from_scatter; + } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [ray-color-mixture]: <kbd>[main.cc]</kbd> The ray_color function, using mixture PDF] + [Listing [ray-color-mixture]: <kbd>[camera.h]</kbd> The ray_color function, using mixture PDF] + </div> <div class='together'> -1000 samples per pixel yields: - - <div class="render"> +Updating `main.cc` to 1000 samples per pixel (not listed) yields: - ![Cornell box, mixture density of cosine and light sampling](../images/img.cornell-coslight.jpg) - - </div> + ![<span class='num'>Image 11:</span> Cornell box, mixture density of cosine and light sampling + ](../images/img-3.11-cosine-and-light.jpg class='pixel') </div> -We’ve basically gotten this same picture (with different levels of noise) with several different -sampling patterns. It looks like the original picture was slightly wrong! Note by “wrong” here I -mean not a correct Lambertian picture. Yet Lambertian is just an ideal approximation to matte, so -our original picture was some other accidental approximation to matte. I don’t think the new one is -any better, but we can at least compare it more easily with other Lambertian renderers. - Some Architectural Decisions ==================================================================================================== - -I won't write any code in this chapter. We’re at a crossroads where I need to make some -architectural decisions. The mixture-density approach is to not have traditional shadow rays, and is -something I personally like, because in addition to lights you can sample windows or bright cracks -under doors or whatever else you think might be bright. But most programs branch, and send one or -more terminal rays to lights explicitly, and one according to the reflective distribution of the -surface. This could be a time you want faster convergence on more restricted scenes and add shadow -rays; that’s a personal design preference. +We won't write any code in this chapter. We’re at a crossroads and we need to make some +architectural decisions. + +The mixture-density approach is an alternative to having more traditional shadow rays. These are +rays that check for an unobstructed path from an intersection point to a given light source. Rays +that intersect an object between a point and a given light source indicate that the intersection +point is in the shadow of that particular light source. The mixture-density approach is something +that I personally prefer, because in addition to lights, you can sample windows or bright cracks +under doors or whatever else you think might be bright -- or important. But you'll still see shadow +rays in most professional path tracers. Typically they'll have a predefined number of shadow rays +(_e.g_ 1, 4, 8, 16) where over the course of rendering, at each place where the path tracing ray +intersects, they'll send these terminal shadow rays to random lights in the scene to determine if +the intersection is lit by that random light. The intersection will either be lit by that light, or +completely in shadow, where more shadow rays lead to a more accurate illumination. After all of the +shadow rays terminate (either at a light or at an occluding surface), the inital path tracing ray +continues on and more shadow rays are sent at the next intersection. You can't tell the shadow rays +what is important, you can only tell them what is emissive, so shadow rays work best on simpler +scenes that don't have overly complicated photon distribution. That said, shadow rays terminate at +the first thing they run into and don't bounce around, so one shadow ray is cheaper than one path +tracing ray, which is the reason that you'll typically see a lot more shadow rays than path tracing +rays (_e.g_ 1, 4, 8, 16). You could choose shadow rays over mixture-density in a more restricted +scene; that’s a personal design preference. Shadow rays tend to be cheaper for a crude result than +mixture-density and is becoming increasingly common in realtime. There are some other issues with the code. -The PDF construction is hard coded in the `ray_color()` function. We should clean that up, probably -by passing something into color about the lights. Unlike BVH construction, we should be careful -about memory leaks as there are an unbounded number of samples. +The PDF construction is hard coded in the `ray_color()` function. We should clean that up. -The specular rays (glass and metal) are no longer supported. The math would work out if we just made -their scattering function a delta function. But that would be floating point disaster. We could -either separate out specular reflections, or have surface roughness never be zero and have -almost-mirrors that look perfectly smooth but don’t generate NaNs. I don’t have an opinion on which -way to do it (I have tried both and they both have their advantages), but we have smooth metal and -glass code anyway, so I add perfect specular surfaces that do not do explicit f()/p() calculations. +We've accidentally broken the specular rays (glass and metal), and they are no longer supported. The +math would continue to work out if we just made their scattering function a delta function, but that +would lead to all kinds of floating point disasters. We could either make specular reflection a +special case that skips $f()/p()$, or we could set surface roughness to a very small -- but nonzero +-- value and have almost-mirrors that look perfectly smooth but that don’t generate NaNs. I don’t +have an opinion on which way to do it (I have tried both and they both have their advantages), but +we have smooth metal and glass code anyway, so we'll add perfect specular surfaces that just skip +over explicit $f()/p()$ calculations. We also lack a real background function infrastructure in case we want to add an environment map or -more interesting functional background. Some environment maps are HDR (the R/G/B components are -floats rather than 0–255 bytes usually interpreted as 0-1). Our output has been HDR all along; we’ve -just been truncating it. - -Finally, our renderer is RGB and a more physically based one -- like an automobile manufacturer -might use -- would probably need to use spectral colors and maybe even polarization. For a movie -renderer, you would probably want RGB. You can make a hybrid renderer that has both modes, but that -is of course harder. I’m going to stick to RGB for now, but I will revisit this near the end of the +a more interesting functional background. Some environment maps are HDR (the RGB components are +normalized floats rather than 0–255 bytes). Our output has been HDR all along; we’ve just been +truncating it. + +Finally, our renderer is RGB. A more physically based one -- like an automobile manufacturer might +use -- would probably need to use spectral colors and maybe even polarization. For a movie renderer, +most studios still get away with RGB. You can make a hybrid renderer that has both modes, but that +is of course harder. I’m going to stick to RGB for now, but I will touch on this at the end of the book. -Cleaning Up PDF Management. +Cleaning Up PDF Management ==================================================================================================== - -<div class='together'> So far I have the `ray_color()` function create two hard-coded PDFs: -1. `p0()` related to the shape of the light -2. `p1()` related to the normal vector and type of surface - -</div> + 1. `p0()` related to the shape of the light + 2. `p1()` related to the normal vector and type of surface We can pass information about the light (or whatever `hittable` we want to sample) into the -`ray_color()` function, and we can ask the `material` function for a PDF (we would have to -instrument it to do that). We can also either ask `hit` function or the `material` class to supply -whether there is a specular vector. +`ray_color()` function, and we can ask the `material` function for a PDF (we would have to add +instrumentation to do that). We also need to know if the scattered ray is specular, and we can do +this either by asking the `hit()` function or the `material` class. + -One thing we would like to allow for is a material like varnished wood that is partially ideal +Diffuse Versus Specular +------------------------ +One thing we would like to allow for is a material -- like varnished wood -- that is partially ideal specular (the polish) and partially diffuse (the wood). Some renderers have the material generate two rays: one specular and one diffuse. I am not fond of branching, so I would rather have the material randomly decide whether it is diffuse or specular. The catch with that approach is that we -need to be careful when we ask for the PDF value and be aware of whether for this evaluation of -`ray_color()` it is diffuse or specular. Fortunately, we know that we should only call the -`pdf_value()` if it is diffuse so we can handle that implicitly. +need to be careful when we ask for the PDF value, and `ray_color()` needs to be aware of whether +this ray is diffuse or specular. Fortunately, we have decided that we should only call the +`pdf_value()` if it is diffuse, so we can handle that implicitly. <div class='together'> -We can redesign `material` and stuff all the new arguments into a `struct` like we did for -`hittable`: +We can redesign `material` and stuff all the new arguments into a class like we did for `hittable`: + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include "hittable.h" + #include "onb.h" + #include "texture.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - struct scatter_record { - ray specular_ray; - bool is_specular; - vec3 attenuation; + class scatter_record { + public: + color attenuation; shared_ptr<pdf> pdf_ptr; + bool skip_pdf; + ray skip_pdf_ray; }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class material { - public: - virtual vec3 emitted( - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - const ray& r_in, const hit_record& rec, double u, double v, const vec3& p - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - ) const { - return vec3(0,0,0); - } + public: + ... + - virtual bool scatter( ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - const ray& r_in, const hit_record& rec, scatter_record& srec + virtual bool scatter(const ray& r_in, const hit_record& rec, scatter_record& srec) const { + return false; + } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - ) const { - return false; - } - virtual double scattering_pdf( - const ray& r_in, const hit_record& rec, const ray& scattered - ) const { - return 0; - } + ... }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [material-refactor]: <kbd>[material.h]</kbd> Refactoring the material class] + </div> <div class='together'> -The Lambertian material becomes simpler: +The `lambertian` material becomes simpler: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include "hittable.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ delete + #include "onb.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + #include "pdf.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + #include "texture.h" + + ... + class lambertian : public material { - public: - lambertian(shared_ptr<texture> a) : albedo(a) {} + public: + lambertian(const color& albedo) : tex(make_shared<solid_color>(albedo)) {} + lambertian(shared_ptr<texture> tex) : tex(tex) {} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - bool scatter(const ray& r_in, const hit_record& rec, scatter_record& srec) const { - srec.is_specular = false; - srec.attenuation = albedo->value(rec.u, rec.v, rec.p); - srec.pdf_ptr = new cosine_pdf(rec.normal); - return true; - } + bool scatter(const ray& r_in, const hit_record& rec, scatter_record& srec) const override { + srec.attenuation = tex->value(rec.u, rec.v, rec.p); + srec.pdf_ptr = make_shared<cosine_pdf>(rec.normal); + srec.skip_pdf = false; + return true; + } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - double scattering_pdf( - const ray& r_in, const hit_record& rec, const ray& scattered - ) const { - auto cosine = dot(rec.normal, unit_vector(scattered.direction())); - return cosine < 0 ? 0 : cosine/pi; - } + double scattering_pdf(const ray& r_in, const hit_record& rec, const ray& scattered) + const override { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + auto cos_theta = dot(rec.normal, unit_vector(scattered.direction())); + return cos_theta < 0 ? 0 : cos_theta/pi; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + } - public: - shared_ptr<texture> albedo; + private: + shared_ptr<texture> tex; }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [lambertian-scatter]: <kbd>[material.h]</kbd> New lambertian scatter() method] + </div> <div class='together'> -And `ray_color()` changes are small: +As does the `isotropic` material: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - vec3 ray_color( - const ray& r, - const vec3& background, - const hittable& world, - shared_ptr<hittable> lights, - int depth - ) { - hit_record rec; - - // If we've exceeded the ray bounce limit, no more light is gathered. - if (depth <= 0) - return vec3(0,0,0); + class isotropic : public material { + public: + isotropic(const color& albedo) : tex(make_shared<solid_color>(albedo)) {} + isotropic(shared_ptr<texture> tex) : tex(tex) {} - // If the ray hits nothing, return the background color. - if (!world.hit(r, 0.001, infinity, rec)) - return background; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - scatter_record srec; - vec3 emitted = rec.mat_ptr->emitted(r, rec, rec.u, rec.v, rec.p); - if (!rec.mat_ptr->scatter(r, rec, srec)) - return emitted; - - auto light_ptr = make_shared<hittable_pdf>(lights, rec.p); - mixture_pdf p(light_ptr, srec.pdf_ptr); + bool scatter(const ray& r_in, const hit_record& rec, scatter_record& srec) const override { + srec.attenuation = tex->value(rec.u, rec.v, rec.p); + srec.pdf_ptr = make_shared<sphere_pdf>(); + srec.skip_pdf = false; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + return true; + } - ray scattered = ray(rec.p, p.generate(), r.time()); - auto pdf_val = p.value(scattered.direction()); + double scattering_pdf(const ray& r_in, const hit_record& rec, const ray& scattered) + const override { + return 1 / (4 * pi); + } - return emitted - + srec.attenuation * rec.mat_ptr->scattering_pdf(r, rec, scattered) - * ray_color(scattered, background, world, lights, depth-1) - / pdf_val; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - } + private: + shared_ptr<texture> tex; + }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [ray-color-scatter]: <kbd>[main.cc]</kbd> Ray color with scatter] + [Listing [isotropic-scatter]: <kbd>[material.h]</kbd> New isotropic scatter() method] + </div> <div class='together'> -We have not yet dealt with specular surfaces, nor instances that mess with the surface normal, and -we have added a memory leak by calling `new` in Lambertian material. However, this design is clean -overall, and those are all fixable. For now, I will just fix `specular`. Metal is easy to fix. +And `ray_color()` changes are small: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - class metal : public material { - public: - metal(const vec3& a, double f) : albedo(a), fuzz(f < 1 ? f : 1) {} - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - virtual bool scatter( - const ray& r_in, const hit_record& rec, scatter_record& srec - ) const { - vec3 reflected = reflect(unit_vector(r_in.direction()), rec.normal); - srec.specular_ray = ray(rec.p, reflected+fuzz*random_in_unit_sphere()); - srec.attenuation = albedo; - srec.is_specular = true; - srec.pdf_ptr = 0; - return true; - } + class camera { + ... + private: + ... + + color ray_color(const ray& r, int depth, const hittable& world, const hittable& lights) + const { + // If we've exceeded the ray bounce limit, no more light is gathered. + if (depth <= 0) + return color(0,0,0); + + hit_record rec; + + // If the ray hits nothing, return the background color. + if (!world.hit(r, interval(0.001, infinity), rec)) + return background; + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + scatter_record srec; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + color color_from_emission = rec.mat->emitted(r, rec, rec.u, rec.v, rec.p); + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + if (!rec.mat->scatter(r, rec, srec)) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + return color_from_emission; + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + auto light_ptr = make_shared<hittable_pdf>(lights, rec.p); + mixture_pdf p(light_ptr, srec.pdf_ptr); + + ray scattered = ray(rec.p, p.generate(), r.time()); + auto pdf_value = p.value(scattered.direction()); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + double scattering_pdf = rec.mat->scattering_pdf(r, rec, scattered); + color sample_color = ray_color(scattered, depth-1, world, lights); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + color color_from_scatter = + (srec.attenuation * scattering_pdf * sample_color) / pdf_value; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - public: - vec3 albedo; - double fuzz; + + return color_from_emission + color_from_scatter; + } }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [metal-scatter]: <kbd>[material.h]</kbd> The metal class scatter method] + [Listing [ray-color-mixture]: <kbd>[camera.h]</kbd> The ray_color function, using mixture PDF] + </div> -Note that if fuzziness is high, this surface isn’t ideally specular, but the implicit sampling works -just like it did before. -<div class='together'> -`ray_color()` just needs a new case to generate an implicitly sampled ray: +Handling Specular +------------------ +We have not yet dealt with specular surfaces, nor instances that mess with the surface normal. But +this design is clean overall, and those are all fixable. For now, I will just fix `specular`. Metal +and dielectric materials are easy to fix. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - vec3 ray_color( - const ray& r, - const vec3& background, - const hittable& world, - shared_ptr<hittable> lights, - int depth - ) { - hit_record rec; + class metal : public material { + public: + metal(const color& albedo, double fuzz) : albedo(albedo), fuzz(fuzz < 1 ? fuzz : 1) {} - // If we've exceeded the ray bounce limit, no more light is gathered. - if (depth <= 0) - return vec3(0,0,0); - // If the ray hits nothing, return the background color. - if (!world.hit(r, 0.001, infinity, rec)) - return background; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + bool scatter(const ray& r_in, const hit_record& rec, scatter_record& srec) const override { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + vec3 reflected = reflect(r_in.direction(), rec.normal); + reflected = unit_vector(reflected) + (fuzz * random_unit_vector()); - scatter_record srec; - vec3 emitted = rec.mat_ptr->emitted(r, rec, rec.u, rec.v, rec.p); - if (!rec.mat_ptr->scatter(r, rec, srec)) - return emitted; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - if (srec.is_specular) { - return srec.attenuation - * ray_color(srec.specular_ray, background, world, lights, depth-1); + srec.attenuation = albedo; + srec.pdf_ptr = nullptr; + srec.skip_pdf = true; + srec.skip_pdf_ray = ray(rec.p, reflected, r_in.time()); + + return true; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ } + + private: + color albedo; + double fuzz; + }; + + class dielectric : public material { + public: + dielectric(double refraction_index) : refraction_index(refraction_index) {} + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + bool scatter(const ray& r_in, const hit_record& rec, scatter_record& srec) const override { + srec.attenuation = color(1.0, 1.0, 1.0); + srec.pdf_ptr = nullptr; + srec.skip_pdf = true; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + double ri = rec.front_face ? (1.0/refraction_index) : refraction_index; - shared_ptr<pdf> light_ptr = make_shared<hittable_pdf>(lights, rec.p); - mixture_pdf p(light_ptr, srec.pdf_ptr); + vec3 unit_direction = unit_vector(r_in.direction()); + double cos_theta = std::fmin(dot(-unit_direction, rec.normal), 1.0); + double sin_theta = std::sqrt(1.0 - cos_theta*cos_theta); - ray scattered = ray(rec.p, p.generate(), r.time()); - auto pdf_val = p.value(scattered.direction()); - delete srec.pdf_ptr; + bool cannot_refract = ri * sin_theta > 1.0; + vec3 direction; - return emitted + srec.attenuation - * rec.mat_ptr->scattering_pdf(r, rec, scattered) - * ray_color(scattered, background, world, lights, depth-1) - / pdf_val; - } + if (cannot_refract || reflectance(cos_theta, ri) > random_double()) + direction = reflect(unit_direction, rec.normal); + else + direction = refract(unit_direction, rec.normal, ri); + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + srec.skip_pdf_ray = ray(rec.p, direction, r_in.time()); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + return true; + } + + ... + }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [ray-color-implicit]: <kbd>[main.cc]</kbd> - Ray color function with implicitly-sampled rays] -</div> + [Listing [material-scatter]: <kbd>[material.h]</kbd> The metal and dielectric scatter methods] + +Note that if the fuzziness is nonzero, this surface isn’t really ideally specular, but the implicit +sampling works just like it did before. We're effectively skipping all of our PDF work for the +materials that we're treating specularly. <div class='together'> -We also need to change the block to metal. +`ray_color()` just needs a new case to generate an implicitly sampled ray: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - hittable_list cornell_box(camera& cam, double aspect) { - hittable_list world; + class camera { + ... + private: + ... + color ray_color(const ray& r, int depth, const hittable& world, const hittable& lights) + const { + ... + + if (!rec.mat->scatter(r, rec, srec)) + return color_from_emission; + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + if (srec.skip_pdf) { + return srec.attenuation * ray_color(srec.skip_pdf_ray, depth-1, world, lights); + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + auto light_ptr = make_shared<hittable_pdf>(lights, rec.p); + mixture_pdf p(light_ptr, srec.pdf_ptr); - auto red = make_shared<lambertian>(make_shared<constant_texture>(vec3(0.65, 0.05, 0.05))); - auto white = make_shared<lambertian>(make_shared<constant_texture>(vec3(0.73, 0.73, 0.73))); - auto green = make_shared<lambertian>(make_shared<constant_texture>(vec3(0.12, 0.45, 0.15))); - auto light = make_shared<diffuse_light>(make_shared<constant_texture>(vec3(15, 15, 15))); + ... + } + }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [Listing [ray-color-implicit]: <kbd>[camera.h]</kbd> Ray color function with implicitly-sampled rays] + +</div> - world.add(make_shared<flip_face>(make_shared<yz_rect>(0, 555, 0, 555, 555, green))); - world.add(make_shared<yz_rect>(0, 555, 0, 555, 0, red)); - world.add(make_shared<flip_face>(make_shared<xz_rect>(213, 343, 227, 332, 554, light))); - world.add(make_shared<flip_face>(make_shared<xz_rect>(0, 555, 0, 555, 555, white))); - world.add(make_shared<xz_rect>(0, 555, 0, 555, 0, white)); - world.add(make_shared<flip_face>(make_shared<xy_rect>(0, 555, 0, 555, 555, white))); +We'll check our work by changing a block to metal. We'd also like to swap out one of the blocks for +a glass object, but we'll push that off for the next section. Glass objects are difficult to render +well, so we'd like to make a PDF for them, but we have some more work to do before we're able to do +that. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + int main() { + ... + // Light + world.add(make_shared<quad>(point3(213,554,227), vec3(130,0,0), vec3(0,0,105), light)); + // Box 1 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - shared_ptr<material> aluminum = make_shared<metal>(vec3(0.8, 0.85, 0.88), 0.0); - shared_ptr<hittable> box1 = make_shared<box>(vec3(0,0,0), vec3(165,330,165), aluminum); + shared_ptr<material> aluminum = make_shared<metal>(color(0.8, 0.85, 0.88), 0.0); + shared_ptr<hittable> box1 = box(point3(0,0,0), point3(165,330,165), aluminum); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ box1 = make_shared<rotate_y>(box1, 15); box1 = make_shared<translate>(box1, vec3(265,0,295)); world.add(box1); - shared_ptr<hittable> box2 = make_shared<box>(vec3(0,0,0), vec3(165,165,165), white); + // Box 2 + shared_ptr<hittable> box2 = box(point3(0,0,0), point3(165,165,165), white); box2 = make_shared<rotate_y>(box2, -18); - box2 = make_shared<translate>(box2, vec3(130,0,65); + box2 = make_shared<translate>(box2, vec3(130,0,65)); world.add(box2); - vec3 lookfrom(278, 278, -800); - vec3 lookat(278, 278, 0); - vec3 vup(0, 1, 0); - auto dist_to_focus = 10.0; - auto aperture = 0.0; - auto vfov = 40.0; - auto t0 = 0.0; - auto t1 = 1.0; - - cam = camera(lookfrom, lookat, vup, vfov, aspect, aperture, dist_to_focus, t0, t1); + // Light Sources + auto empty_material = shared_ptr<material>(); + quad lights(point3(343,554,332), vec3(-130,0,0), vec3(0,0,-105), empty_material); - return world; + ... } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [scene-cornell-al]: <kbd>[main.cc]</kbd> Cornell box scene with aluminum material] -</div> <div class='together'> The resulting image has a noisy reflection on the ceiling because the directions toward the box are not sampled with more density. - <div class="render"> - - ![Cornell box with arbitrary PDF functions](../images/img.cornell-flexpdf.jpg) - - </div> + ![<span class='num'>Image 12:</span> Cornell box with arbitrary PDF functions + ](../images/img-3.12-arbitrary-pdf.jpg class='pixel') </div> -<div class='together'> -We could make the PDF include the block. Let’s do that instead with a glass sphere because it’s -easier. When we sample a sphere’s solid angle uniformly from a point outside the sphere, we are -really just sampling a cone uniformly (the cone is tangent to the sphere). Let’s say the code has + +Sampling a Sphere Object +------------------------- +The noisiness on the ceiling could be reduced by making a PDF of the metal block. We would also want +a PDF for the block if we made it glass. But making a PDF for a block is quite a bit of work and +isn't terribly interesting, so let’s create a PDF for a glass sphere instead. It's quicker and makes +for a more interesting render. We need to figure out how to sample a sphere to determine an +appropriate PDF distribution. If we want to sample a sphere from a point outside of the sphere, we +can't just pick a random point on its surface and be done. If we did that, we would frequently pick +a point on the far side of the sphere, which would be occluded by the front side of the sphere. We +need a way to uniformly sample the side of the sphere that is visible from an arbitrary point. When +we sample a sphere’s solid angle uniformly from a point outside the sphere, we are really just +sampling a cone uniformly. The cone axis goes from the ray origin through the sphere center, with +the sides of the cone tangent to the sphere -- see illustration below. Let’s say the code has `theta_max`. Recall from the Generating Random Directions chapter that to sample $\theta$ we have: - $$ r_2 = \int_{0}^{\theta} 2\pi \cdot f(t) \cdot \sin(t) dt $$ + $$ r_2 = \int_{0}^{\theta} 2 \pi f(\theta') \sin(\theta') d\theta' $$ -Here $f(t)$ is an as yet uncalculated constant $C$, so: +Here $f(\theta')$ is an as-of-yet uncalculated constant $C$, so: - $$ r_2 = \int_{0}^{\theta} 2\pi \cdot C \cdot \sin(t) dt $$ + $$ r_2 = \int_{0}^{\theta} 2 \pi C \sin(\theta') d\theta' $$ -Doing some algebra/calculus this yields: +If we solve through the calculus: $$ r_2 = 2\pi \cdot C \cdot (1-\cos(\theta)) $$ -</div> -<div class='together'> So - $$ cos(\theta) = 1 - \frac{r_2}{2 \pi \cdot C} $$ -</div> + $$ \cos(\theta) = 1 - \frac{r_2}{2 \pi \cdot C} $$ -<div class='together'> -We know that for $r_2 = 1$ we should get $\theta_{max}$, so we can solve for $C$: +We are constraining our distribution so that the random direction must be less than $\theta_{max}$. +This means that the integral from 0 to $\theta_{max}$ must be one, and therefore $r_2 = 1$. We can +use this to solve for $C$: + + $$ r_2 = 2\pi \cdot C \cdot (1-\cos(\theta)) $$ + $$ 1 = 2\pi \cdot C \cdot (1-\cos(\theta_{max})) $$ + $$ C = \frac{1}{2\pi \cdot (1-\cos(\theta_{max}))} $$ + +Which gives us an equality between $\theta$, $\theta_{max}$, and $r_2$: $$ \cos(\theta) = 1 + r_2 \cdot (\cos(\theta_{max})-1) $$ -</div> -<div class='together'> -$\phi$ we sample like before, so: +We sample $\phi$ like before, so: $$ z = \cos(\theta) = 1 + r_2 \cdot (\cos(\theta_{max}) - 1) $$ $$ x = \cos(\phi) \cdot \sin(\theta) = \cos(2\pi \cdot r_1) \cdot \sqrt{1-z^2} $$ $$ y = \sin(\phi) \cdot \sin(\theta) = \sin(2\pi \cdot r_1) \cdot \sqrt{1-z^2} $$ -</div> -<div class='together'> Now what is $\theta_{max}$? - ![Figure [sphere-cone]: A sphere enclosing cone](../images/fig.sphere-cone.jpg) + ![Figure [sphere-enclosing-cone]: A sphere-enclosing cone + ](../images/fig-3.12-sphere-enclosing-cone.jpg) -</div> - -<div class='together'> We can see from the figure that $\sin(\theta_{max}) = R / length(\mathbf{c} - \mathbf{p})$. So: $$ \cos(\theta_{max}) = \sqrt{1 - \frac{R^2}{length^2(\mathbf{c} - \mathbf{p})}} $$ -</div> -<div class='together'> -We also need to evaluate the PDF of directions. For directions toward the sphere this is -$1/solid\_angle$. What is the solid angle of the sphere? It has something to do with the $C$ above. -It, by definition, is the area on the unit sphere, so the integral is +We also need to evaluate the PDF of directions. For a uniform distribution toward the sphere the PDF +is $1/\text{solid_angle}$. What is the solid angle of the sphere? It has something to do with +the $C$ above. It is -- by definition -- the area on the unit sphere, so the integral is - $$ solid\_angle = \int_{0}^{2\pi} \int_{0}^{\theta_{max}} \sin(\theta) + $$ \text{solid_angle} = \int_{0}^{2\pi} \int_{0}^{\theta_{max}} \sin(\theta) = 2 \pi \cdot (1-\cos(\theta_{max})) $$ -</div> It’s good to check the math on all such calculations. I usually plug in the extreme cases (thank you for that concept, Mr. Horton -- my high school physics teacher). For a zero radius sphere -$\cos(\theta_{max}) = 0$, and that works. For a sphere tangent at $\mathbf{p}$, +$\cos(\theta_{max}) = 1$, and that works. For a sphere tangent at $\mathbf{p}$, $\cos(\theta_{max}) = 0$, and $2\pi$ is the area of a hemisphere, so that works too. -<div class='together'> + +Updating the Sphere Code +------------------------- The sphere class needs the two PDF-related functions: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - double sphere::pdf_value(const vec3& o, const vec3& v) const { - hit_record rec; - if (!this->hit(ray(o, v), 0.001, infinity, rec)) - return 0; + #include "hittable.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + #include "onb.h" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - auto cos_theta_max = sqrt(1 - radius*radius/(center-o).length_squared()); - auto solid_angle = 2*pi*(1-cos_theta_max); + class sphere : public hittable { + public: + ... - return 1 / solid_angle; - } + aabb bounding_box() const override { return bbox; } - vec3 sphere::random(const vec3& o) const { - vec3 direction = center - o; - auto distance_squared = direction.length_squared(); - onb uvw; - uvw.build_from_w(direction); - return uvw.local(random_to_sphere(radius, distance_squared)); - } - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [sphere-pdf]: <kbd>[sphere.h]</kbd> Sphere with PDF] -</div> -<div class='together'> -With the utility function: + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + double pdf_value(const point3& origin, const vec3& direction) const override { + // This method only works for stationary spheres. + + hit_record rec; + if (!this->hit(ray(origin, direction), interval(0.001, infinity), rec)) + return 0; + + auto dist_squared = (center.at(0) - origin).length_squared(); + auto cos_theta_max = std::sqrt(1 - radius*radius/dist_squared); + auto solid_angle = 2*pi*(1-cos_theta_max); + + return 1 / solid_angle; + } + vec3 random(const point3& origin) const override { + vec3 direction = center.at(0) - origin; + auto distance_squared = direction.length_squared(); + onb uvw(direction); + return uvw.transform(random_to_sphere(radius, distance_squared)); + } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - inline vec3 random_to_sphere(double radius, double distance_squared) { - auto r1 = random_double(); - auto r2 = random_double(); - auto z = 1 + r2*(sqrt(1-radius*radius/distance_squared) - 1); - auto phi = 2*pi*r1; - auto x = cos(phi)*sqrt(1-z*z); - auto y = sin(phi)*sqrt(1-z*z); + private: + ... - return vec3(x, y, z); - } + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + static vec3 random_to_sphere(double radius, double distance_squared) { + auto r1 = random_double(); + auto r2 = random_double(); + auto z = 1 + r2*(std::sqrt(1-radius*radius/distance_squared) - 1); + + auto phi = 2*pi*r1; + auto x = std::cos(phi) * std::sqrt(1-z*z); + auto y = std::sin(phi) * std::sqrt(1-z*z); + + return vec3(x, y, z); + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [rand-to-sphere]: <kbd>[pdf.h]</kbd> The random_to_sphere utility function] -</div> + [Listing [sphere-pdf]: <kbd>[sphere.h]</kbd> Sphere with PDF] <div class='together'> We can first try just sampling the sphere rather than the light: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - shared_ptr<hittable> light_shape = make_shared<xz_rect>(213, 343, 227, 332, 554, 0); - shared_ptr<hittable> glass_sphere = make_shared<sphere>(vec3(190, 90, 190), 90, 0); + int main() { + ... - for (int j = image_height-1; j >= 0; --j) { - std::cerr << "\rScanlines remaining: " << j << ' ' << std::flush; - for (int i = 0; i < image_width; ++i) { - vec3 col(0, 0, 0); - for (int s=0; s < ns; ++s) { - auto u = (i + random_double()) / image_width; - auto v = (j + random_double()) / image_height; - ray r = cam->get_ray(u, v); - col += ray_color(r, background, world, glass_sphere, max_depth); - } + // Light + world.add(make_shared<quad>(point3(213,554,227), vec3(130,0,0), vec3(0,0,105), light)); + + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + // Box + shared_ptr<hittable> box1 = box(point3(0,0,0), point3(165,330,165), white); + box1 = make_shared<rotate_y>(box1, 15); + box1 = make_shared<translate>(box1, vec3(265,0,295)); + world.add(box1); + + // Glass Sphere + auto glass = make_shared<dielectric>(1.5); + world.add(make_shared<sphere>(point3(190,90,190), 90, glass)); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + // Light Sources + auto empty_material = shared_ptr<material>(); + quad lights(point3(343,554,332), vec3(-130,0,0), vec3(0,0,-105), empty_material); + + ... + } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [sampling-sphere]: <kbd>[main.cc]</kbd> Sampling just the sphere] + </div> <div class='together'> -This yields a noisy box, but the caustic under the sphere is good. It took five times as long as +This yields a noisy room, but the caustic under the sphere is good. It took five times as long as sampling the light did for my code. This is probably because those rays that hit the glass are expensive! - <div class="render"> - - ![Cornell box with glass sphere, using new PDF functions](../images/img.cornell-glass.jpg) - - </div> + ![<span class='num'>Image 13:</span> Cornell box with glass sphere, using new PDF functions + ](../images/img-3.13-cornell-glass-sphere.jpg class='pixel') </div> -<div class='together'> + +Adding PDF Functions to Hittable Lists +--------------------------------------- We should probably just sample both the sphere and the light. We can do that by creating a mixture -density of their two densities. We could do that in the `ray_color()` function by passing a list of -hittables in and building a mixture PDF, or we could add PDF functions to `hittable_list`. I +density of their two distributions. We could do that in the `ray_color()` function by passing a list +of hittables in and building a mixture PDF, or we could add PDF functions to `hittable_list`. I think both tactics would work fine, but I will go with instrumenting `hittable_list`. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - double hittable_list::pdf_value(const vec3& o, const vec3& v) const { - auto weight = 1.0/objects.size(); - auto sum = 0.0; + class hittable_list : public hittable { + public: + ... - for (const auto& object : objects) - sum += weight * object->pdf_value(o, v); + aabb bounding_box() const override { return bbox; } - return sum; - } - vec3 hittable_list::random(const vec3& o) const { - auto int_size = static_cast<int>(objects.size()); - return objects[random_int(0, int_size-1)]->random(o); - } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + double pdf_value(const point3& origin, const vec3& direction) const override { + auto weight = 1.0 / objects.size(); + auto sum = 0.0; + + for (const auto& object : objects) + sum += weight * object->pdf_value(origin, direction); + + return sum; + } + + vec3 random(const point3& origin) const override { + auto int_size = int(objects.size()); + return objects[random_int(0, int_size-1)]->random(origin); + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + ... + }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [density-mixture]: <kbd>[hittable_list.h]</kbd> Creating a mixture of densities] -</div> <div class='together'> -We assemble a list to pass to `ray_color()` `from main()`: +We assemble a list of light sources to pass to `camera::render()`: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - hittable_list lights; - lights.add(make_shared<xz_rect>(213, 343, 227, 332, 554, 0)); - lights.add(make_shared<sphere>(vec3(190, 90, 190), 90, 0)); + int main() { + ... + + // Light Sources + auto empty_material = shared_ptr<material>(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight + hittable_list lights; + lights.add( + make_shared<quad>(point3(343,554,332), vec3(-130,0,0), vec3(0,0,-105), empty_material)); + lights.add(make_shared<sphere>(point3(190, 90, 190), 90, empty_material)); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ + + ... + } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [scene-density-mixture]: <kbd>[main.cc]</kbd> Updating the scene] + </div> <div class='together'> And we get a decent image with 1000 samples as before: - <div class="render"> - - ![Cornell box, using a mixture of glass & light PDFs](../images/img.cornell-glasslight.jpg) - - </div> + ![<span class='num'>Image 14:</span> Cornell box using a mixture of glass & light PDFs + ](../images/img-3.14-glass-and-light.jpg class='pixel') </div> -<div class='together'> + +Handling Surface Acne +---------------------- An astute reader pointed out there are some black specks in the image above. All Monte Carlo Ray Tracers have this as a main loop: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ pixel_color = average(many many samples) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -</div> -If you find yourself getting some form of acne in the images, and this acne is white or black, so -one "bad" sample seems to kill the whole pixel, that sample is probably a huge number or a `NaN`. -This particular acne is probably a `NaN`. Mine seems to come up once in every 10–100 million rays -or so. +If you find yourself getting some form of acne in your renders, and this acne is white or black -- +where one "bad" sample seems to kill the whole pixel -- then that sample is probably a huge number +or a `NaN` (Not A Number). This particular acne is probably a `NaN`. Mine seems to come up once in +every 10–100 million rays or so. <div class='together'> So big decision: sweep this bug under the rug and check for `NaN`s, or just kill `NaN`s and hope this doesn't come back to bite us later. I will always opt for the lazy strategy, especially when I -know floating point is hard. First, how do we check for a `NaN`? The one thing I always remember -for `NaN`s is that any `if` test with a `NaN` in it is false. Using this trick, we update the -`vec3::write_color()` function to replace any NaN components with zero: +know that working with floating point is hard. First, how do we check for a `NaN`? The one thing I +always remember for `NaN`s is that a `NaN` does not equal itself. Using this trick, we update the +`write_color()` function to replace any `NaN` components with zero: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - void write_color(std::ostream &out, int samples_per_pixel) { + void write_color(std::ostream& out, const color& pixel_color) { + auto r = pixel_color.x(); + auto g = pixel_color.y(); + auto b = pixel_color.z(); + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight - // Replace NaN component values with zero. - if (e[0] != e[0]) e[0] = 0.0; - if (e[1] != e[1]) e[1] = 0.0; - if (e[2] != e[2]) e[2] = 0.0; + // Replace NaN components with zero. + if (r != r) r = 0.0; + if (g != g) g = 0.0; + if (b != b) b = 0.0; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ - // Divide the color total by the number of samples and gamma-correct - // for a gamma value of 2.0. - auto scale = 1.0 / samples_per_pixel; - auto r = sqrt(scale * e[0]); - auto g = sqrt(scale * e[1]); - auto b = sqrt(scale * e[2]); + // Apply a linear to gamma transform for gamma 2 + r = linear_to_gamma(r); + g = linear_to_gamma(g); + b = linear_to_gamma(b); - // Write the translated [0,255] value of each color component. - out << static_cast<int>(256 * clamp(r, 0.0, 0.999)) << ' ' - << static_cast<int>(256 * clamp(g, 0.0, 0.999)) << ' ' - << static_cast<int>(256 * clamp(b, 0.0, 0.999)) << '\n'; + // Translate the [0,1] component values to the byte range [0,255]. + static const interval intensity(0.000, 0.999); + int rbyte = int(256 * intensity.clamp(r)); + int gbyte = int(256 * intensity.clamp(g)); + int bbyte = int(256 * intensity.clamp(b)); + + // Write out the pixel color components. + out << rbyte << ' ' << gbyte << ' ' << bbyte << '\n'; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - [Listing [write-color-nan]: <kbd>[vec3.h]</kbd> NaN-tolerant write_color function] + [Listing [write-color-nan]: <kbd>[color.h]</kbd> NaN-tolerant write_color function] + </div> <div class='together'> Happily, the black specks are gone: - <div class="render"> - - ![Cornell box with anti-acne color function](../images/img.book3-final.jpg) - - </div> + ![<span class='num'>Image 15:</span> Cornell box with anti-acne color function + ](../images/img-3.15-book3-final.jpg class='pixel') </div> @@ -2365,10 +3969,9 @@ The Rest of Your Life ==================================================================================================== - -The purpose of this book was to show the details of dotting all the i’s of the math on one way of -organizing a physically based renderer’s sampling approach. Now you can explore a lot of different -potential paths. +The purpose of this book was to walk through all of the little details (dotting all the i's and +crossing all of the t's) necessary when organizing a physically based renderer’s sampling approach. +You should now be able to take all of this detail and explore a lot of different potential paths. If you want to explore Monte Carlo methods, look into bidirectional and path spaced approaches such as Metropolis. Your probability space won't be over solid angle, but will instead be over path @@ -2381,8 +3984,8 @@ If you want to do movie renderers, look at the papers out of studios and Solid Angle. They are surprisingly open about their craft. -If you want to do high-performance ray tracing, look first at papers from Intel and NVIDIA. Again, -they are surprisingly open. +If you want to do high-performance ray tracing, look first at papers from Intel and NVIDIA. They are +also surprisingly open. If you want to do hard-core physically based renderers, convert your renderer from RGB to spectral. I am a big fan of each ray having a random wavelength and almost all the RGBs in your program @@ -2393,7 +3996,7 @@ Have fun! -Peter Shirley<br> +[Peter Shirley][]<br> Salt Lake City, March, 2016 @@ -2402,9 +4005,93 @@ +Citing This Book +==================================================================================================== +Consistent citations make it easier to identify the source, location and versions of this work. If +you are citing this book, we ask that you try to use one of the following forms if possible. + +Basic Data +----------- + - **Title (series)**: “Ray Tracing in One Weekend Series” + - **Title (book)**: “Ray Tracing: The Rest of Your Life” + - **Author**: Peter Shirley, Trevor David Black, Steve Hollasch + - **Version/Edition**: (v4.0.2) + - **Date**: 2025-04-25 + - **URL (series)**: <https://raytracing.github.io/> + - **URL (book)**: <https://raytracing.github.io/books/RayTracingTheRestOfYourLife.html> + +Snippets +--------- + + ### Markdown + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + [_Ray Tracing: The Rest of Your Life_](https://raytracing.github.io/books/RayTracingTheRestOfYourLife.html) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + ### HTML + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + <a href='https://raytracing.github.io/books/RayTracingTheRestOfYourLife.html'> + <cite>Ray Tracing: The Rest of Your Life</cite> + </a> + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + ### LaTeX and BibTex + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ~\cite{Shirley2025} + + @misc{Shirley2025, + title = {Ray Tracing: The Rest of Your Life}, + author = {Peter Shirley, Trevor David Black, Steve Hollasch}, + year = {2025}, + month = {April}, + note = {\small \texttt{https://raytracing.github.io/books/RayTracingTheRestOfYourLife.html}}, + url = {https://raytracing.github.io/books/RayTracingTheRestOfYourLife.html} + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + ### BibLaTeX + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + \usepackage{biblatex} + + ~\cite{Shirley2025} + + @online{Shirley2025, + title = {Ray Tracing: The Rest of Your Life}, + author = {Peter Shirley, Trevor David Black, Steve Hollasch}, + year = {2025}, + month = {April}, + url = {https://raytracing.github.io/books/RayTracingTheRestOfYourLife.html} + } + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + ### IEEE + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + “Ray Tracing: The Rest of Your Life.” + raytracing.github.io/books/RayTracingTheRestOfYourLife.html + (accessed MMM. DD, YYYY) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + ### MLA: + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Ray Tracing: The Rest of Your Life. raytracing.github.io/books/RayTracingTheRestOfYourLife.html + Accessed DD MMM. YYYY. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + + +[Peter Shirley]: https://github.com/petershirley +[Steve Hollasch]: https://github.com/hollasch +[Trevor David Black]: https://github.com/trevordblack + +[readme]: ../README.md +[releases]: https://github.com/RayTracing/raytracing.github.io/releases/ +[wiki-further]: https://github.com/RayTracing/raytracing.github.io/wiki/Further-Readings + + + <!-- Markdeep: https://casual-effects.com/markdeep/ --> <link rel='stylesheet' href='../style/book.css'> <style class="fallback">body{visibility:hidden;white-space:pre;font-family:monospace}</style> <script src="markdeep.min.js"></script> -<script src="https://casual-effects.com/markdeep/latest/markdeep.min.js"></script> +<script src="https://morgan3d.github.io/markdeep/latest/markdeep.min.js"></script> <script>window.alreadyProcessedMarkdeep||(document.body.style.visibility="visible")</script> diff --git a/books/acknowledgments.md.html b/books/acknowledgments.md.html index 27ec874f0..bd7c518c6 100644 --- a/books/acknowledgments.md.html +++ b/books/acknowledgments.md.html @@ -12,49 +12,71 @@ <div class="credit-list"> **Web Release** - - Berna Kabadayı - - Lorenzo Mancini - - Lori Whippler Hollasch - - Ronald Wotzlaw + - [Berna Kabadayı](https://github.com/bernakabadayi) + - [Lorenzo Mancini](https://github.com/lmancini) + - [Lori Whippler Hollasch](https://github.com/lorihollasch) + - [Ronald Wotzlaw](https://github.com/ronaldfw) </div> <div class="credit-list"> **Corrections and Improvements** - - Aaryaman Vasishta + - [Aaryaman Vasishta](https://github.com/jammm) - Andrew Kensler + - [Antonio Gamiz](https://github.com/antoniogamiz) - Apoorva Joshi - - Aras Pranckevičius + - [Aras Pranckevičius](https://github.com/aras-p) + - [Arman Uguray](https://github.com/armansito) - Becker - Ben Kerl - Benjamin Summerton - Bennett Hardwick + - [Benny Tsang](https://bthtsang.github.io/) - Dan Drummond - - David Chambers + - [David Chambers](https://github.com/dafhi) - David Hart - - Eric Haines + - [Dimitry Ishenko](https://github.com/dimitry-ishenko) + - [Dmitry Lomov](https://github.com/mu-lambda) + - [Eric Haines](https://github.com/erich666) - Fabio Sancinetti - Filipe Scur - Frank He - - Gerrit Wessendorf + - [Gareth Martin](https://github.com/TheThief) + - [Gerrit Wessendorf](https://github.com/celeph) - Grue Debry + - [Gustaf Waldemarson](https://github.com/xaldew) - Ingo Wald - Jason Stone + - [JC-ProgJava](https://github.com/JC-ProgJava) - Jean Buckley + - [Jeff Smith](https://github.com/whydoubt) - Joey Cho - - Lorenzo Mancini + - [John Kilpatrick](https://github.com/rjkilpatrick) + - [Kaan Eraslan](https://github.com/D-K-E) + - [Lorenzo Mancini](https://github.com/lmancini) + - [Manas Kale](https://github.com/manas96) - Marcus Ottosson + - [Mark Craig](https://github.com/mrmcsoftware) + - Markus Boos - Matthew Heimlich - Nakata Daisuke + - [Nate Rupsis](https://github.com/rupsis) + - [Niccolò Tiezzi](https://github.com/niccolot) - Paul Melis - Phil Cristensen - - Ronald Wotzlaw - - Shaun P. Lee + - [LollipopFt](https://github.com/LollipopFt) + - [Ronald Wotzlaw](https://github.com/ronaldfw) + - [Shaun P. Lee](https://github.com/shaunplee) + - [Shota Kawajiri](https://github.com/estshorter) - Tatsuya Ogawa - Thiago Ize + - [Thien Tran](https://github.com/gau-nernst) - Vahan Sosoyan + - [WANG Lei](https://github.com/wlbksy) + - [Yann Herklotz](https://github.com/ymherklotz) + - [ZeHao Chen](https://github.com/oxine) </div> -<div class="credit-list"> **Tools** +<div class="credit-list"> **Special Thanks** <div class="indented"> Thanks to the team at [Limnu](https://limnu.com/) for help on the figures. @@ -63,6 +85,9 @@ [Markdeep](https://casual-effects.com/markdeep/) library. To see what this looks like, view the page source from your browser. + Thanks to [Helen Hu](https://github.com/hhu) for graciously donating her + https://github.com/RayTracing/ GitHub organization to this project. + </div> </div> @@ -72,5 +97,5 @@ <link rel='stylesheet' href='../style/book.css'> <style class="fallback">body{visibility:hidden;white-space:pre;font-family:monospace}</style> <script src="markdeep.min.js"></script> -<script src="https://casual-effects.com/markdeep/latest/markdeep.min.js"></script> +<script src="https://morgan3d.github.io/markdeep/latest/markdeep.min.js"></script> <script>window.alreadyProcessedMarkdeep||(document.body.style.visibility="visible")</script> diff --git a/books/markdeep.min.js b/books/markdeep.min.js index c2e370f98..6ca938621 100644 --- a/books/markdeep.min.js +++ b/books/markdeep.min.js @@ -1,1348 +1,10 @@ - - - - - - -<!DOCTYPE html> -<html lang="en"> - <head> - <meta charset="utf-8"> - <link rel="dns-prefetch" href="https://github.githubassets.com"> - <link rel="dns-prefetch" href="https://avatars0.githubusercontent.com"> - <link rel="dns-prefetch" href="https://avatars1.githubusercontent.com"> - <link rel="dns-prefetch" href="https://avatars2.githubusercontent.com"> - <link rel="dns-prefetch" href="https://avatars3.githubusercontent.com"> - <link rel="dns-prefetch" href="https://github-cloud.s3.amazonaws.com"> - <link rel="dns-prefetch" href="https://user-images.githubusercontent.com/"> - - - - <link crossorigin="anonymous" media="all" integrity="sha512-FG+rXqMOivrAjdEQE7tO4BwM1poGmg70hJFTlNSxjX87grtrZ6UnPR8NkzwUHlQEGviu9XuRYeO8zH9YwvZhdg==" rel="stylesheet" href="https://github.githubassets.com/assets/frameworks-146fab5ea30e8afac08dd11013bb4ee0.css" /> - - <link crossorigin="anonymous" media="all" integrity="sha512-vMKRtbQ9h8VmzccMNdmnlBnTLM9zZar8f9BKU3A5UNRZgr3o2+zXRScLx7V1nd9HupewEuevhEx2D3yuqNpkXw==" rel="stylesheet" href="https://github.githubassets.com/assets/github-bcc291b5b43d87c566cdc70c35d9a794.css" /> - - - - - - - <meta name="viewport" content="width=device-width"> - - <title>markdeep/markdeep.min.js at master · morgan3d/markdeep - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Skip to content - - - - - - - - - - - - - -
- -
- - -
- - - -
- - - - - - - - - -
-
-
- - - - - - - - - - - - - - -
- -
- -
-

- - - / - - markdeep - - -

- - -
- -
    - -
  • - -
  • - - - - -
  • - -
    - -
    - - - Watch - - -
    - Notifications -
    -
    - - - - - - - -
    -
    -
    - -
    -
  • - -
  • -
    -
    - - -
    -
    - - -
    - -
  • - -
  • -
    -
    - - - Fork - - - -
    - -

    Fork markdeep

    -
    -
    - -
    - -
    -

    If this dialog fails to load, you can visit the fork page directly.

    -
    -
    - -
    -
    - - -
    -
    - - -
  • -
- -
- - - - - - -
- - - - - - -
-
- - - - - - - - Permalink - - - - -
- - -
- - Branch: - master - - - - -
- - - -
-
-
- -
- - Find file - - - Copy path - -
-
- - -
- - Find file - - - Copy path - -
-
- - - - -
- Fetching contributors… -
- -
- - Cannot retrieve contributors at this time -
-
- - - - - -
- -
-
- - 4 lines (4 sloc) - - 143 KB -
- -
- -
- Raw - Blame - History -
- - -
- - - - -
- -
-
- -
-
-
- - -
-
-
-
-

Code navigation is available!

-
-

- Navigate your code with ease. Click on function and method calls to jump to their definitions or references in the same repository. - Learn more -

-
-
-
-
-
-
- - - -
- - - - - - - - - - - - - - - - - - -
/**See https://casual-effects.com/markdeep for @license and documentation.
markdeep.min.js 1.10 (C) 2020 Morgan McGuire
highlight.min.js 10.0.0-beta.0 (C) 2020 Ivan Sagalaev https://highlightjs.org */
!function(){"use strict";var e=String.prototype;e.rp=e.replace,e.ss=e.substring;var t,r,P=!(e.regexIndexOf=function(e,t){var r=this.ss(t||0).search(e);return 0<=r?r+(t||0):r}),V=P,H=V,W=2,x="*",_=Array(6).join(x);function z(e,t,r){return"<"+e+(r?" "+r:"")+">"+t+"</"+e+">"}t=function(a){function y(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}function u(e){return e.nodeName.toLowerCase()}function v(e,t){var r=e&&e.exec(t);return r&&0===r.index}function p(e){return n.test(e)}function c(e){var t,r={},n=Array.prototype.slice.call(arguments,1);for(t in e)r[t]=e[t];return n.forEach(function(e){for(t in e)r[t]=e[t]}),r}function m(e){var a=[];return function e(t,r){for(var n=t.firstChild;n;n=n.nextSibling)3===n.nodeType?r+=n.nodeValue.length:1===n.nodeType&&(a.push({event:"start",offset:r,node:n}),r=e(n,r),u(n).match(/br|hr|img|input/)||a.push({event:"stop",offset:r,node:n}));return r}(e,0),a}function g(e,t,r){function n(){return e.length&&t.length?e[0].offset!==t[0].offset?e[0].offset<t[0].offset?e:t:"start"===t[0].event?e:t:e.length?e:t}function a(e){l+="<"+u(e)+f.map.call(e.attributes,function(e){return" "+e.nodeName+'="'+y(e.value).replace('"',"&quot;")+'"'}).join("")+">"}function i(e){l+="</"+u(e)+">"}function s(e){("start"===e.event?a:i)(e.node)}for(var o=0,l="",c=[];e.length||t.length;){var d=n();if(l+=y(r.substring(o,d[0].offset)),o=d[0].offset,d===e){for(c.reverse().forEach(i);s(d.splice(0,1)[0]),(d=n())===e&&d.length&&d[0].offset===o;);c.reverse().forEach(a)}else"start"===d[0].event?c.push(d[0].node):c.pop(),s(d.splice(0,1)[0])}return l+y(r.substr(o))}function x(s){function o(e){return e&&e.source||e}function l(e,t){return new RegExp(o(e),"m"+(s.cI?"i":"")+(t?"g":""))}!function t(r,e){if(!r.compiled){if(r.compiled=!0,r.k=r.k||r.bK,r.k){var n={},a=function(r,e){s.cI&&(e=e.toLowerCase()),e.split(" ").forEach(function(e){var t=e.split("|");n[t[0]]=[r,t[1]?Number(t[1]):1]})};"string"==typeof r.k?a("keyword",r.k):d(r.k).forEach(function(e){a(e,r.k[e])}),r.k=n}r.lR=l(r.l||/\w+/,!0),e&&(r.bK&&(r.b="\\b("+r.bK.split(" ").join("|")+")\\b"),r.b||(r.b=/\B|\b/),r.bR=l(r.b),r.endSameAsBegin&&(r.e=r.b),r.e||r.eW||(r.e=/\B|\b/),r.e&&(r.eR=l(r.e)),r.tE=o(r.e)||"",r.eW&&e.tE&&(r.tE+=(r.e?"|":"")+e.tE)),r.i&&(r.iR=l(r.i)),null==r.r&&(r.r=1),r.c||(r.c=[]),r.c=Array.prototype.concat.apply([],r.c.map(function(e){return(t="self"===e?r:e).v&&!t.cached_variants&&(t.cached_variants=t.v.map(function(e){return c(t,{v:null},e)})),t.cached_variants||t.eW&&[c(t)]||[t];var t})),r.c.forEach(function(e){t(e,r)}),r.starts&&t(r.starts,e);var i=r.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([r.tE,r.i]).map(o).filter(Boolean);r.t=i.length?l(i.join("|"),!0):{exec:function(){return null}}}}(s)}function _(e,t,o,r){function l(e,t,r,n){var a='<span class="'+(n?"":A.classPrefix);return(a+=e+'">')+t+(r?"":M)}function c(){m+=(null!=p.sL?function(){var e="string"==typeof p.sL;if(e&&!C[p.sL])return y(g);var t=e?_(p.sL,g,!0,i[p.sL]):w(g,p.sL.length?p.sL:void 0);return 0<p.r&&(b+=t.r),e&&(i[p.sL]=t.top),l(t.language,t.value,!1,!0)}:function(){var e,t,r,n,a,i,s;if(!p.k)return y(g);for(n="",p.lR.lastIndex=t=0,r=p.lR.exec(g);r;)n+=y(g.substring(t,r.index)),a=p,i=r,s=u.cI?i[0].toLowerCase():i[0],(e=a.k.hasOwnProperty(s)&&a.k[s])?(b+=e[1],n+=l(e[0],y(r[0]))):n+=y(r[0]),t=p.lR.lastIndex,r=p.lR.exec(g);return n+y(g.substr(t))})(),g=""}function d(e){m+=e.cN?l(e.cN,"",!0):"",p=Object.create(e,{parent:{value:p}})}function n(e,t){if(g+=e,null==t)return c(),0;var r=function(e,t){var r,n,a;for(r=0,n=t.c.length;r<n;r++)if(v(t.c[r].bR,e))return t.c[r].endSameAsBegin&&(t.c[r].eR=(a=t.c[r].bR.exec(e)[0],new RegExp(a.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&"),"m"))),t.c[r]}(t,p);if(r)return r.skip?g+=t:(r.eB&&(g+=t),c(),r.rB||r.eB||(g=t)),d(r),r.rB?0:t.length;var n,a,i=function e(t,r){if(v(t.eR,r)){for(;t.endsParent&&t.parent;)t=t.parent;return t}return t.eW?e(t.parent,r):void 0}(p,t);if(i){var s=p;for(s.skip?g+=t:(s.rE||s.eE||(g+=t),c(),s.eE&&(g=t));p.cN&&(m+=M),p.skip||p.sL||(b+=p.r),(p=p.parent)!==i.parent;);return i.starts&&(i.endSameAsBegin&&(i.starts.eR=i.eR),d(i.starts)),s.rE?0:t.length}if(n=t,a=p,!o&&v(a.iR,n))throw new Error('Illegal lexeme "'+t+'" for mode "'+(p.cN||"<unnamed>")+'"');return g+=t,t.length||1}var u=k(e);if(!u)throw new Error('Unknown language: "'+e+'"');x(u);var a,p=r||u,i={},m="";for(a=p;a!==u;a=a.parent)a.cN&&(m=l(a.cN,"",!0)+m);var g="",b=0;try{for(var s,f,h=0;p.t.lastIndex=h,s=p.t.exec(t);)f=n(t.substring(h,s.index),s[0]),h=s.index+f;for(n(t.substr(h)),a=p;a.parent;a=a.parent)a.cN&&(m+=M);return{r:b,value:m,language:e,top:p}}catch(e){if(e.message&&-1!==e.message.indexOf("Illegal"))return{r:0,value:y(t)};throw e}}function w(r,e){e=e||A.languages||d(C);var n={r:0,value:y(r)},a=n;return e.filter(k).filter(i).forEach(function(e){var t=_(e,r,!1);t.language=e,a.r<t.r&&(a=t),n.r<t.r&&(a=n,n=t)}),a.language&&(n.second_best=a),n}function b(e){return A.tabReplace||A.useBR?e.replace(s,function(e,t){return A.useBR&&"\n"===e?"<br>":A.tabReplace?t.replace(/\t/g,A.tabReplace):""}):e}function t(e){var t,r,n,a,i,s,o,l,c,d,u=function(e){var t,r,n,a,i=e.className+" ";if(r=N.exec(i+=e.parentNode?e.parentNode.className:""))return k(r[1])?r[1]:"no-highlight";for(t=0,n=(i=i.split(/\s+/)).length;t<n;t++)if(p(a=i[t])||k(a))return a}(e);p(u)||(A.useBR?(t=document.createElementNS("http://www.w3.org/1999/xhtml","div")).innerHTML=e.innerHTML.replace(/\n/g,"").replace(/<br[ \/]*>/g,"\n"):t=e,i=t.textContent,n=u?_(u,i,!0):w(i),(r=m(t)).length&&((a=document.createElementNS("http://www.w3.org/1999/xhtml","div")).innerHTML=n.value,n.value=g(r,m(a),i)),n.value=b(n.value),e.innerHTML=n.value,e.className=(l=n.language,c=(o=u)?h[o]:l,d=[(s=e.className).trim()],s.match(/\bhljs\b/)||d.push("hljs"),-1===s.indexOf(c)&&d.push(c),d.join(" ").trim()),e.result={language:n.language,re:n.r},n.second_best&&(e.second_best={language:n.second_best.language,re:n.second_best.r}))}function r(){if(!r.called){r.called=!0;var e=document.querySelectorAll("pre code");f.forEach.call(e,t)}}function k(e){return e=(e||"").toLowerCase(),C[e]||C[h[e]]}function i(e){var t=k(e);return t&&!t.disableAutodetect}var f=[],d=Object.keys,C={},h={},n=/^(no-?highlight|plain|text)$/i,N=/\blang(?:uage)?-([\w-]+)\b/i,s=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,M="</span>",A={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0};return a.highlight=_,a.highlightAuto=w,a.fixMarkup=b,a.highlightBlock=t,a.configure=function(e){A=c(A,e)},a.initHighlighting=r,a.initHighlightingOnLoad=function(){addEventListener("DOMContentLoaded",r,!1),addEventListener("load",r,!1)},a.registerLanguage=function(t,e){var r=C[t]=e(a);r.aliases&&r.aliases.forEach(function(e){h[e]=t})},a.listLanguages=function(){return d(C)},a.getLanguage=k,a.autoDetection=i,a.inherit=c,a.IR="[a-zA-Z]\\w*",a.UIR="[a-zA-Z_]\\w*",a.NR="\\b\\d+(\\.\\d+)?",a.CNR="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",a.BNR="\\b(0b[01]+)",a.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",a.BE={b:"\\\\[\\s\\S]",r:0},a.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[a.BE]},a.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[a.BE]},a.PWM={b:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},a.C=function(e,t,r){var n=a.inherit({cN:"comment",b:e,e:t,c:[]},r||{});return n.c.push(a.PWM),n.c.push({cN:"doctag",b:"(?:TODO|FIXME|NOTE|BUG|XXX):",r:0}),n},a.CLCM=a.C("//","$"),a.CBCM=a.C("/\\*","\\*/"),a.HCM=a.C("#","$"),a.NM={cN:"number",b:a.NR,r:0},a.CNM={cN:"number",b:a.CNR,r:0},a.BNM={cN:"number",b:a.BNR,r:0},a.CSSNM={cN:"number",b:a.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},a.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[a.BE,{b:/\[/,e:/\]/,r:0,c:[a.BE]}]},a.TM={cN:"title",b:a.IR,r:0},a.UTM={cN:"title",b:a.UIR,r:0},a.METHOD_GUARD={b:"\\.\\s*"+a.UIR,r:0},a.registerLanguage("armasm",function(e){return{cI:!0,aliases:["arm"],l:"\\.?"+e.IR,k:{meta:".2byte .4byte .align .ascii .asciz .balign .byte .code .data .else .end .endif .endm .endr .equ .err .exitm .extern .global .hword .if .ifdef .ifndef .include .irp .long .macro .rept .req .section .set .skip .space .text .word .arm .thumb .code16 .code32 .force_thumb .thumb_func .ltorg ALIAS ALIGN ARM AREA ASSERT ATTR CN CODE CODE16 CODE32 COMMON CP DATA DCB DCD DCDU DCDO DCFD DCFDU DCI DCQ DCQU DCW DCWU DN ELIF ELSE END ENDFUNC ENDIF ENDP ENTRY EQU EXPORT EXPORTAS EXTERN FIELD FILL FUNCTION GBLA GBLL GBLS GET GLOBAL IF IMPORT INCBIN INCLUDE INFO KEEP LCLA LCLL LCLS LTORG MACRO MAP MEND MEXIT NOFP OPT PRESERVE8 PROC QN READONLY RELOC REQUIRE REQUIRE8 RLIST FN ROUT SETA SETL SETS SN SPACE SUBT THUMB THUMBX TTL WHILE WEND ",built_in:"r0 r1 r2 r3 r4 r5 r6 r7 r8 r9 r10 r11 r12 r13 r14 r15 pc lr sp ip sl sb fp a1 a2 a3 a4 v1 v2 v3 v4 v5 v6 v7 v8 f0 f1 f2 f3 f4 f5 f6 f7 p0 p1 p2 p3 p4 p5 p6 p7 p8 p9 p10 p11 p12 p13 p14 p15 c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 c10 c11 c12 c13 c14 c15 q0 q1 q2 q3 q4 q5 q6 q7 q8 q9 q10 q11 q12 q13 q14 q15 cpsr_c cpsr_x cpsr_s cpsr_f cpsr_cx cpsr_cxs cpsr_xs cpsr_xsf cpsr_sf cpsr_cxsf spsr_c spsr_x spsr_s spsr_f spsr_cx spsr_cxs spsr_xs spsr_xsf spsr_sf spsr_cxsf s0 s1 s2 s3 s4 s5 s6 s7 s8 s9 s10 s11 s12 s13 s14 s15 s16 s17 s18 s19 s20 s21 s22 s23 s24 s25 s26 s27 s28 s29 s30 s31 d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 d10 d11 d12 d13 d14 d15 d16 d17 d18 d19 d20 d21 d22 d23 d24 d25 d26 d27 d28 d29 d30 d31 {PC} {VAR} {TRUE} {FALSE} {OPT} {CONFIG} {ENDIAN} {CODESIZE} {CPU} {FPU} {ARCHITECTURE} {PCSTOREOFFSET} {ARMASM_VERSION} {INTER} {ROPI} {RWPI} {SWST} {NOSWST} . @"},c:[{cN:"keyword",b:"\\b(adc|(qd?|sh?|u[qh]?)?add(8|16)?|usada?8|(q|sh?|u[qh]?)?(as|sa)x|and|adrl?|sbc|rs[bc]|asr|b[lx]?|blx|bxj|cbn?z|tb[bh]|bic|bfc|bfi|[su]bfx|bkpt|cdp2?|clz|clrex|cmp|cmn|cpsi[ed]|cps|setend|dbg|dmb|dsb|eor|isb|it[te]{0,3}|lsl|lsr|ror|rrx|ldm(([id][ab])|f[ds])?|ldr((s|ex)?[bhd])?|movt?|mvn|mra|mar|mul|[us]mull|smul[bwt][bt]|smu[as]d|smmul|smmla|mla|umlaal|smlal?([wbt][bt]|d)|mls|smlsl?[ds]|smc|svc|sev|mia([bt]{2}|ph)?|mrr?c2?|mcrr2?|mrs|msr|orr|orn|pkh(tb|bt)|rbit|rev(16|sh)?|sel|[su]sat(16)?|nop|pop|push|rfe([id][ab])?|stm([id][ab])?|str(ex)?[bhd]?|(qd?)?sub|(sh?|q|u[qh]?)?sub(8|16)|[su]xt(a?h|a?b(16)?)|srs([id][ab])?|swpb?|swi|smi|tst|teq|wfe|wfi|yield)(eq|ne|cs|cc|mi|pl|vs|vc|hi|ls|ge|lt|gt|le|al|hs|lo)?[sptrx]?",e:"\\s"},e.C("[;@]","$",{r:0}),e.CBCM,e.QSM,{cN:"string",b:"'",e:"[^\\\\]'",r:0},{cN:"title",b:"\\|",e:"\\|",i:"\\n",r:0},{cN:"number",v:[{b:"[#$=]?0x[0-9a-f]+"},{b:"[#$=]?0b[01]+"},{b:"[#$=]\\d+"},{b:"\\b\\d+"}],r:0},{cN:"symbol",v:[{b:"^[a-z_\\.\\$][a-z0-9_\\.\\$]+"},{b:"^\\s*[a-z_\\.\\$][a-z0-9_\\.\\$]+:"},{b:"[=#]\\w+"}],r:0}]}}),a.registerLanguage("bash",function(e){var t={cN:"variable",v:[{b:/\$[\w\d#@][\w\d_]*/},{b:/\$\{(.*?)}/}]},r={cN:"string",b:/"/,e:/"/,c:[e.BE,t,{cN:"variable",b:/\$\(/,e:/\)/,c:[e.BE]}]};return{aliases:["sh","zsh"],l:/\b-?[a-z\._]+\b/,k:{keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp",_:"-ne -eq -lt -gt -f -d -e -s -l -a"},c:[{cN:"meta",b:/^#![^\n]+sh\s*$/,r:10},{cN:"function",b:/\w[\w\d_]*\s*\(\s*\)\s*\{/,rB:!0,c:[e.inherit(e.TM,{b:/\w[\w\d_]*/})],r:0},e.HCM,r,{cN:"string",b:/'/,e:/'/},t]}}),a.registerLanguage("coffeescript",function(e){var t={keyword:"in if for while finally new do return else break catch instanceof throw try this switch continue typeof delete debugger super yield import export from as default await then unless until loop of by when and or is isnt not",literal:"true false null undefined yes no on off",built_in:"npm require console print module global window document"},r="[A-Za-z$_][0-9A-Za-z$_]*",n={cN:"subst",b:/#\{/,e:/}/,k:t},a=[e.BNM,e.inherit(e.CNM,{starts:{e:"(\\s*/)?",r:0}}),{cN:"string",v:[{b:/'''/,e:/'''/,c:[e.BE]},{b:/'/,e:/'/,c:[e.BE]},{b:/"""/,e:/"""/,c:[e.BE,n]},{b:/"/,e:/"/,c:[e.BE,n]}]},{cN:"regexp",v:[{b:"///",e:"///",c:[n,e.HCM]},{b:"//[gim]*",r:0},{b:/\/(?![ *])(\\\/|.)*?\/[gim]*(?=\W|$)/}]},{b:"@"+r},{sL:"javascript",eB:!0,eE:!0,v:[{b:"```",e:"```"},{b:"`",e:"`"}]}];n.c=a;var i=e.inherit(e.TM,{b:r}),s="(\\(.*\\))?\\s*\\B[-=]>",o={cN:"params",b:"\\([^\\(]",rB:!0,c:[{b:/\(/,e:/\)/,k:t,c:["self"].concat(a)}]};return{aliases:["coffee","cson","iced"],k:t,i:/\/\*/,c:a.concat([e.C("###","###"),e.HCM,{cN:"function",b:"^\\s*"+r+"\\s*=\\s*"+s,e:"[-=]>",rB:!0,c:[i,o]},{b:/[:\(,=]\s*/,r:0,c:[{cN:"function",b:s,e:"[-=]>",rB:!0,c:[o]}]},{cN:"class",bK:"class",e:"$",i:/[:="\[\]]/,c:[{bK:"extends",eW:!0,i:/[:="\[\]]/,c:[i]},i]},{b:r+":",e:":",rB:!0,rE:!0,r:0}])}}),a.registerLanguage("cpp",function(e){var t={cN:"keyword",b:"\\b[a-z\\d_]*_t\\b"},r={cN:"string",v:[{b:'(u8?|U|L)?"',e:'"',i:"\\n",c:[e.BE]},{b:'(u8?|U|L)?R"\\(',e:'\\)"'},{b:"'\\\\?.",e:"'",i:"."}]},n={cN:"number",v:[{b:"\\b(0b[01']+)"},{b:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{b:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"}],r:0},a={cN:"meta",b:/#\s*[a-z]+\b/,e:/$/,k:{"meta-keyword":"if else elif endif define undef warning error line pragma ifdef ifndef include"},c:[{b:/\\\n/,r:0},e.inherit(r,{cN:"meta-string"}),{cN:"meta-string",b:/<[^\n>]*>/,e:/$/,i:"\\n"},e.CLCM,e.CBCM]},i=e.IR+"\\s*\\(",s={keyword:"int float while private char catch import module export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const for static_cast|10 union namespace unsigned long volatile static protected bool template mutable if public friend do goto auto void enum else break extern using asm case typeid short reinterpret_cast|10 default double register explicit signed typename try this switch continue inline delete alignof constexpr decltype noexcept static_assert thread_local restrict _Bool complex _Complex _Imaginary atomic_bool atomic_char atomic_schar atomic_uchar atomic_short atomic_ushort atomic_int atomic_uint atomic_long atomic_ulong atomic_llong atomic_ullong new throw return and or not",built_in:"std string cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap array shared_ptr abort abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr",literal:"true false nullptr NULL"},o=[t,e.CLCM,e.CBCM,n,r];return{aliases:["c","cc","h","c++","h++","hpp"],k:s,i:"</",c:o.concat([a,{b:"\\b(deque|list|queue|stack|vector|map|set|bitset|multiset|multimap|unordered_map|unordered_set|unordered_multiset|unordered_multimap|array)\\s*<",e:">",k:s,c:["self",t]},{b:e.IR+"::",k:s},{v:[{b:/=/,e:/;/},{b:/\(/,e:/\)/},{bK:"new throw return else",e:/;/}],k:s,c:o.concat([{b:/\(/,e:/\)/,k:s,c:o.concat(["self"]),r:0}]),r:0},{cN:"function",b:"("+e.IR+"[\\*&\\s]+)+"+i,rB:!0,e:/[{;=]/,eE:!0,k:s,i:/[^\w\s\*&]/,c:[{b:i,rB:!0,c:[e.TM],r:0},{cN:"params",b:/\(/,e:/\)/,k:s,r:0,c:[e.CLCM,e.CBCM,r,n,t,{b:/\(/,e:/\)/,k:s,r:0,c:["self",e.CLCM,e.CBCM,r,n,t]}]},e.CLCM,e.CBCM,a]},{cN:"class",bK:"class struct",e:/[{;:]/,c:[{b:/</,e:/>/,c:["self"]},e.TM]}]),exports:{preprocessor:a,strings:r,k:s}}}),a.registerLanguage("cs",function(e){var t={keyword:"abstract as base bool break byte case catch char checked const continue decimal default delegate do double enum event explicit extern finally fixed float for foreach goto if implicit in int interface internal is lock long nameof object operator out override params private protected public readonly ref sbyte sealed short sizeof stackalloc static string struct switch this try typeof uint ulong unchecked unsafe ushort using virtual void volatile while add alias ascending async await by descending dynamic equals from get global group into join let on orderby partial remove select set value var where yield",literal:"null false true"},r={cN:"number",v:[{b:"\\b(0b[01']+)"},{b:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{b:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"}],r:0},n={cN:"string",b:'@"',e:'"',c:[{b:'""'}]},a=e.inherit(n,{i:/\n/}),i={cN:"subst",b:"{",e:"}",k:t},s=e.inherit(i,{i:/\n/}),o={cN:"string",b:/\$"/,e:'"',i:/\n/,c:[{b:"{{"},{b:"}}"},e.BE,s]},l={cN:"string",b:/\$@"/,e:'"',c:[{b:"{{"},{b:"}}"},{b:'""'},i]},c=e.inherit(l,{i:/\n/,c:[{b:"{{"},{b:"}}"},{b:'""'},s]});i.c=[l,o,n,e.ASM,e.QSM,r,e.CBCM],s.c=[c,o,a,e.ASM,e.QSM,r,e.inherit(e.CBCM,{i:/\n/})];var d={v:[l,o,n,e.ASM,e.QSM]},u=e.IR+"(<"+e.IR+"(\\s*,\\s*"+e.IR+")*>)?(\\[\\])?";return{aliases:["csharp","c#"],k:t,i:/::/,c:[e.C("///","$",{rB:!0,c:[{cN:"doctag",v:[{b:"///",r:0},{b:"\x3c!--|--\x3e"},{b:"</?",e:">"}]}]}),e.CLCM,e.CBCM,{cN:"meta",b:"#",e:"$",k:{"meta-keyword":"if else elif endif define undef warning error line region endregion pragma checksum"}},d,r,{bK:"class interface",e:/[{;=]/,i:/[^\s:,]/,c:[e.TM,e.CLCM,e.CBCM]},{bK:"namespace",e:/[{;=]/,i:/[^\s:]/,c:[e.inherit(e.TM,{b:"[a-zA-Z](\\.?\\w)*"}),e.CLCM,e.CBCM]},{cN:"meta",b:"^\\s*\\[",eB:!0,e:"\\]",eE:!0,c:[{cN:"meta-string",b:/"/,e:/"/}]},{bK:"new return throw await else",r:0},{cN:"function",b:"("+u+"\\s+)+"+e.IR+"\\s*\\(",rB:!0,e:/\s*[{;=]/,eE:!0,k:t,c:[{b:e.IR+"\\s*\\(",rB:!0,c:[e.TM],r:0},{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,k:t,r:0,c:[d,r,e.CBCM]},e.CLCM,e.CBCM]}]}}),a.registerLanguage("css",function(e){return{cI:!0,i:/[=\/|'\$]/,c:[e.CBCM,{cN:"selector-id",b:/#[A-Za-z0-9_-]+/},{cN:"selector-class",b:/\.[A-Za-z0-9_-]+/},{cN:"selector-attr",b:/\[/,e:/\]/,i:"$"},{cN:"selector-pseudo",b:/:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/},{b:"@(font-face|page)",l:"[a-z-]+",k:"font-face page"},{b:"@",e:"[{;]",i:/:/,c:[{cN:"keyword",b:/\w+/},{b:/\s/,eW:!0,eE:!0,r:0,c:[e.ASM,e.QSM,e.CSSNM]}]},{cN:"selector-tag",b:"[a-zA-Z-][a-zA-Z0-9_-]*",r:0},{b:"{",e:"}",i:/\S/,c:[e.CBCM,{b:/[A-Z\_\.\-]+\s*:/,rB:!0,e:";",eW:!0,c:[{cN:"attribute",b:/\S/,e:":",eE:!0,starts:{eW:!0,eE:!0,c:[{b:/[\w-]+\(/,rB:!0,c:[{cN:"built_in",b:/[\w-]+/},{b:/\(/,e:/\)/,c:[e.ASM,e.QSM]}]},e.CSSNM,e.QSM,e.ASM,e.CBCM,{cN:"number",b:"#[0-9A-Fa-f]+"},{cN:"meta",b:"!important"}]}}]}]}]}}),a.registerLanguage("glsl",function(e){return{k:{keyword:"break continue discard do else for if return while switch case default attribute binding buffer ccw centroid centroid varying coherent column_major const cw depth_any depth_greater depth_less depth_unchanged early_fragment_tests equal_spacing flat fractional_even_spacing fractional_odd_spacing highp in index inout invariant invocations isolines layout line_strip lines lines_adjacency local_size_x local_size_y local_size_z location lowp max_vertices mediump noperspective offset origin_upper_left out packed patch pixel_center_integer point_mode points precise precision quads r11f_g11f_b10f r16 r16_snorm r16f r16i r16ui r32f r32i r32ui r8 r8_snorm r8i r8ui readonly restrict rg16 rg16_snorm rg16f rg16i rg16ui rg32f rg32i rg32ui rg8 rg8_snorm rg8i rg8ui rgb10_a2 rgb10_a2ui rgba16 rgba16_snorm rgba16f rgba16i rgba16ui rgba32f rgba32i rgba32ui rgba8 rgba8_snorm rgba8i rgba8ui row_major sample shared smooth std140 std430 stream triangle_strip triangles triangles_adjacency uniform varying vertices volatile writeonly",type:"atomic_uint bool bvec2 bvec3 bvec4 dmat2 dmat2x2 dmat2x3 dmat2x4 dmat3 dmat3x2 dmat3x3 dmat3x4 dmat4 dmat4x2 dmat4x3 dmat4x4 double dvec2 dvec3 dvec4 float iimage1D iimage1DArray iimage2D iimage2DArray iimage2DMS iimage2DMSArray iimage2DRect iimage3D iimageBufferiimageCube iimageCubeArray image1D image1DArray image2D image2DArray image2DMS image2DMSArray image2DRect image3D imageBuffer imageCube imageCubeArray int isampler1D isampler1DArray isampler2D isampler2DArray isampler2DMS isampler2DMSArray isampler2DRect isampler3D isamplerBuffer isamplerCube isamplerCubeArray ivec2 ivec3 ivec4 mat2 mat2x2 mat2x3 mat2x4 mat3 mat3x2 mat3x3 mat3x4 mat4 mat4x2 mat4x3 mat4x4 sampler1D sampler1DArray sampler1DArrayShadow sampler1DShadow sampler2D sampler2DArray sampler2DArrayShadow sampler2DMS sampler2DMSArray sampler2DRect sampler2DRectShadow sampler2DShadow sampler3D samplerBuffer samplerCube samplerCubeArray samplerCubeArrayShadow samplerCubeShadow image1D uimage1DArray uimage2D uimage2DArray uimage2DMS uimage2DMSArray uimage2DRect uimage3D uimageBuffer uimageCube uimageCubeArray uint usampler1D usampler1DArray usampler2D usampler2DArray usampler2DMS usampler2DMSArray usampler2DRect usampler3D samplerBuffer usamplerCube usamplerCubeArray uvec2 uvec3 uvec4 vec2 vec3 vec4 void",built_in:"gl_MaxAtomicCounterBindings gl_MaxAtomicCounterBufferSize gl_MaxClipDistances gl_MaxClipPlanes gl_MaxCombinedAtomicCounterBuffers gl_MaxCombinedAtomicCounters gl_MaxCombinedImageUniforms gl_MaxCombinedImageUnitsAndFragmentOutputs gl_MaxCombinedTextureImageUnits gl_MaxComputeAtomicCounterBuffers gl_MaxComputeAtomicCounters gl_MaxComputeImageUniforms gl_MaxComputeTextureImageUnits gl_MaxComputeUniformComponents gl_MaxComputeWorkGroupCount gl_MaxComputeWorkGroupSize gl_MaxDrawBuffers gl_MaxFragmentAtomicCounterBuffers gl_MaxFragmentAtomicCounters gl_MaxFragmentImageUniforms gl_MaxFragmentInputComponents gl_MaxFragmentInputVectors gl_MaxFragmentUniformComponents gl_MaxFragmentUniformVectors gl_MaxGeometryAtomicCounterBuffers gl_MaxGeometryAtomicCounters gl_MaxGeometryImageUniforms gl_MaxGeometryInputComponents gl_MaxGeometryOutputComponents gl_MaxGeometryOutputVertices gl_MaxGeometryTextureImageUnits gl_MaxGeometryTotalOutputComponents gl_MaxGeometryUniformComponents gl_MaxGeometryVaryingComponents gl_MaxImageSamples gl_MaxImageUnits gl_MaxLights gl_MaxPatchVertices gl_MaxProgramTexelOffset gl_MaxTessControlAtomicCounterBuffers gl_MaxTessControlAtomicCounters gl_MaxTessControlImageUniforms gl_MaxTessControlInputComponents gl_MaxTessControlOutputComponents gl_MaxTessControlTextureImageUnits gl_MaxTessControlTotalOutputComponents gl_MaxTessControlUniformComponents gl_MaxTessEvaluationAtomicCounterBuffers gl_MaxTessEvaluationAtomicCounters gl_MaxTessEvaluationImageUniforms gl_MaxTessEvaluationInputComponents gl_MaxTessEvaluationOutputComponents gl_MaxTessEvaluationTextureImageUnits gl_MaxTessEvaluationUniformComponents gl_MaxTessGenLevel gl_MaxTessPatchComponents gl_MaxTextureCoords gl_MaxTextureImageUnits gl_MaxTextureUnits gl_MaxVaryingComponents gl_MaxVaryingFloats gl_MaxVaryingVectors gl_MaxVertexAtomicCounterBuffers gl_MaxVertexAtomicCounters gl_MaxVertexAttribs gl_MaxVertexImageUniforms gl_MaxVertexOutputComponents gl_MaxVertexOutputVectors gl_MaxVertexTextureImageUnits gl_MaxVertexUniformComponents gl_MaxVertexUniformVectors gl_MaxViewports gl_MinProgramTexelOffset gl_BackColor gl_BackLightModelProduct gl_BackLightProduct gl_BackMaterial gl_BackSecondaryColor gl_ClipDistance gl_ClipPlane gl_ClipVertex gl_Color gl_DepthRange gl_EyePlaneQ gl_EyePlaneR gl_EyePlaneS gl_EyePlaneT gl_Fog gl_FogCoord gl_FogFragCoord gl_FragColor gl_FragCoord gl_FragData gl_FragDepth gl_FrontColor gl_FrontFacing gl_FrontLightModelProduct gl_FrontLightProduct gl_FrontMaterial gl_FrontSecondaryColor gl_GlobalInvocationID gl_InstanceID gl_InvocationID gl_Layer gl_LightModel gl_LightSource gl_LocalInvocationID gl_LocalInvocationIndex gl_ModelViewMatrix gl_ModelViewMatrixInverse gl_ModelViewMatrixInverseTranspose gl_ModelViewMatrixTranspose gl_ModelViewProjectionMatrix gl_ModelViewProjectionMatrixInverse gl_ModelViewProjectionMatrixInverseTranspose gl_ModelViewProjectionMatrixTranspose gl_MultiTexCoord0 gl_MultiTexCoord1 gl_MultiTexCoord2 gl_MultiTexCoord3 gl_MultiTexCoord4 gl_MultiTexCoord5 gl_MultiTexCoord6 gl_MultiTexCoord7 gl_Normal gl_NormalMatrix gl_NormalScale gl_NumSamples gl_NumWorkGroups gl_ObjectPlaneQ gl_ObjectPlaneR gl_ObjectPlaneS gl_ObjectPlaneT gl_PatchVerticesIn gl_Point gl_PointCoord gl_PointSize gl_Position gl_PrimitiveID gl_PrimitiveIDIn gl_ProjectionMatrix gl_ProjectionMatrixInverse gl_ProjectionMatrixInverseTranspose gl_ProjectionMatrixTranspose gl_SampleID gl_SampleMask gl_SampleMaskIn gl_SamplePosition gl_SecondaryColor gl_TessCoord gl_TessLevelInner gl_TessLevelOuter gl_TexCoord gl_TextureEnvColor gl_TextureMatrix gl_TextureMatrixInverse gl_TextureMatrixInverseTranspose gl_TextureMatrixTranspose gl_Vertex gl_VertexID gl_ViewportIndex gl_WorkGroupID gl_WorkGroupSize gl_in gl_out EmitStreamVertex EmitVertex EndPrimitive EndStreamPrimitive abs acos acosh all any asin asinh atan atanh atomicAdd atomicAnd atomicCompSwap atomicCounter atomicCounterDecrement atomicCounterIncrement atomicExchange atomicMax atomicMin atomicOr atomicXor barrier bitCount bitfieldExtract bitfieldInsert bitfieldReverse ceil clamp cos cosh cross dFdx dFdy degrees determinant distance dot equal exp exp2 faceforward findLSB findMSB floatBitsToInt floatBitsToUint floor fma fract frexp ftransform fwidth greaterThan greaterThanEqual groupMemoryBarrier imageAtomicAdd imageAtomicAnd imageAtomicCompSwap imageAtomicExchange imageAtomicMax imageAtomicMin imageAtomicOr imageAtomicXor imageLoad imageSize imageStore imulExtended intBitsToFloat interpolateAtCentroid interpolateAtOffset interpolateAtSample inverse inversesqrt isinf isnan ldexp length lessThan lessThanEqual log log2 matrixCompMult max memoryBarrier memoryBarrierAtomicCounter memoryBarrierBuffer memoryBarrierImage memoryBarrierShared min mix mod modf noise1 noise2 noise3 noise4 normalize not notEqual outerProduct packDouble2x32 packHalf2x16 packSnorm2x16 packSnorm4x8 packUnorm2x16 packUnorm4x8 pow radians reflect refract round roundEven shadow1D shadow1DLod shadow1DProj shadow1DProjLod shadow2D shadow2DLod shadow2DProj shadow2DProjLod sign sin sinh smoothstep sqrt step tan tanh texelFetch texelFetchOffset texture texture1D texture1DLod texture1DProj texture1DProjLod texture2D texture2DLod texture2DProj texture2DProjLod texture3D texture3DLod texture3DProj texture3DProjLod textureCube textureCubeLod textureGather textureGatherOffset textureGatherOffsets textureGrad textureGradOffset textureLod textureLodOffset textureOffset textureProj textureProjGrad textureProjGradOffset textureProjLod textureProjLodOffset textureProjOffset textureQueryLevels textureQueryLod textureSize transpose trunc uaddCarry uintBitsToFloat umulExtended unpackDouble2x32 unpackHalf2x16 unpackSnorm2x16 unpackSnorm4x8 unpackUnorm2x16 unpackUnorm4x8 usubBorrow",literal:"true false"},i:'"',c:[e.CLCM,e.CBCM,e.CNM,{cN:"meta",b:"#",e:"$"}]}}),a.registerLanguage("go",function(e){var t={keyword:"break default func interface select case map struct chan else goto package switch const fallthrough if range type continue for import return var go defer bool byte complex64 complex128 float32 float64 int8 int16 int32 int64 string uint8 uint16 uint32 uint64 int uint uintptr rune",literal:"true false iota nil",built_in:"append cap close complex copy imag len make new panic print println real recover delete"};return{aliases:["golang"],k:t,i:"</",c:[e.CLCM,e.CBCM,{cN:"string",v:[e.QSM,{b:"'",e:"[^\\\\]'"},{b:"`",e:"`"}]},{cN:"number",v:[{b:e.CNR+"[dflsi]",r:1},e.CNM]},{b:/:=/},{cN:"function",bK:"func",e:/\s*\{/,eE:!0,c:[e.TM,{cN:"params",b:/\(/,e:/\)/,k:t,i:/["']/}]}]}}),a.registerLanguage("haskell",function(e){var t={v:[e.C("--","$"),e.C("{-","-}",{c:["self"]})]},r={cN:"meta",b:"{-#",e:"#-}"},n={cN:"meta",b:"^#",e:"$"},a={cN:"type",b:"\\b[A-Z][\\w']*",r:0},i={b:"\\(",e:"\\)",i:'"',c:[r,n,{cN:"type",b:"\\b[A-Z][\\w]*(\\((\\.\\.|,|\\w+)\\))?"},e.inherit(e.TM,{b:"[_a-z][\\w']*"}),t]};return{aliases:["hs"],k:"let in if then else case of where do module import hiding qualified type data newtype deriving class instance as default infix infixl infixr foreign export ccall stdcall cplusplus jvm dotnet safe unsafe family forall mdo proc rec",c:[{bK:"module",e:"where",k:"module where",c:[i,t],i:"\\W\\.|;"},{b:"\\bimport\\b",e:"$",k:"import qualified as hiding",c:[i,t],i:"\\W\\.|;"},{cN:"class",b:"^(\\s*)?(class|instance)\\b",e:"where",k:"class family instance where",c:[a,i,t]},{cN:"class",b:"\\b(data|(new)?type)\\b",e:"$",k:"data family type newtype deriving",c:[r,a,i,{b:"{",e:"}",c:i.c},t]},{bK:"default",e:"$",c:[a,i,t]},{bK:"infix infixl infixr",e:"$",c:[e.CNM,t]},{b:"\\bforeign\\b",e:"$",k:"foreign import export ccall stdcall cplusplus jvm dotnet safe unsafe",c:[a,e.QSM,t]},{cN:"meta",b:"#!\\/usr\\/bin\\/env runhaskell",e:"$"},r,n,e.QSM,e.CNM,a,e.inherit(e.TM,{b:"^[_a-z][\\w']*"}),t,{b:"->|<-"}]}}),a.registerLanguage("http",function(e){var t="HTTP/[0-9\\.]+";return{aliases:["https"],i:"\\S",c:[{b:"^"+t,e:"$",c:[{cN:"number",b:"\\b\\d{3}\\b"}]},{b:"^[A-Z]+ (.*?) "+t+"$",rB:!0,e:"$",c:[{cN:"string",b:" ",e:" ",eB:!0,eE:!0},{b:t},{cN:"keyword",b:"[A-Z]+"}]},{cN:"attribute",b:"^\\w",e:": ",eE:!0,i:"\\n|\\s|=",starts:{e:"$",r:0}},{b:"\\n\\n",starts:{sL:[],eW:!0}}]}}),a.registerLanguage("java",function(e){var t="false synchronized int abstract float private char boolean var static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports do",r={cN:"number",b:"\\b(0[bB]([01]+[01_]+[01]+|[01]+)|0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)|(([\\d]+[\\d_]+[\\d]+|[\\d]+)(\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))?|\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))([eE][-+]?\\d+)?)[lLfF]?",r:0};return{aliases:["jsp"],k:t,i:/<\/|#/,c:[e.C("/\\*\\*","\\*/",{r:0,c:[{b:/\w+@/,r:0},{cN:"doctag",b:"@[A-Za-z]+"}]}),e.CLCM,e.CBCM,e.ASM,e.QSM,{cN:"class",bK:"class interface",e:/[{;=]/,eE:!0,k:"class interface",i:/[:"\[\]]/,c:[{bK:"extends implements"},e.UTM]},{bK:"new throw return else",r:0},{cN:"function",b:"([À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*(<[À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*(\\s*,\\s*[À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*)*>)?\\s+)+"+e.UIR+"\\s*\\(",rB:!0,e:/[{;=]/,eE:!0,k:t,c:[{b:e.UIR+"\\s*\\(",rB:!0,r:0,c:[e.UTM]},{cN:"params",b:/\(/,e:/\)/,k:t,r:0,c:[e.ASM,e.QSM,e.CNM,e.CBCM]},e.CLCM,e.CBCM]},r,{cN:"meta",b:"@[A-Za-z]+"}]}}),a.registerLanguage("javascript",function(e){var t="[A-Za-z$_][0-9A-Za-z$_]*",r={keyword:"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await static import from as",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Promise"},n={cN:"number",v:[{b:"\\b(0[bB][01]+)"},{b:"\\b(0[oO][0-7]+)"},{b:e.CNR}],r:0},a={cN:"subst",b:"\\$\\{",e:"\\}",k:r,c:[]},i={cN:"string",b:"`",e:"`",c:[e.BE,a]};a.c=[e.ASM,e.QSM,i,n,e.RM];var s=a.c.concat([e.CBCM,e.CLCM]);return{aliases:["js","jsx"],k:r,c:[{cN:"meta",r:10,b:/^\s*['"]use (strict|asm)['"]/},{cN:"meta",b:/^#!/,e:/$/},e.ASM,e.QSM,i,e.CLCM,e.CBCM,n,{b:/[{,]\s*/,r:0,c:[{b:t+"\\s*:",rB:!0,r:0,c:[{cN:"attr",b:t,r:0}]}]},{b:"("+e.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[e.CLCM,e.CBCM,e.RM,{cN:"function",b:"(\\(.*?\\)|"+t+")\\s*=>",rB:!0,e:"\\s*=>",c:[{cN:"params",v:[{b:t},{b:/\(\s*\)/},{b:/\(/,e:/\)/,eB:!0,eE:!0,k:r,c:s}]}]},{b:/</,e:/(\/\w+|\w+\/)>/,sL:"xml",c:[{b:/<\w+\s*\/>/,skip:!0},{b:/<\w+/,e:/(\/\w+|\w+\/)>/,skip:!0,c:[{b:/<\w+\s*\/>/,skip:!0},"self"]}]}],r:0},{cN:"function",bK:"function",e:/\{/,eE:!0,c:[e.inherit(e.TM,{b:t}),{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,c:s}],i:/\[|%/},{b:/\$[(.]/},e.METHOD_GUARD,{cN:"class",bK:"class",e:/[{;=]/,eE:!0,i:/[:"\[\]]/,c:[{bK:"extends"},e.UTM]},{bK:"constructor",e:/\{/,eE:!0}],i:/#(?!!)/}}),a.registerLanguage("json",function(e){var t={literal:"true false null"},r=[e.QSM,e.CNM],n={e:",",eW:!0,eE:!0,c:r,k:t},a={b:"{",e:"}",c:[{cN:"attr",b:/"/,e:/"/,c:[e.BE],i:"\\n"},e.inherit(n,{b:/:/})],i:"\\S"},i={b:"\\[",e:"\\]",c:[e.inherit(n)],i:"\\S"};return r.splice(r.length,0,a,i),{c:r,k:t,i:"\\S"}}),a.registerLanguage("kotlin",function(e){var t={keyword:"abstract as val var vararg get set class object open private protected public noinline crossinline dynamic final enum if else do while for when throw try catch finally import package is in fun override companion reified inline lateinit init interface annotation data sealed internal infix operator out by constructor super tailrec where const inner suspend typealias external expect actual trait volatile transient native default",built_in:"Byte Short Char Int Long Boolean Float Double Void Unit Nothing",literal:"true false null"},r={cN:"symbol",b:e.UIR+"@"},n={cN:"subst",b:"\\${",e:"}",c:[e.ASM,e.CNM]},a={cN:"variable",b:"\\$"+e.UIR},i={cN:"string",v:[{b:'"""',e:'"""',c:[a,n]},{b:"'",e:"'",i:/\n/,c:[e.BE]},{b:'"',e:'"',i:/\n/,c:[e.BE,a,n]}]},s={cN:"meta",b:"@(?:file|property|field|get|set|receiver|param|setparam|delegate)\\s*:(?:\\s*"+e.UIR+")?"},o={cN:"meta",b:"@"+e.UIR,c:[{b:/\(/,e:/\)/,c:[e.inherit(i,{cN:"meta-string"})]}]},l={cN:"number",b:"\\b(0[bB]([01]+[01_]+[01]+|[01]+)|0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)|(([\\d]+[\\d_]+[\\d]+|[\\d]+)(\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))?|\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))([eE][-+]?\\d+)?)[lLfF]?",r:0};return{aliases:["kt"],k:t,c:[e.C("/\\*\\*","\\*/",{r:0,c:[{cN:"doctag",b:"@[A-Za-z]+"}]}),e.CLCM,e.CBCM,{cN:"keyword",b:/\b(break|continue|return|this)\b/,starts:{c:[{cN:"symbol",b:/@\w+/}]}},r,s,o,{cN:"function",bK:"fun",e:"[(]|$",rB:!0,eE:!0,k:t,i:/fun\s+(<.*>)?[^\s\(]+(\s+[^\s\(]+)\s*=/,r:5,c:[{b:e.UIR+"\\s*\\(",rB:!0,r:0,c:[e.UTM]},{cN:"type",b:/</,e:/>/,k:"reified",r:0},{cN:"params",b:/\(/,e:/\)/,endsParent:!0,k:t,r:0,c:[{b:/:/,e:/[=,\/]/,eW:!0,c:[{cN:"type",b:e.UIR},e.CLCM,e.CBCM],r:0},e.CLCM,e.CBCM,s,o,i,e.CNM]},e.CBCM]},{cN:"class",bK:"class interface trait",e:/[:\{(]|$/,eE:!0,i:"extends implements",c:[{bK:"public protected internal private constructor"},e.UTM,{cN:"type",b:/</,e:/>/,eB:!0,eE:!0,r:0},{cN:"type",b:/[,:]\s*/,e:/[<\(,]|$/,eB:!0,rE:!0},s,o]},i,{cN:"meta",b:"^#!/usr/bin/env",e:"$",i:"\n"},l]}}),a.registerLanguage("lisp",function(e){var t="[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*",r="\\|[^]*?\\|",n="(\\-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s|D|E|F|L|S)(\\+|\\-)?\\d+)?",a={cN:"literal",b:"\\b(t{1}|nil)\\b"},i={cN:"number",v:[{b:n,r:0},{b:"#(b|B)[0-1]+(/[0-1]+)?"},{b:"#(o|O)[0-7]+(/[0-7]+)?"},{b:"#(x|X)[0-9a-fA-F]+(/[0-9a-fA-F]+)?"},{b:"#(c|C)\\("+n+" +"+n,e:"\\)"}]},s=e.inherit(e.QSM,{i:null}),o=e.C(";","$",{r:0}),l={b:"\\*",e:"\\*"},c={cN:"symbol",b:"[:&]"+t},d={b:t,r:0},u={b:r},p={c:[i,s,l,c,{b:"\\(",e:"\\)",c:["self",a,s,i,d]},d],v:[{b:"['`]\\(",e:"\\)"},{b:"\\(quote ",e:"\\)",k:{name:"quote"}},{b:"'"+r}]},m={v:[{b:"'"+t},{b:"#'"+t+"(::"+t+")*"}]},g={b:"\\(\\s*",e:"\\)"},b={eW:!0,r:0};return g.c=[{cN:"name",v:[{b:t},{b:r}]},b],b.c=[p,m,g,a,i,s,o,l,c,u,d],{i:/\S/,c:[i,{cN:"meta",b:"^#!",e:"$"},a,s,o,p,m,g,d]}}),a.registerLanguage("lua",function(e){var t="\\[=*\\[",r="\\]=*\\]",n={b:t,e:r,c:["self"]},a=[e.C("--(?!"+t+")","$"),e.C("--"+t,r,{c:[n],r:10})];return{l:e.UIR,k:{literal:"true false nil",keyword:"and break do else elseif end for goto if in local not or repeat return then until while",built_in:"_G _ENV _VERSION __index __newindex __mode __call __metatable __tostring __len __gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstringmodule next pairs pcall print rawequal rawget rawset require select setfenvsetmetatable tonumber tostring type unpack xpcall arg selfcoroutine resume yield status wrap create running debug getupvalue debug sethook getmetatable gethook setmetatable setlocal traceback setfenv getinfo setupvalue getlocal getregistry getfenv io lines write close flush open output type read stderr stdin input stdout popen tmpfile math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan os exit setlocale date getenv difftime remove time clock tmpname rename execute package preload loadlib loaded loaders cpath config path seeall string sub upper len gfind rep find match char dump gmatch reverse byte format gsub lower table setn insert getn foreachi maxn foreach concat sort remove"},c:a.concat([{cN:"function",bK:"function",e:"\\)",c:[e.inherit(e.TM,{b:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),{cN:"params",b:"\\(",eW:!0,c:a}].concat(a)},e.CNM,e.ASM,e.QSM,{cN:"string",b:t,e:r,c:[n],r:5}])}}),a.registerLanguage("makefile",function(e){var t={cN:"variable",v:[{b:"\\$\\("+e.UIR+"\\)",c:[e.BE]},{b:/\$[@%<?\^\+\*]/}]};return{aliases:["mk","mak"],k:"define endef undefine ifdef ifndef ifeq ifneq else endif include -include sinclude override export unexport private vpath",l:/[\w-]+/,c:[e.HCM,t,{cN:"string",b:/"/,e:/"/,c:[e.BE,t]},{cN:"variable",b:/\$\([\w-]+\s/,e:/\)/,k:{built_in:"subst patsubst strip findstring filter filter-out sort word wordlist firstword lastword dir notdir suffix basename addsuffix addprefix join wildcard realpath abspath error warning shell origin flavor foreach if or and call eval file value"},c:[t]},{b:"^"+e.UIR+"\\s*[:+?]?=",i:"\\n",rB:!0,c:[{b:"^"+e.UIR,e:"[:+?]?=",eE:!0}]},{cN:"meta",b:/^\.PHONY:/,e:/$/,k:{"meta-keyword":".PHONY"},l:/[\.\w]+/},{cN:"section",b:/^[^\s]+:/,e:/$/,c:[t]}]}}),a.registerLanguage("xml",function(e){var t={eW:!0,i:/</,r:0,c:[{cN:"attr",b:"[A-Za-z0-9\\._:-]+",r:0},{b:/=\s*/,r:0,c:[{cN:"string",endsParent:!0,v:[{b:/"/,e:/"/},{b:/'/,e:/'/},{b:/[^\s"'=<>`]+/}]}]}]};return{aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist"],cI:!0,c:[{cN:"meta",b:"<!DOCTYPE",e:">",r:10,c:[{b:"\\[",e:"\\]"}]},e.C("\x3c!--","--\x3e",{r:10}),{b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{cN:"meta",b:/<\?xml/,e:/\?>/,r:10},{b:/<\?(php)?/,e:/\?>/,sL:"php",c:[{b:"/\\*",e:"\\*/",skip:!0},{b:'b"',e:'"',skip:!0},{b:"b'",e:"'",skip:!0},e.inherit(e.ASM,{i:null,cN:null,c:null,skip:!0}),e.inherit(e.QSM,{i:null,cN:null,c:null,skip:!0})]},{cN:"tag",b:"<style(?=\\s|>|$)",e:">",k:{name:"style"},c:[t],starts:{e:"</style>",rE:!0,sL:["css","xml"]}},{cN:"tag",b:"<script(?=\\s|>|$)",e:">",k:{name:"script"},c:[t],starts:{e:"<\/script>",rE:!0,sL:["actionscript","javascript","handlebars","xml"]}},{cN:"tag",b:"</?",e:"/?>",c:[{cN:"name",b:/[^\/><\s]+/,r:0},t]}]}}),a.registerLanguage("markdown",function(e){return{aliases:["md","mkdown","mkd"],c:[{cN:"section",v:[{b:"^#{1,6}",e:"$"},{b:"^.+?\\n[=-]{2,}$"}]},{b:"<",e:">",sL:"xml",r:0},{cN:"bullet",b:"^([*+-]|(\\d+\\.))\\s+"},{cN:"strong",b:"[*_]{2}.+?[*_]{2}"},{cN:"emphasis",v:[{b:"\\*.+?\\*"},{b:"_.+?_",r:0}]},{cN:"quote",b:"^>\\s+",e:"$"},{cN:"code",v:[{b:"^```w*s*$",e:"^```s*$"},{b:"`.+?`"},{b:"^( {4}|\t)",e:"$",r:0}]},{b:"^[-\\*]{3,}",e:"$"},{b:"\\[.+?\\][\\(\\[].*?[\\)\\]]",rB:!0,c:[{cN:"string",b:"\\[",e:"\\]",eB:!0,rE:!0,r:0},{cN:"link",b:"\\]\\(",e:"\\)",eB:!0,eE:!0},{cN:"symbol",b:"\\]\\[",e:"\\]",eB:!0,eE:!0}],r:10},{b:/^\[[^\n]+\]:/,rB:!0,c:[{cN:"symbol",b:/\[/,e:/\]/,eB:!0,eE:!0},{cN:"link",b:/:\s*/,e:/$/,eB:!0}]}]}}),a.registerLanguage("matlab",function(e){var t="('|\\.')+",r={r:0,c:[{b:t}]};return{k:{keyword:"break case catch classdef continue else elseif end enumerated events for function global if methods otherwise parfor persistent properties return spmd switch try while",built_in:"sin sind sinh asin asind asinh cos cosd cosh acos acosd acosh tan tand tanh atan atand atan2 atanh sec secd sech asec asecd asech csc cscd csch acsc acscd acsch cot cotd coth acot acotd acoth hypot exp expm1 log log1p log10 log2 pow2 realpow reallog realsqrt sqrt nthroot nextpow2 abs angle complex conj imag real unwrap isreal cplxpair fix floor ceil round mod rem sign airy besselj bessely besselh besseli besselk beta betainc betaln ellipj ellipke erf erfc erfcx erfinv expint gamma gammainc gammaln psi legendre cross dot factor isprime primes gcd lcm rat rats perms nchoosek factorial cart2sph cart2pol pol2cart sph2cart hsv2rgb rgb2hsv zeros ones eye repmat rand randn linspace logspace freqspace meshgrid accumarray size length ndims numel disp isempty isequal isequalwithequalnans cat reshape diag blkdiag tril triu fliplr flipud flipdim rot90 find sub2ind ind2sub bsxfun ndgrid permute ipermute shiftdim circshift squeeze isscalar isvector ans eps realmax realmin pi i inf nan isnan isinf isfinite j why compan gallery hadamard hankel hilb invhilb magic pascal rosser toeplitz vander wilkinson max min nanmax nanmin mean nanmean type table readtable writetable sortrows sort figure plot plot3 scatter scatter3 cellfun legend intersect ismember procrustes hold num2cell "},i:'(//|"|#|/\\*|\\s+/\\w+)',c:[{cN:"function",bK:"function",e:"$",c:[e.UTM,{cN:"params",v:[{b:"\\(",e:"\\)"},{b:"\\[",e:"\\]"}]}]},{cN:"built_in",b:/true|false/,r:0,starts:r},{b:"[a-zA-Z][a-zA-Z_0-9]*"+t,r:0},{cN:"number",b:e.CNR,r:0,starts:r},{cN:"string",b:"'",e:"'",c:[e.BE,{b:"''"}]},{b:/\]|}|\)/,r:0,starts:r},{cN:"string",b:'"',e:'"',c:[e.BE,{b:'""'}],starts:r},e.C("^\\s*\\%\\{\\s*$","^\\s*\\%\\}\\s*$"),e.C("\\%","$")]}}),a.registerLanguage("objectivec",function(e){var t=/[a-zA-Z@][a-zA-Z0-9_]*/,r="@interface @class @protocol @implementation";return{aliases:["mm","objc","obj-c"],k:{keyword:"int float while char export sizeof typedef const struct for union unsigned long volatile static bool mutable if do return goto void enum else break extern asm case short default double register explicit signed typename this switch continue wchar_t inline readonly assign readwrite self @synchronized id typeof nonatomic super unichar IBOutlet IBAction strong weak copy in out inout bycopy byref oneway __strong __weak __block __autoreleasing @private @protected @public @try @property @end @throw @catch @finally @autoreleasepool @synthesize @dynamic @selector @optional @required @encode @package @import @defs @compatibility_alias __bridge __bridge_transfer __bridge_retained __bridge_retain __covariant __contravariant __kindof _Nonnull _Nullable _Null_unspecified __FUNCTION__ __PRETTY_FUNCTION__ __attribute__ getter setter retain unsafe_unretained nonnull nullable null_unspecified null_resettable class instancetype NS_DESIGNATED_INITIALIZER NS_UNAVAILABLE NS_REQUIRES_SUPER NS_RETURNS_INNER_POINTER NS_INLINE NS_AVAILABLE NS_DEPRECATED NS_ENUM NS_OPTIONS NS_SWIFT_UNAVAILABLE NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_END NS_REFINED_FOR_SWIFT NS_SWIFT_NAME NS_SWIFT_NOTHROW NS_DURING NS_HANDLER NS_ENDHANDLER NS_VALUERETURN NS_VOIDRETURN",literal:"false true FALSE TRUE nil YES NO NULL",built_in:"BOOL dispatch_once_t dispatch_queue_t dispatch_sync dispatch_async dispatch_once"},l:t,i:"</",c:[{cN:"built_in",b:"\\b(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)\\w+"},e.CLCM,e.CBCM,e.CNM,e.QSM,{cN:"string",v:[{b:'@"',e:'"',i:"\\n",c:[e.BE]},{b:"'",e:"[^\\\\]'",i:"[^\\\\][^']"}]},{cN:"meta",b:"#",e:"$",c:[{cN:"meta-string",v:[{b:'"',e:'"'},{b:"<",e:">"}]}]},{cN:"class",b:"("+r.split(" ").join("|")+")\\b",e:"({|$)",eE:!0,k:r,l:t,c:[e.UTM]},{b:"\\."+e.UIR,r:0}]}}),a.registerLanguage("perl",function(e){var t="getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qqfileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent shutdown dump chomp connect getsockname die socketpair close flock exists index shmgetsub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedirioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when",r={cN:"subst",b:"[$@]\\{",e:"\\}",k:t},n={b:"->{",e:"}"},a={v:[{b:/\$\d/},{b:/[\$%@](\^\w\b|#\w+(::\w+)*|{\w+}|\w+(::\w*)*)/},{b:/[\$%@][^\s\w{]/,r:0}]},i=[e.BE,r,a],s=[a,e.HCM,e.C("^\\=\\w","\\=cut",{eW:!0}),n,{cN:"string",c:i,v:[{b:"q[qwxr]?\\s*\\(",e:"\\)",r:5},{b:"q[qwxr]?\\s*\\[",e:"\\]",r:5},{b:"q[qwxr]?\\s*\\{",e:"\\}",r:5},{b:"q[qwxr]?\\s*\\|",e:"\\|",r:5},{b:"q[qwxr]?\\s*\\<",e:"\\>",r:5},{b:"qw\\s+q",e:"q",r:5},{b:"'",e:"'",c:[e.BE]},{b:'"',e:'"'},{b:"`",e:"`",c:[e.BE]},{b:"{\\w+}",c:[],r:0},{b:"-?\\w+\\s*\\=\\>",c:[],r:0}]},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{b:"(\\/\\/|"+e.RSR+"|\\b(split|return|print|reverse|grep)\\b)\\s*",k:"split return print reverse grep",r:0,c:[e.HCM,{cN:"regexp",b:"(s|tr|y)/(\\\\.|[^/])*/(\\\\.|[^/])*/[a-z]*",r:10},{cN:"regexp",b:"(m|qr)?/",e:"/[a-z]*",c:[e.BE],r:0}]},{cN:"function",bK:"sub",e:"(\\s*\\(.*?\\))?[;{]",eE:!0,r:5,c:[e.TM]},{b:"-\\w\\b",r:0},{b:"^__DATA__$",e:"^__END__$",sL:"mojolicious",c:[{b:"^@@.*",e:"$",cN:"comment"}]}];return{aliases:["pl","pm"],l:/[\w\.]+/,k:t,c:n.c=r.c=s}}),a.registerLanguage("php",function(e){var t={b:"\\$+[a-zA-Z_-ÿ][a-zA-Z0-9_-ÿ]*"},r={cN:"meta",b:/<\?(php)?|\?>/},n={cN:"string",c:[e.BE,r],v:[{b:'b"',e:'"'},{b:"b'",e:"'"},e.inherit(e.ASM,{i:null}),e.inherit(e.QSM,{i:null})]},a={v:[e.BNM,e.CNM]};return{aliases:["php","php3","php4","php5","php6","php7"],cI:!0,k:"and include_once list abstract global private echo interface as static endswitch array null if endwhile or const for endforeach self var while isset public protected exit foreach throw elseif include __FILE__ empty require_once do xor return parent clone use __CLASS__ __LINE__ else break print eval new catch __METHOD__ case exception default die require __FUNCTION__ enddeclare final try switch continue endfor endif declare unset true false trait goto instanceof insteadof __DIR__ __NAMESPACE__ yield finally",c:[e.HCM,e.C("//","$",{c:[r]}),e.C("/\\*","\\*/",{c:[{cN:"doctag",b:"@[A-Za-z]+"}]}),e.C("__halt_compiler.+?;",!1,{eW:!0,k:"__halt_compiler",l:e.UIR}),{cN:"string",b:/<<<['"]?\w+['"]?$/,e:/^\w+;?$/,c:[e.BE,{cN:"subst",v:[{b:/\$\w+/},{b:/\{\$/,e:/\}/}]}]},r,{cN:"keyword",b:/\$this\b/},t,{b:/(::|->)+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/},{cN:"function",bK:"function",e:/[;{]/,eE:!0,i:"\\$|\\[|%",c:[e.UTM,{cN:"params",b:"\\(",e:"\\)",c:["self",t,e.CBCM,n,a]}]},{cN:"class",bK:"class interface",e:"{",eE:!0,i:/[:\(\$"]/,c:[{bK:"extends implements"},e.UTM]},{bK:"namespace",e:";",i:/[\.']/,c:[e.UTM]},{bK:"use",e:";",c:[e.UTM]},{b:"=>"},n,a]}}),a.registerLanguage("plaintext",function(e){return{disableAutodetect:!0}}),a.registerLanguage("python",function(e){var t={keyword:"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda async await nonlocal|10 None True False",built_in:"Ellipsis NotImplemented"},r={cN:"meta",b:/^(>>>|\.\.\.) /},n={cN:"subst",b:/\{/,e:/\}/,k:t,i:/#/},a={cN:"string",c:[e.BE],v:[{b:/(u|b)?r?'''/,e:/'''/,c:[e.BE,r],r:10},{b:/(u|b)?r?"""/,e:/"""/,c:[e.BE,r],r:10},{b:/(fr|rf|f)'''/,e:/'''/,c:[e.BE,r,n]},{b:/(fr|rf|f)"""/,e:/"""/,c:[e.BE,r,n]},{b:/(u|r|ur)'/,e:/'/,r:10},{b:/(u|r|ur)"/,e:/"/,r:10},{b:/(b|br)'/,e:/'/},{b:/(b|br)"/,e:/"/},{b:/(fr|rf|f)'/,e:/'/,c:[e.BE,n]},{b:/(fr|rf|f)"/,e:/"/,c:[e.BE,n]},e.ASM,e.QSM]},i={cN:"number",r:0,v:[{b:e.BNR+"[lLjJ]?"},{b:"\\b(0o[0-7]+)[lLjJ]?"},{b:e.CNR+"[lLjJ]?"}]},s={cN:"params",b:/\(/,e:/\)/,c:["self",r,i,a]};return n.c=[a,i,r],{aliases:["py","gyp","ipython"],k:t,i:/(<\/|->|\?)|=>/,c:[r,i,a,e.HCM,{v:[{cN:"function",bK:"def"},{cN:"class",bK:"class"}],e:/:/,i:/[${=;\n,]/,c:[e.UTM,s,{b:/->/,eW:!0,k:"None"}]},{cN:"meta",b:/^[\t ]*@/,e:/$/},{b:/\b(print|exec)\(/}]}}),a.registerLanguage("pyxlscript",function(e){var t={keyword:"⌊ ⌋ ⌈ ⌉ | ‖ ∊ ∈ bitor pad joy bitxor bitand and because at local in if then for return while mod preserving_transform|10 else continue let|2 const break not with assert with debug_watch draw_sprite draw_text debug_print|4 while continue or nil|2 pi nan infinity true false ∅"},r={cN:"subst",b:/\{/,e:/\}/,k:t},n={cN:"string",c:[e.BE],v:[{b:/(u|r|ur)"/,e:/"/,r:10},{b:/(b|br)"/,e:/"/},{b:/(fr|rf|f)"/,e:/"/,c:[e.BE,r]},e.QSM]},a={cN:"number",r:0,v:[{b:/[+-]?[∞επ½⅓⅔¼¾⅕⅖⅗⅘⅙⅐⅛⅑⅒`]/},{b:/#[0-7a-fA-F]+/},{b:/[+-]?(\d*\.)?\d+(%|deg|°)?/},{b:/[₀₁₂₃₄₅₆₇₈₉⁰¹²³⁴⁵⁶⁷⁸⁹]/}]},i={cN:"params",b:/\(/,e:/\)/,c:[a,n]};return r.c=[n,a],{aliases:["pyxlscript"],k:t,i:/(<\/|->|\?)|=>|@|\$/,c:[{cN:"section",r:10,v:[{b:/^[^\n]+?\\n(-|─|—|━|⎯|=|═|⚌){5,}/}]},a,n,e.CLCM,e.CBCM,{v:[{cN:"function",bK:"def"}],e:/:/,i:/[${=;\n,]/,c:[e.UTM,i,{b:/->/,eW:!0,k:"None"}]}]}}),a.registerLanguage("r",function(e){var t="([a-zA-Z]|\\.[a-zA-Z.])[a-zA-Z0-9._]*";return{c:[e.HCM,{b:t,l:t,k:{keyword:"function if in break next repeat else for return switch while try tryCatch stop warning require library attach detach source setMethod setGeneric setGroupGeneric setClass ...",literal:"NULL NA TRUE FALSE T F Inf NaN NA_integer_|10 NA_real_|10 NA_character_|10 NA_complex_|10"},r:0},{cN:"number",b:"0[xX][0-9a-fA-F]+[Li]?\\b",r:0},{cN:"number",b:"\\d+(?:[eE][+\\-]?\\d*)?L\\b",r:0},{cN:"number",b:"\\d+\\.(?!\\d)(?:i\\b)?",r:0},{cN:"number",b:"\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d*)?i?\\b",r:0},{cN:"number",b:"\\.\\d+(?:[eE][+\\-]?\\d*)?i?\\b",r:0},{b:"`",e:"`",r:0},{cN:"string",c:[e.BE],v:[{b:'"',e:'"'},{b:"'",e:"'"}]}]}}),a.registerLanguage("ruby",function(e){var t="[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?",r={keyword:"and then defined module in return redo if BEGIN retry end for self when next until do begin unless END rescue else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor",literal:"true false nil"},n={cN:"doctag",b:"@[A-Za-z]+"},a={b:"#<",e:">"},i=[e.C("#","$",{c:[n]}),e.C("^\\=begin","^\\=end",{c:[n],r:10}),e.C("^__END__","\\n$")],s={cN:"subst",b:"#\\{",e:"}",k:r},o={cN:"string",c:[e.BE,s],v:[{b:/'/,e:/'/},{b:/"/,e:/"/},{b:/`/,e:/`/},{b:"%[qQwWx]?\\(",e:"\\)"},{b:"%[qQwWx]?\\[",e:"\\]"},{b:"%[qQwWx]?{",e:"}"},{b:"%[qQwWx]?<",e:">"},{b:"%[qQwWx]?/",e:"/"},{b:"%[qQwWx]?%",e:"%"},{b:"%[qQwWx]?-",e:"-"},{b:"%[qQwWx]?\\|",e:"\\|"},{b:/\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/},{b:/<<(-?)\w+$/,e:/^\s*\w+$/}]},l={cN:"params",b:"\\(",e:"\\)",endsParent:!0,k:r},c=[o,a,{cN:"class",bK:"class module",e:"$|;",i:/=/,c:[e.inherit(e.TM,{b:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{b:"<\\s*",c:[{b:"("+e.IR+"::)?"+e.IR}]}].concat(i)},{cN:"function",bK:"def",e:"$|;",c:[e.inherit(e.TM,{b:t}),l].concat(i)},{b:e.IR+"::"},{cN:"symbol",b:e.UIR+"(\\!|\\?)?:",r:0},{cN:"symbol",b:":(?!\\s)",c:[o,{b:t}],r:0},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{b:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{cN:"params",b:/\|/,e:/\|/,k:r},{b:"("+e.RSR+"|unless)\\s*",k:"unless",c:[a,{cN:"regexp",c:[e.BE,s],i:/\n/,v:[{b:"/",e:"/[a-z]*"},{b:"%r{",e:"}[a-z]*"},{b:"%r\\(",e:"\\)[a-z]*"},{b:"%r!",e:"![a-z]*"},{b:"%r\\[",e:"\\][a-z]*"}]}].concat(i),r:0}].concat(i);return{aliases:["rb","gemspec","podspec","thor","irb"],k:r,i:/\/\*/,c:i.concat([{b:/^\s*=>/,starts:{e:"$",c:l.c=s.c=c}},{cN:"meta",b:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+>|(\\w+-)?\\d+\\.\\d+\\.\\d(p\\d+)?[^>]+>)",starts:{e:"$",c:c}}]).concat(c)}}),a.registerLanguage("rust",function(e){var t="([ui](8|16|32|64|128|size)|f(32|64))?",r="drop i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize f32 f64 str char bool Box Option Result String Vec Copy Send Sized Sync Drop Fn FnMut FnOnce ToOwned Clone Debug PartialEq PartialOrd Eq Ord AsRef AsMut Into From Default Iterator Extend IntoIterator DoubleEndedIterator ExactSizeIterator SliceConcatExt ToString assert! assert_eq! bitflags! bytes! cfg! col! concat! concat_idents! debug_assert! debug_assert_eq! env! panic! file! format! format_args! include_bin! include_str! line! local_data_key! module_path! option_env! print! println! select! stringify! try! unimplemented! unreachable! vec! write! writeln! macro_rules! assert_ne! debug_assert_ne!";return{aliases:["rs"],k:{keyword:"alignof as be box break const continue crate do else enum extern false fn for if impl in let loop match mod mut offsetof once priv proc pub pure ref return self Self sizeof static struct super trait true type typeof unsafe unsized use virtual while where yield move default",literal:"true false Some None Ok Err",built_in:r},l:e.IR+"!?",i:"</",c:[e.CLCM,e.C("/\\*","\\*/",{c:["self"]}),e.inherit(e.QSM,{b:/b?"/,i:null}),{cN:"string",v:[{b:/r(#*)"(.|\n)*?"\1(?!#)/},{b:/b?'\\?(x\w{2}|u\w{4}|U\w{8}|.)'/}]},{cN:"symbol",b:/'[a-zA-Z_][a-zA-Z0-9_]*/},{cN:"number",v:[{b:"\\b0b([01_]+)"+t},{b:"\\b0o([0-7_]+)"+t},{b:"\\b0x([A-Fa-f0-9_]+)"+t},{b:"\\b(\\d[\\d_]*(\\.[0-9_]+)?([eE][+-]?[0-9_]+)?)"+t}],r:0},{cN:"function",bK:"fn",e:"(\\(|<)",eE:!0,c:[e.UTM]},{cN:"meta",b:"#\\!?\\[",e:"\\]",c:[{cN:"meta-string",b:/"/,e:/"/}]},{cN:"class",bK:"type",e:";",c:[e.inherit(e.UTM,{endsParent:!0})],i:"\\S"},{cN:"class",bK:"trait enum struct union",e:"{",c:[e.inherit(e.UTM,{endsParent:!0})],i:"[\\w\\d]"},{b:e.IR+"::",k:{built_in:r}},{b:"->"}]}}),a.registerLanguage("scheme",function(e){var t="[^\\(\\)\\[\\]\\{\\}\",'`;#|\\\\\\s]+",r="(\\-|\\+)?\\d+([./]\\d+)?",n={cN:"literal",b:"(#t|#f|#\\\\"+t+"|#\\\\.)"},a={cN:"number",v:[{b:r,r:0},{b:"(\\-|\\+)?\\d+([./]\\d+)?[+\\-](\\-|\\+)?\\d+([./]\\d+)?i",r:0},{b:"#b[0-1]+(/[0-1]+)?"},{b:"#o[0-7]+(/[0-7]+)?"},{b:"#x[0-9a-f]+(/[0-9a-f]+)?"}]},i=e.QSM,s=[e.C(";","$",{r:0}),e.C("#\\|","\\|#")],o={b:t,r:0},l={cN:"symbol",b:"'"+t},c={eW:!0,r:0},d={v:[{b:/'/},{b:"`"}],c:[{b:"\\(",e:"\\)",c:["self",n,i,a,o,l]}]},u={cN:"name",b:t,l:t,k:{"builtin-name":"case-lambda call/cc class define-class exit-handler field import inherit init-field interface let*-values let-values let/ec mixin opt-lambda override protect provide public rename require require-for-syntax syntax syntax-case syntax-error unit/sig unless when with-syntax and begin call-with-current-continuation call-with-input-file call-with-output-file case cond define define-syntax delay do dynamic-wind else for-each if lambda let let* let-syntax letrec letrec-syntax map or syntax-rules ' * + , ,@ - ... / ; < <= = => > >= ` abs acos angle append apply asin assoc assq assv atan boolean? caar cadr call-with-input-file call-with-output-file call-with-values car cdddar cddddr cdr ceiling char->integer char-alphabetic? char-ci<=? char-ci<? char-ci=? char-ci>=? char-ci>? char-downcase char-lower-case? char-numeric? char-ready? char-upcase char-upper-case? char-whitespace? char<=? char<? char=? char>=? char>? char? close-input-port close-output-port complex? cons cos current-input-port current-output-port denominator display eof-object? eq? equal? eqv? eval even? exact->inexact exact? exp expt floor force gcd imag-part inexact->exact inexact? input-port? integer->char integer? interaction-environment lcm length list list->string list->vector list-ref list-tail list? load log magnitude make-polar make-rectangular make-string make-vector max member memq memv min modulo negative? newline not null-environment null? number->string number? numerator odd? open-input-file open-output-file output-port? pair? peek-char port? positive? procedure? quasiquote quote quotient rational? rationalize read read-char real-part real? remainder reverse round scheme-report-environment set! set-car! set-cdr! sin sqrt string string->list string->number string->symbol string-append string-ci<=? string-ci<? string-ci=? string-ci>=? string-ci>? string-copy string-fill! string-length string-ref string-set! string<=? string<? string=? string>=? string>? string? substring symbol->string symbol? tan transcript-off transcript-on truncate values vector vector->list vector-fill! vector-length vector-ref vector-set! with-input-from-file with-output-to-file write write-char zero?"}},p={v:[{b:"\\(",e:"\\)"},{b:"\\[",e:"\\]"}],c:[{b:/lambda/,eW:!0,rB:!0,c:[u,{b:/\(/,e:/\)/,endsParent:!0,c:[o]}]},u,c]};return c.c=[n,a,i,o,l,d,p].concat(s),{i:/\S/,c:[{cN:"meta",b:"^#!",e:"$"},a,i,l,d,p].concat(s)}}),a.registerLanguage("shell",function(e){return{aliases:["console"],c:[{cN:"meta",b:"^\\s{0,3}[\\w\\d\\[\\]()@-]*[>%$#]",starts:{e:"$",sL:"bash"}}]}}),a.registerLanguage("sql",function(e){var t=e.C("--","$");return{cI:!0,i:/[<>{}*]/,c:[{bK:"begin end start commit rollback savepoint lock alter create drop rename call delete do handler insert load replace select truncate update set show pragma grant merge describe use explain help declare prepare execute deallocate release unlock purge reset change stop analyze cache flush optimize repair kill install uninstall checksum restore check backup revoke comment values with",e:/;/,eW:!0,l:/[\w\.]+/,k:{keyword:"as abort abs absolute acc acce accep accept access accessed accessible account acos action activate add addtime admin administer advanced advise aes_decrypt aes_encrypt after agent aggregate ali alia alias all allocate allow alter always analyze ancillary and anti any anydata anydataset anyschema anytype apply archive archived archivelog are as asc ascii asin assembly assertion associate asynchronous at atan atn2 attr attri attrib attribu attribut attribute attributes audit authenticated authentication authid authors auto autoallocate autodblink autoextend automatic availability avg backup badfile basicfile before begin beginning benchmark between bfile bfile_base big bigfile bin binary_double binary_float binlog bit_and bit_count bit_length bit_or bit_xor bitmap blob_base block blocksize body both bound bucket buffer_cache buffer_pool build bulk by byte byteordermark bytes cache caching call calling cancel capacity cascade cascaded case cast catalog category ceil ceiling chain change changed char_base char_length character_length characters characterset charindex charset charsetform charsetid check checksum checksum_agg child choose chr chunk class cleanup clear client clob clob_base clone close cluster_id cluster_probability cluster_set clustering coalesce coercibility col collate collation collect colu colum column column_value columns columns_updated comment commit compact compatibility compiled complete composite_limit compound compress compute concat concat_ws concurrent confirm conn connec connect connect_by_iscycle connect_by_isleaf connect_by_root connect_time connection consider consistent constant constraint constraints constructor container content contents context contributors controlfile conv convert convert_tz corr corr_k corr_s corresponding corruption cos cost count count_big counted covar_pop covar_samp cpu_per_call cpu_per_session crc32 create creation critical cross cube cume_dist curdate current current_date current_time current_timestamp current_user cursor curtime customdatum cycle data database databases datafile datafiles datalength date_add date_cache date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts day day_to_second dayname dayofmonth dayofweek dayofyear days db_role_change dbtimezone ddl deallocate declare decode decompose decrement decrypt deduplicate def defa defau defaul default defaults deferred defi defin define degrees delayed delegate delete delete_all delimited demand dense_rank depth dequeue des_decrypt des_encrypt des_key_file desc descr descri describ describe descriptor deterministic diagnostics difference dimension direct_load directory disable disable_all disallow disassociate discardfile disconnect diskgroup distinct distinctrow distribute distributed div do document domain dotnet double downgrade drop dumpfile duplicate duration each edition editionable editions element ellipsis else elsif elt empty enable enable_all enclosed encode encoding encrypt end end-exec endian enforced engine engines enqueue enterprise entityescaping eomonth error errors escaped evalname evaluate event eventdata events except exception exceptions exchange exclude excluding execu execut execute exempt exists exit exp expire explain explode export export_set extended extent external external_1 external_2 externally extract failed failed_login_attempts failover failure far fast feature_set feature_value fetch field fields file file_name_convert filesystem_like_logging final finish first first_value fixed flash_cache flashback floor flush following follows for forall force foreign form forma format found found_rows freelist freelists freepools fresh from from_base64 from_days ftp full function general generated get get_format get_lock getdate getutcdate global global_name globally go goto grant grants greatest group group_concat group_id grouping grouping_id groups gtid_subtract guarantee guard handler hash hashkeys having hea head headi headin heading heap help hex hierarchy high high_priority hosts hour hours http id ident_current ident_incr ident_seed identified identity idle_time if ifnull ignore iif ilike ilm immediate import in include including increment index indexes indexing indextype indicator indices inet6_aton inet6_ntoa inet_aton inet_ntoa infile initial initialized initially initrans inmemory inner innodb input insert install instance instantiable instr interface interleaved intersect into invalidate invisible is is_free_lock is_ipv4 is_ipv4_compat is_not is_not_null is_used_lock isdate isnull isolation iterate java join json json_exists keep keep_duplicates key keys kill language large last last_day last_insert_id last_value lateral lax lcase lead leading least leaves left len lenght length less level levels library like like2 like4 likec limit lines link list listagg little ln load load_file lob lobs local localtime localtimestamp locate locator lock locked log log10 log2 logfile logfiles logging logical logical_reads_per_call logoff logon logs long loop low low_priority lower lpad lrtrim ltrim main make_set makedate maketime managed management manual map mapping mask master master_pos_wait match matched materialized max maxextents maximize maxinstances maxlen maxlogfiles maxloghistory maxlogmembers maxsize maxtrans md5 measures median medium member memcompress memory merge microsecond mid migration min minextents minimum mining minus minute minutes minvalue missing mod mode model modification modify module monitoring month months mount move movement multiset mutex name name_const names nan national native natural nav nchar nclob nested never new newline next nextval no no_write_to_binlog noarchivelog noaudit nobadfile nocheck nocompress nocopy nocycle nodelay nodiscardfile noentityescaping noguarantee nokeep nologfile nomapping nomaxvalue nominimize nominvalue nomonitoring none noneditionable nonschema noorder nopr nopro noprom nopromp noprompt norely noresetlogs noreverse normal norowdependencies noschemacheck noswitch not nothing notice notnull notrim novalidate now nowait nth_value nullif nulls num numb numbe nvarchar nvarchar2 object ocicoll ocidate ocidatetime ociduration ociinterval ociloblocator ocinumber ociref ocirefcursor ocirowid ocistring ocitype oct octet_length of off offline offset oid oidindex old on online only opaque open operations operator optimal optimize option optionally or oracle oracle_date oradata ord ordaudio orddicom orddoc order ordimage ordinality ordvideo organization orlany orlvary out outer outfile outline output over overflow overriding package pad parallel parallel_enable parameters parent parse partial partition partitions pascal passing password password_grace_time password_lock_time password_reuse_max password_reuse_time password_verify_function patch path patindex pctincrease pctthreshold pctused pctversion percent percent_rank percentile_cont percentile_disc performance period period_add period_diff permanent physical pi pipe pipelined pivot pluggable plugin policy position post_transaction pow power pragma prebuilt precedes preceding precision prediction prediction_cost prediction_details prediction_probability prediction_set prepare present preserve prior priority private private_sga privileges procedural procedure procedure_analyze processlist profiles project prompt protection public publishingservername purge quarter query quick quiesce quota quotename radians raise rand range rank raw read reads readsize rebuild record records recover recovery recursive recycle redo reduced ref reference referenced references referencing refresh regexp_like register regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy reject rekey relational relative relaylog release release_lock relies_on relocate rely rem remainder rename repair repeat replace replicate replication required reset resetlogs resize resource respect restore restricted result result_cache resumable resume retention return returning returns reuse reverse revoke right rlike role roles rollback rolling rollup round row row_count rowdependencies rowid rownum rows rtrim rules safe salt sample save savepoint sb1 sb2 sb4 scan schema schemacheck scn scope scroll sdo_georaster sdo_topo_geometry search sec_to_time second seconds section securefile security seed segment select self semi sequence sequential serializable server servererror session session_user sessions_per_user set sets settings sha sha1 sha2 share shared shared_pool short show shrink shutdown si_averagecolor si_colorhistogram si_featurelist si_positionalcolor si_stillimage si_texture siblings sid sign sin size size_t sizes skip slave sleep smalldatetimefromparts smallfile snapshot some soname sort soundex source space sparse spfile split sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_small_result sql_variant_property sqlcode sqldata sqlerror sqlname sqlstate sqrt square standalone standby start starting startup statement static statistics stats_binomial_test stats_crosstab stats_ks_test stats_mode stats_mw_test stats_one_way_anova stats_t_test_ stats_t_test_indep stats_t_test_one stats_t_test_paired stats_wsr_test status std stddev stddev_pop stddev_samp stdev stop storage store stored str str_to_date straight_join strcmp strict string struct stuff style subdate subpartition subpartitions substitutable substr substring subtime subtring_index subtype success sum suspend switch switchoffset switchover sync synchronous synonym sys sys_xmlagg sysasm sysaux sysdate sysdatetimeoffset sysdba sysoper system system_user sysutcdatetime table tables tablespace tablesample tan tdo template temporary terminated tertiary_weights test than then thread through tier ties time time_format time_zone timediff timefromparts timeout timestamp timestampadd timestampdiff timezone_abbr timezone_minute timezone_region to to_base64 to_date to_days to_seconds todatetimeoffset trace tracking transaction transactional translate translation treat trigger trigger_nestlevel triggers trim truncate try_cast try_convert try_parse type ub1 ub2 ub4 ucase unarchived unbounded uncompress under undo unhex unicode uniform uninstall union unique unix_timestamp unknown unlimited unlock unnest unpivot unrecoverable unsafe unsigned until untrusted unusable unused update updated upgrade upped upper upsert url urowid usable usage use use_stored_outlines user user_data user_resources users using utc_date utc_timestamp uuid uuid_short validate validate_password_strength validation valist value values var var_samp varcharc vari varia variab variabl variable variables variance varp varraw varrawc varray verify version versions view virtual visible void wait wallet warning warnings week weekday weekofyear wellformed when whene whenev wheneve whenever where while whitespace window with within without work wrapped xdb xml xmlagg xmlattributes xmlcast xmlcolattval xmlelement xmlexists xmlforest xmlindex xmlnamespaces xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltype xor year year_to_month years yearweek",literal:"true false null unknown",built_in:"array bigint binary bit blob bool boolean char character date dec decimal float int int8 integer interval number numeric real record serial serial8 smallint text time timestamp tinyint varchar varying void"},c:[{cN:"string",b:"'",e:"'",c:[e.BE,{b:"''"}]},{cN:"string",b:'"',e:'"',c:[e.BE,{b:'""'}]},{cN:"string",b:"`",e:"`",c:[e.BE]},e.CNM,e.CBCM,t,e.HCM]},e.CBCM,t,e.HCM]}}),a.registerLanguage("swift",function(e){var t={keyword:"#available #colorLiteral #column #else #elseif #endif #file #fileLiteral #function #if #imageLiteral #line #selector #sourceLocation _ __COLUMN__ __FILE__ __FUNCTION__ __LINE__ Any as as! as? associatedtype associativity break case catch class continue convenience default defer deinit didSet do dynamic dynamicType else enum extension fallthrough false fileprivate final for func get guard if import in indirect infix init inout internal is lazy left let mutating nil none nonmutating open operator optional override postfix precedence prefix private protocol Protocol public repeat required rethrows return right self Self set static struct subscript super switch throw throws true try try! try? Type typealias unowned var weak where while willSet",literal:"true false nil",built_in:"abs advance alignof alignofValue anyGenerator assert assertionFailure bridgeFromObjectiveC bridgeFromObjectiveCUnconditional bridgeToObjectiveC bridgeToObjectiveCUnconditional c contains count countElements countLeadingZeros debugPrint debugPrintln distance dropFirst dropLast dump encodeBitsAsWords enumerate equal fatalError filter find getBridgedObjectiveCType getVaList indices insertionSort isBridgedToObjectiveC isBridgedVerbatimToObjectiveC isUniquelyReferenced isUniquelyReferencedNonObjC join lazy lexicographicalCompare map max maxElement min minElement numericCast overlaps partition posix precondition preconditionFailure print println quickSort readLine reduce reflect reinterpretCast reverse roundUpToAlignment sizeof sizeofValue sort split startsWith stride strideof strideofValue swap toString transcode underestimateCount unsafeAddressOf unsafeBitCast unsafeDowncast unsafeUnwrap unsafeReflect withExtendedLifetime withObjectAtPlusZero withUnsafePointer withUnsafePointerToObject withUnsafeMutablePointer withUnsafeMutablePointers withUnsafePointer withUnsafePointers withVaList zip"},r=e.C("/\\*","\\*/",{c:["self"]}),n={cN:"subst",b:/\\\(/,e:"\\)",k:t,c:[]},a={cN:"string",c:[e.BE,n],v:[{b:/"""/,e:/"""/},{b:/"/,e:/"/}]},i={cN:"number",b:"\\b([\\d_]+(\\.[\\deE_]+)?|0x[a-fA-F0-9_]+(\\.[a-fA-F0-9p_]+)?|0b[01_]+|0o[0-7_]+)\\b",r:0};return n.c=[i],{k:t,c:[a,e.CLCM,r,{cN:"type",b:"\\b[A-Z][\\wÀ-ʸ']*",r:0},i,{cN:"function",bK:"func",e:"{",eE:!0,c:[e.inherit(e.TM,{b:/[A-Za-z$_][0-9A-Za-z$_]*/}),{b:/</,e:/>/},{cN:"params",b:/\(/,e:/\)/,endsParent:!0,k:t,c:["self",i,a,e.CBCM,{b:":"}],i:/["']/}],i:/\[|%/},{cN:"class",bK:"struct protocol class extension enum",k:t,e:"\\{",eE:!0,c:[e.inherit(e.TM,{b:/[A-Za-z$_][\u00C0-\u02B80-9A-Za-z$_]*/})]},{cN:"meta",b:"(@discardableResult|@warn_unused_result|@exported|@lazy|@noescape|@NSCopying|@NSManaged|@objc|@objcMembers|@convention|@required|@noreturn|@IBAction|@IBDesignable|@IBInspectable|@IBOutlet|@infix|@prefix|@postfix|@autoclosure|@testable|@available|@nonobjc|@NSApplicationMain|@UIApplicationMain)"},{bK:"import",e:/$/,c:[e.CLCM,r]}]}}),a.registerLanguage("tex",function(e){var t={cN:"tag",b:/\\/,r:0,c:[{cN:"name",v:[{b:/[a-zA-Zа-яА-я]+[*]?/},{b:/[^a-zA-Zа-яА-я0-9]/}],starts:{eW:!0,r:0,c:[{cN:"string",v:[{b:/\[/,e:/\]/},{b:/\{/,e:/\}/}]},{b:/\s*=\s*/,eW:!0,r:0,c:[{cN:"number",b:/-?\d*\.?\d+(pt|pc|mm|cm|in|dd|cc|ex|em)?/}]}]}}]};return{c:[t,{cN:"formula",c:[t],r:0,v:[{b:/\$\$/,e:/\$\$/},{b:/\$/,e:/\$/}]},e.C("%","$",{r:0})]}}),a.registerLanguage("typescript",function(e){var t="[A-Za-z$_][0-9A-Za-z$_]*",r={keyword:"in if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const class public private protected get set super static implements enum export import declare type namespace abstract as from extends async await",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document any number boolean string void Promise"},n={cN:"meta",b:"@"+t},a={b:"\\(",e:/\)/,k:r,c:["self",e.QSM,e.ASM,e.NM]},i={cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,k:r,c:[e.CLCM,e.CBCM,n,a]};return{aliases:["ts"],k:r,c:[{cN:"meta",b:/^\s*['"]use strict['"]/},e.ASM,e.QSM,{cN:"string",b:"`",e:"`",c:[e.BE,{cN:"subst",b:"\\$\\{",e:"\\}"}]},e.CLCM,e.CBCM,{cN:"number",v:[{b:"\\b(0[bB][01]+)"},{b:"\\b(0[oO][0-7]+)"},{b:e.CNR}],r:0},{b:"("+e.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[e.CLCM,e.CBCM,e.RM,{cN:"function",b:"(\\(.*?\\)|"+e.IR+")\\s*=>",rB:!0,e:"\\s*=>",c:[{cN:"params",v:[{b:e.IR},{b:/\(\s*\)/},{b:/\(/,e:/\)/,eB:!0,eE:!0,k:r,c:["self",e.CLCM,e.CBCM]}]}]}],r:0},{cN:"function",b:"function",e:/[\{;]/,eE:!0,k:r,c:["self",e.inherit(e.TM,{b:t}),i],i:/%/,r:0},{bK:"constructor",e:/\{/,eE:!0,c:["self",i]},{b:/module\./,k:{built_in:"module"},r:0},{bK:"module",e:/\{/,eE:!0},{bK:"interface",e:/\{/,eE:!0,k:"interface extends"},{b:/\$[(.]/},{b:"\\."+e.IR,r:0},n,a]}}),a.registerLanguage("yaml",function(e){var t="true false yes no null",r="^[ \\-]*",n="[a-zA-Z_][\\w\\-]*",a={cN:"attr",v:[{b:r+n+":"},{b:r+'"'+n+'":'},{b:r+"'"+n+"':"}]},i={cN:"string",r:0,v:[{b:/'/,e:/'/},{b:/"/,e:/"/},{b:/\S+/}],c:[e.BE,{cN:"template-variable",v:[{b:"{{",e:"}}"},{b:"%{",e:"}"}]}]};return{cI:!0,aliases:["yml","YAML","yaml"],c:[a,{cN:"meta",b:"^---s*$",r:10},{cN:"string",b:"[\\|>] *$",rE:!0,c:i.c,e:a.v[0].b},{b:"<%[%=-]?",e:"[%-]?%>",sL:"ruby",eB:!0,eE:!0,r:0},{cN:"type",b:"!"+e.UIR},{cN:"type",b:"!!"+e.UIR},{cN:"meta",b:"&"+e.UIR+"$"},{cN:"meta",b:"\\*"+e.UIR+"$"},{cN:"bullet",b:"^ *-",r:0},e.HCM,{bK:t,k:{literal:t}},e.CNM,i]}}),a},r="object"==typeof window&&window||"object"==typeof self&&self,"undefined"!=typeof exports?t(exports):r&&(r.hljs=t({}),"function"==typeof define&&define.amd&&define([],function(){return r.hljs}));var n="Menlo,Consolas,monospace",a=Math.round(683.3555157/function(e){try{var t=document.createElement("canvas").getContext("2d");return t.font="10pt "+e,t.measureText("M").width}catch(e){return 10}}(n))+"%",o=(z("style",'body{max-width:680px;margin:auto;padding:20px;text-align:justify;line-height:140%; -webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-smoothing:antialiased;color:#222;font-family:Palatino,Georgia,"Times New Roman",serif}'),z("style",'@media print{*{-webkit-print-color-adjust:exact;text-shadow:none !important}}body{counter-reset: h1 h2 h3 h4 h5 h6 paragraph}@page{margin:0;size:auto}#mdContextMenu{position:absolute;background:#383838;cursor:default;border:1px solid #999;color:#fff;padding:4px 0px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,"Helvetica Neue",sans-serif;font-size:85%;font-weight:600;border-radius:4px;box-shadow:0px 3px 10px rgba(0,0,0,35%)}#mdContextMenu div{padding:0px 20px}#mdContextMenu div:hover{background:#1659d1}.md code,.md pre{font-family:'+n+";font-size:"+a+';text-align:left;line-height:140%}.md .mediumToc code,.md longToc code,.md .shortToc code,.md h1 code,.md h2 code,.md h3 code,.md h4 code,.md h5 code,.md h6 code{font-size:unset}.md div.title{font-size:26px;font-weight:800;line-height:120%;text-align:center}.md div.afterTitles{height:10px}.md div.subtitle{text-align:center}.md iframe.textinsert, .md object.textinsert,.md iframe:not(.markdeep){display:block;margin-top:10px;margin-bottom:10px;width:100%;height:75vh;border:1px solid #000;border-radius:4px;background:#f5f5f4}.md .image{display:inline-block}.md img{max-width:100%;page-break-inside:avoid}.md li{text-align:left;text-indent:0}.md pre.listing {width:100%;tab-size:4;-moz-tab-size:4;-o-tab-size:4;counter-reset:line;overflow-x:auto;resize:horizontal}.md pre.listing .linenumbers span.line:before{width:30px;margin-left:-28px;font-size:80%;text-align:right;counter-increment:line;content:counter(line);display:inline-block;padding-right:13px;margin-right:8px;color:#ccc}.md div.tilde{margin:20px 0 -10px;text-align:center}.md .imagecaption,.md .tablecaption,.md .listingcaption{display:inline-block;margin:7px 5px 12px;text-align:justify;font-style:italic}.md img.pixel{image-rendering:-moz-crisp-edges;image-rendering:pixelated}.md blockquote.fancyquote{margin:25px 0 25px;text-align:left;line-height:160%}.md blockquote.fancyquote::before{content:"“";color:#DDD;font-family:Times New Roman;font-size:45px;line-height:0;margin-right:6px;vertical-align:-0.3em}.md span.fancyquote{font-size:118%;color:#777;font-style:italic}.md span.fancyquote::after{content:"”";font-style:normal;color:#DDD;font-family:Times New Roman;font-size:45px;line-height:0;margin-left:6px;vertical-align:-0.3em}.md blockquote.fancyquote .author{width:100%;margin-top:10px;display:inline-block;text-align:right}.md small{font-size:60%}.md big{font-size:150%}.md div.title,contents,.md .tocHeader,.md h1,.md h2,.md h3,.md h4,.md h5,.md h6,.md .shortTOC,.md .mediumTOC,.nonumberh1,.nonumberh2,.nonumberh3,.nonumberh4,.nonumberh5,.nonumberh6{font-family:Verdana,Helvetica,Arial,sans-serif;margin:13.4px 0 13.4px;padding:15px 0 3px;border-top:none;clear:both}.md .tocTop {display:none}.md h1,.md h2,.md h3,.md h4,.md h5,.md h6,.md .nonumberh1,.md .nonumberh2,.md .nonumberh3,.md .nonumberh4,.md .nonumberh5,.md .nonumberh6{page-break-after:avoid;break-after:avoid}.md svg.diagram{display:block;font-family:'+n+";font-size:"+a+";text-align:center;stroke-linecap:round;stroke-width:"+W+'px;page-break-inside:avoid;stroke:#000;fill:#000}.md svg.diagram .opendot{fill:#fff}.md svg.diagram .shadeddot{fill:#CCC}.md svg.diagram .dotteddot{stroke:#000;stroke-dasharray:4;fill:none}.md svg.diagram text{stroke:none}@media print{@page{margin:1in 5mm;transform: scale(150%)}}@media print{.md .pagebreak{page-break-after:always;visibility:hidden}}.md a{font-family:Georgia,Palatino,\'Times New Roman\'}.md h1,.md .tocHeader,.md .nonumberh1{border-bottom:3px solid;font-size:20px;font-weight:bold;}.md h1,.md .nonumberh1{counter-reset: h2 h3 h4 h5 h6}.md h2,.md .nonumberh2{counter-reset: h3 h4 h5 h6;border-bottom:2px solid #999;color:#555;font-weight:bold;font-size:18px;}.md h3,.md h4,.md h5,.md h6,.md .nonumberh3,.md .nonumberh4,.md .nonumberh5,.md .nonumberh6{font-family:Verdana,Helvetica,Arial,sans-serif;color:#555;font-size:16px;}.md h3{counter-reset:h4 h5 h6}.md h4{counter-reset:h5 h6}.md h5{counter-reset:h6}.md div.table{margin:16px 0 16px 0}.md table{border-collapse:collapse;line-height:140%;page-break-inside:avoid}.md table.table{margin:auto}.md table.calendar{width:100%;margin:auto;font-size:11px;font-family:Verdana,Helvetica,Arial,sans-serif}.md table.calendar th{font-size:16px}.md .today{background:#ECF8FA}.md .calendar .parenthesized{color:#999;font-style:italic}.md table.table th{color:#FFF;background-color:#AAA;border:1px solid #888;padding:8px 15px 8px 15px}.md table.table td{padding:5px 15px 5px 15px;border:1px solid #888}.md table.table tr:nth-child(even){background:#EEE}.md pre.tilde{border-top: 1px solid #CCC;border-bottom: 1px solid #CCC;padding: 5px 0 5px 20px;margin:0 0 0 0;background:#FCFCFC;page-break-inside:avoid}.md a.target{width:0px;height:0px;visibility:hidden;font-size:0px;display:inline-block}.md a:link, .md a:visited{color:#38A;text-decoration:none}.md a:link:hover{text-decoration:underline}.md dt{font-weight:700}.md dl>dd{margin-top:-8px; margin-bottom:8px}.md dl>table{margin:35px 0 30px}.md code{page-break-inside:avoid;} @media print{.md .listing code{white-space:pre-wrap}}.md .endnote{font-size:13px;line-height:15px;padding-left:10px;text-indent:-10px}.md .bib{padding-left:80px;text-indent:-80px;text-align:left}.markdeepFooter{font-size:9px;text-align:right;padding-top:80px;color:#999}.md .mediumTOC{float:right;font-size:12px;line-height:15px;border-left:1px solid #CCC;padding-left:15px;margin:15px 0px 15px 25px}.md .mediumTOC .level1{font-weight:600}.md .longTOC .level1{font-weight:600;display:block;padding-top:12px;margin:0 0 -20px}.md .shortTOC{text-align:center;font-weight:bold;margin-top:15px;font-size:14px}.md .admonition{position:relative;margin:1em 0;padding:.4rem 1rem;border-radius:.2rem;border-left:2.5rem solid rgba(68,138,255,.4);background-color:rgba(68,138,255,.15);}.md .admonition-title{font-weight:bold;border-bottom:solid 1px rgba(68,138,255,.4);padding-bottom:4px;margin-bottom:4px;margin-left: -1rem;padding-left:1rem;margin-right:-1rem;border-color:rgba(68,138,255,.4)}.md .admonition.tip{border-left:2.5rem solid rgba(50,255,90,.4);background-color:rgba(50,255,90,.15)}.md .admonition.tip::before{content:"\\24d8";font-weight:bold;font-size:150%;position:relative;top:3px;color:rgba(26,128,46,.8);left:-2.95rem;display:block;width:0;height:0}.md .admonition.tip>.admonition-title{border-color:rgba(50,255,90,.4)}.md .admonition.warn,.md .admonition.warning{border-left:2.5rem solid rgba(255,145,0,.4);background-color:rgba(255,145,0,.15)}.md .admonition.warn::before,.md .admonition.warning::before{content:"\\26A0";font-weight:bold;font-size:150%;position:relative;top:2px;color:rgba(128,73,0,.8);left:-2.95rem;display:block;width:0;height:0}.md .admonition.warn>.admonition-title,.md .admonition.warning>.admonition-title{border-color:rgba(255,145,0,.4)}.md .admonition.error{border-left: 2.5rem solid rgba(255,23,68,.4);background-color:rgba(255,23,68,.15)}.md .admonition.error>.admonition-title{border-color:rgba(255,23,68,.4)}.md .admonition.error::before{content: "\\2612";font-family:"Arial";font-size:200%;position:relative;color:rgba(128,12,34,.8);top:-2px;left:-3rem;display:block;width:0;height:0}.md .admonition p:last-child{margin-bottom:0}.md li.checked,.md li.unchecked{list-style:none;overflow:visible;text-indent:-1.2em}.md li.checked:before,.md li.unchecked:before{content:"\\2611";display:block;float:left;width:1em;font-size:120%}.md li.unchecked:before{content:"\\2610"}')),i={mode:"markdeep",detectMath:!0,lang:{keyword:{}},tocStyle:"auto",hideEmptyWeekends:!0,showLabels:!1,sortScheduleLists:!0,definitionStyle:"auto",linkAPIDefinitions:!1,scrollThreshold:90,captionAbove:{diagram:!1,image:!1,table:!1,listing:!1},smartQuotes:!0},s={en:{keyword:{}},ru:{keyword:{table:"таблица",figure:"рисунок",listing:"листинг",diagram:"диаграмма",contents:"Содержание",sec:"сек",section:"раздел",subsection:"подраздел",chapter:"глава",Monday:"понедельник",Tuesday:"вторник",Wednesday:"среда",Thursday:"четверг",Friday:"пятница",Saturday:"суббота",Sunday:"воскресенье",January:"январьr",February:"февраль",March:"март",April:"апрель",May:"май",June:"июнь",July:"июль",August:"август",September:"сентябрь",October:"октябрь",November:"ноябрь",December:"декабрь",jan:"янв",feb:"февр",mar:"март",apr:"апр",may:"май",jun:"июнь",jul:"июль",aug:"авг",sep:"сент",oct:"окт",nov:"ноябрь",dec:"дек","&ldquo;":"«","&rdquo;":"»"}},fr:{keyword:{table:"tableau",figure:"figure",listing:"liste",diagram:"diagramme",contents:"Table des matières",sec:"sec",section:"section",subsection:"paragraphe",chapter:"chapitre",Monday:"lundi",Tuesday:"mardi",Wednesday:"mercredi",Thursday:"jeudi",Friday:"vendredi",Saturday:"samedi",Sunday:"dimanche",January:"Janvier",February:"Février",March:"Mars",April:"Avril",May:"Mai",June:"Juin",July:"Julliet",August:"Août",September:"Septembre",October:"Octobre",November:"Novembre",December:"Décembre",jan:"janv",feb:"févr",mar:"mars",apr:"avril",may:"mai",jun:"juin",jul:"juil",aug:"août",sep:"sept",oct:"oct",nov:"nov",dec:"déc","&ldquo;":"&laquo;&nbsp;","&rtquo;":"&nbsp;&raquo;"}},pl:{keyword:{table:"tabela",figure:"ilustracja",listing:"wykaz",diagram:"diagram",contents:"Spis treści",sec:"rozdz.",section:"rozdział",subsection:"podrozdział",chapter:"kapituła",Monday:"Poniedziałek",Tuesday:"Wtorek",Wednesday:"Środa",Thursday:"Czwartek",Friday:"Piątek",Saturday:"Sobota",Sunday:"Niedziela",January:"Styczeń",February:"Luty",March:"Marzec",April:"Kwiecień",May:"Maj",June:"Czerwiec",July:"Lipiec",August:"Sierpień",September:"Wrzesień",October:"Październik",November:"Listopad",December:"Grudzień",jan:"sty",feb:"lut",mar:"mar",apr:"kwi",may:"maj",jun:"cze",jul:"lip",aug:"sie",sep:"wrz",oct:"paź",nov:"lis",dec:"gru","&ldquo;":"&bdquo;","&rdquo;":"&rdquo;"}},bg:{keyword:{table:"таблица",figure:"фигура",listing:"списък",diagram:"диаграма",contents:"cъдържание",sec:"сек",section:"раздел",subsection:"подраздел",chapter:"глава",Monday:"понеделник",Tuesday:"вторник",Wednesday:"сряда",Thursday:"четвъртък",Friday:"петък",Saturday:"събота",Sunday:"неделя",January:"януари",February:"февруари",March:"март",April:"април",May:"май",June:"юни",July:"юли",August:"август",September:"септември",October:"октомври",November:"ноември",December:"декември",jan:"ян",feb:"февр",mar:"март",apr:"апр",may:"май",jun:"юни",jul:"юли",aug:"авг",sep:"септ",oct:"окт",nov:"ноем",dec:"дек","&ldquo;":"&bdquo;","&rdquo;":"&rdquo;"}},de:{keyword:{table:"Tabelle",figure:"Abbildung",listing:"Auflistung",diagram:"Diagramm",contents:"Inhaltsverzeichnis",sec:"Kap",section:"Kapitel",subsection:"Unterabschnitt",chapter:"Kapitel",Monday:"Montag",Tuesday:"Dienstag",Wednesday:"Mittwoch",Thursday:"Donnerstag",Friday:"Freitag",Saturday:"Samstag",Sunday:"Sonntag",January:"Januar",February:"Februar",March:"März",April:"April",May:"Mai",June:"Juni",July:"Juli",August:"August",September:"September",October:"Oktober",November:"November",December:"Dezember",jan:"Jan",feb:"Feb",mar:"Mär",apr:"Apr",may:"Mai",jun:"Jun",jul:"Jul",aug:"Aug",sep:"Sep",oct:"Okt",nov:"Nov",dec:"Dez","&ldquo;":"&bdquo;","&rdquo;":"&ldquo;"}},hu:{keyword:{table:"táblázat",figure:"ábra",listing:"lista",diagram:"diagramm",contents:"Tartalomjegyzék",sec:"fej",section:"fejezet",subsection:"alfejezet",chapter:"fejezet",Monday:"hétfő",Tuesday:"kedd",Wednesday:"szerda",Thursday:"csütörtök",Friday:"péntek",Saturday:"szombat",Sunday:"vasárnap",January:"január",February:"február",March:"március",April:"április",May:"május",June:"június",July:"július",August:"augusztus",September:"szeptember",October:"október",November:"november",December:"december",jan:"jan",feb:"febr",mar:"márc",apr:"ápr",may:"máj",jun:"jún",jul:"júl",aug:"aug",sep:"szept",oct:"okt",nov:"nov",dec:"dec","&ldquo;":"&bdquo;","&rdquo;":"&rdquo;"}},sv:{keyword:{table:"tabell",figure:"figur",listing:"lista",diagram:"diagram",contents:"Innehållsförteckning",sec:"sek",section:"sektion",subsection:"sektion",chapter:"kapitel",Monday:"måndag",Tuesday:"tisdag",Wednesday:"onsdag",Thursday:"torsdag",Friday:"fredag",Saturday:"lördag",Sunday:"söndag",January:"januari",February:"februari",March:"mars",April:"april",May:"maj",June:"juni",July:"juli",August:"augusti",September:"september",October:"oktober",November:"november",December:"december",jan:"jan",feb:"feb",mar:"mar",apr:"apr",may:"maj",jun:"jun",jul:"jul",aug:"aug",sep:"sep",oct:"okt",nov:"nov",dec:"dec","&ldquo;":"&rdquo;","&rdquo;":"&rdquo;"}},pt:{keyword:{table:"tabela",figure:"figura",listing:"lista",diagram:"diagrama",contents:"conteúdo",sec:"sec",section:"secção",subsection:"subsecção",chapter:"capítulo",Monday:"Segunda-feira",Tuesday:"Terça-feira",Wednesday:"Quarta-feira",Thursday:"Quinta-feira",Friday:"Sexta-feira",Saturday:"Sábado",Sunday:"Domingo",January:"Janeiro",February:"Fevereiro",March:"Março",April:"Abril",May:"Maio",June:"Junho",July:"Julho",August:"Agosto",September:"Setembro",October:"Outubro",November:"Novembro",December:"Dezembro",jan:"jan",feb:"fev",mar:"mar",apr:"abr",may:"mai",jun:"jun",jul:"jul",aug:"ago",sep:"set",oct:"oct",nov:"nov",dec:"dez","&ldquo;":"&laquo;","&rtquo;":"&raquo;"}},ja:{keyword:{table:"表",figure:"図",listing:"一覧",diagram:"図",contents:"目次",sec:"節",section:"節",subsection:"項",chapter:"章",Monday:"月",Tuesday:"火",Wednesday:"水",Thursday:"木",Friday:"金",Saturday:"土",Sunday:"日",January:"1月",February:"2月",March:"3月",April:"4月",May:"5月",June:"6月",July:"7月",August:"8月",September:"9月",October:"10月",November:"11月",December:"12月",jan:"1月",feb:"2月",mar:"3月",apr:"4月",may:"5月",jun:"6月",jul:"7月",aug:"8月",sep:"9月",oct:"10月",nov:"11月",dec:"12月","&ldquo;":"「","&rdquo;":"」"}},it:{keyword:{table:"tabella",figure:"figura",listing:"lista",diagram:"diagramma",contents:"indice",sec:"sez",section:"sezione",subsection:"paragrafo",chapter:"capitolo",Monday:"lunedì",Tuesday:"martedì",Wednesday:"mercoledì",Thursday:"giovedì",Friday:"venerdì",Saturday:"sabato",Sunday:"domenica",January:"Gennaio",February:"Febbraio",March:"Marzo",April:"Aprile",May:"Maggio",June:"Giugno",July:"Luglio",August:"Agosto",September:"Settembre",October:"Ottobre",November:"Novembre",December:"Dicembre",jan:"gen",feb:"feb",mar:"mar",apr:"apr",may:"mag",jun:"giu",jul:"lug",aug:"ago",sep:"set",oct:"ott",nov:"nov",dec:"dic","&ldquo;":"&ldquo;","&rtquo;":"&rdquo;"}},lt:{keyword:{table:"lentelė",figure:"paveikslėlis",listing:"sąrašas",diagram:"diagrama",contents:"Turinys",sec:"sk",section:"skyrius",subsection:"poskyris",chapter:"skyrius",Monday:"pirmadienis",Tuesday:"antradienis",Wednesday:"trečiadienis",Thursday:"ketvirtadienis",Friday:"penktadienis",Saturday:"šeštadienis",Sunday:"sekmadienis",January:"Sausis",February:"Vasaris",March:"Kovas",April:"Balandis",May:"Gegužė",June:"Birželis",July:"Liepa",August:"Rugpjūtis",September:"Rugsėjis",October:"Spalis",November:"Lapkritis",December:"Gruodis",jan:"saus",feb:"vas",mar:"kov",apr:"bal",may:"geg",jun:"birž",jul:"liep",aug:"rugpj",sep:"rugs",oct:"spal",nov:"lapkr",dec:"gruod","&ldquo;":"&bdquo;","&rtquo;":"&ldquo;"}},cz:{keyword:{table:"Tabulka",figure:"Obrázek",listing:"Seznam",diagram:"Diagram",contents:"Obsah",sec:"kap.",section:"kapitola",subsection:"podkapitola",chapter:"kapitola",Monday:"pondělí",Tuesday:"úterý",Wednesday:"středa",Thursday:"čtvrtek",Friday:"pátek",Saturday:"sobota",Sunday:"neděle",January:"leden",February:"únor",March:"březen",April:"duben",May:"květen",June:"červen",July:"červenec",August:"srpen",September:"září",October:"říjen",November:"listopad",December:"prosinec",jan:"led",feb:"úno",mar:"bře",apr:"dub",may:"kvě",jun:"čvn",jul:"čvc",aug:"srp",sep:"zář",oct:"říj",nov:"lis",dec:"pro","&ldquo;":"&bdquo;","&rdquo;":"&ldquo;"}},es:{keyword:{table:"Tabla",figure:"Figura",listing:"Listado",diagram:"Diagrama",contents:"Tabla de Contenidos",sec:"sec",section:"Sección",subsection:"Subsección",chapter:"Capítulo",Monday:"Lunes",Tuesday:"Martes",Wednesday:"Miércoles",Thursday:"Jueves",Friday:"Viernes",Saturday:"Sábado",Sunday:"Domingo",January:"Enero",February:"Febrero",March:"Marzo",April:"Abril",May:"Mayo",June:"Junio",July:"Julio",August:"Agosto",September:"Septiembre",October:"Octubre",November:"Noviembre",December:"Diciembre",jan:"ene",feb:"feb",mar:"mar",apr:"abr",may:"may",jun:"jun",jul:"jul",aug:"ago",sep:"sept",oct:"oct",nov:"nov",dec:"dic","&ldquo;":"&laquo;&nbsp;","&rtquo;":"&nbsp;&raquo;"}}};[].slice.call(document.getElementsByTagName("meta")).forEach(function(e){var t=e.getAttribute("lang");if(t){var r=s[t];r&&(i.lang=r)}});var Z=Math.max,J=Math.min,G=Math.abs,K=Math.sign||function(e){return+e===e?0===e?e:0<e?1:-1:NaN};function U(e,t){if(window.markdeepOptions&&void 0!==window.markdeepOptions[e]){var r=window.markdeepOptions[e];return t?void 0!==(r=r[t])?r:i[e][t]:window.markdeepOptions[e]}return void 0!==i[e]?t?i[e][t]:i[e]:void console.warn('Illegal option: "'+e+'"')}function I(e,t){if(U("showLabels")){var r=" { "+e+" }";return t?z(t,r):r}return""}function q(e){return U("lang").keyword[e]||U("lang").keyword[e.toLowerCase()]||e}function Q(e){return String(e).rp(/&/g,"&amp;").rp(/</g,"&lt;").rp(/>/g,"&gt;").rp(/"/g,"&quot;")}function $(e){return e.rp(/&lt;/g,"<").rp(/&gt;/g,">").rp(/&quot;/g,'"').rp(/&#39;/g,"'").rp(/&ndash;/g,"–").rp(/&mdash;/g,"---").rp(/&amp;/g,"&")}function R(e){return e.rp(/<.*?>/g,"")}function O(e){return encodeURI(e.rp(/\s/g,"").toLowerCase())}function l(){for(var e="",t=1;t<=6;++t){e+=".md h"+t+"::before {\ncontent:";for(var r=1;r<=t;++r)e+="counter(h"+r+') "'+(r<t?".":" ")+'"';e+=";\ncounter-increment: h"+t+";margin-right:10px}"}return z("style",e)}function c(e){var t=e?e.innerHTML:"";return t=$(t=(t=(t=t.rp(/<\/https?:.*>|<\/ftp:.*>|<\/[^ "\t\n>]+@[^ "\t\n>]+>/gi,"")).rp(/<(https?|ftp): (.*?)>/gi,function(e,t,r){var n="<"+t+"://"+r.rp(/=""\s/g,"/");return'=""'===n.ss(n.length-3)&&(n=n.ss(0,n.length-3)),(n=n.rp(/"/g,""))+">"})).rp(/<style class=["']fallback["']>.*?<\/style>/gim,""))}function F(e){function t(){c=e.indexOf("\n",i)+1,d=function(e,t,r){for(var n=t,a=t;a<r;++a,++n){var i=e.charCodeAt(n);n+=55296<=i&&i<=56319}return n-r}(e,i+s,i+o),u=u||/\S/.test(e.ss(i,i+s)),p=!(m=m||"*"!==e[i+o+d])&&(p||/[^ *\t\n\r]/.test(e.ss(i+o+d+1,c)))}for(var r={beforeString:e,diagramString:"",alignmentHint:"",afterString:""},n=e.indexOf(_);0<=n;n=e.indexOf(_,n+_.length)){var a,i=Z(0,e.lastIndexOf("\n",n))+1,s=n-i;for(a=n+_.length;e[a]===x;++a);var o=a-i-1,l={beforeString:e.ss(0,i),diagramString:"",alignmentHint:"center",afterString:e.ss(i,n).rp(/[ \t]+$/," ")},c=0,d=0,u=!1,p=!1,m=!1;t();for(var g=!0,b=a;g;){if(i=c,t(),0===i)return r;if(u?l.alignmentHint="floatright":p&&(l.alignmentHint="floatleft"),e[i+s]!==x||u&&e[i+o+d]!==x)g=!1;else{var f;for(f=s;f<o&&e[i+f]===x;++f);var h=i+s,y=i+o+d;if(!u){var v=e.indexOf("\n",h);-1!==v&&(y=Math.min(y,v))}if(l.afterString+=e.ss(b,h).rp(/^[ \t]*[ \t]/," ").rp(/[ \t][ \t]*$/," "),f===o)return l.afterString+=e.ss(i+o+1),l;l.diagramString+=e.ss(h+1,y)+"\n",b=y+1}}}return r}function X(e,t,r,n){var a=t.source;return e.rp(new RegExp("([^A-Za-z0-9])("+a+")("+("[^ \\t\\n"+a+"]")+".*?(\\n.+?)*?)"+a+"(?![A-Za-z0-9])","g"),"$1<"+r+(n?" "+n:"")+">$3</"+r+">")}function Y(e,t){return!(!e||!t)&&(e=e.match(/\n/g),t=t.match(/\n/g),e&&1<e.length&&t&&1<t.length)}function d(a,e){var n={},i=0,s={},r="",o=32,l=[],c=4,d=RegExp(r+"[0-9a-w]{"+c+","+c+"}"+r,"g");function protect(e){for(var t=(l.push(e)-1).toString(o);t.length<c;)t="0"+t;return r+t+r}var u=!1;function p(e){var t=parseInt(e.ss(1,e.length-1),o);return u=!0,l[t]}function t(e,t){return protect(t)}function m(r){return function(e,t){return"\n\n</p>\n<a "+protect('class="target" name="'+O(R(t.rp(d,p)))+'"')+">&nbsp;</a>"+z("h"+r,t)+"\n<p>\n\n"}}void 0===e&&(e=!0),void 0!==a.innerHTML&&(a=a.innerHTML),a=(a="\n\n"+a).rp(/<script\s+type\s*=\s*['"]preformatted['"]\s*>([\s\S]*?)<\/script>/gi,"$1");function g(m,g){var e=new RegExp("\n([ \\t]*)"+g+"{3,}([ \\t]*\\S*)([ \\t]+.+)?\n([\\s\\S]+?)\n\\1"+g+"{3,}[ \t]*\n([ \\t]*\\[.+(?:\n.+){0,3}\\])?","g");a=a.rp(e,function(e,t,r,n,a,i){i&&(i=i.trim(),i=z("center","<div "+protect('class="listingcaption '+m+'"')+">"+i.ss(1,i.length-1)+"</div>")+"\n"),a=a.rp(new RegExp("(^|\n)"+t,"g"),"$1");var s,o,l,c=U("captionAbove","listing"),d=[];do{var u;if(s=o=l=void 0,a=a.rp(new RegExp("\\n([ \\t]*)"+g+"{3,}([ \\t]*\\S+)([ \\t]+.+)?\n([\\s\\S]*)"),function(e,t,r,n,a){return o=r,l=n,s=a,""}),"none"===(r=r?r.trim():void 0))u=hljs.highlightAuto(a,[]);else if(void 0===r)u=hljs.highlightAuto(a);else try{u=hljs.highlight(r,a,!0)}catch(e){u=hljs.highlightAuto(a,[])}var p=u.value;p=p.rp(/^(.*)$/gm,z("span","$1",'class="line"')),n&&(p=z("div",p,'class="'+n+'"')),d.push(p),a=s,r=o,n=l}while(a);return"\n"+t+"</p>"+(i&&c?i:"")+protect(z("pre",z("code",d.join("")),'class="listing '+m+'"'))+(i&&!c?i:"")+"<p>\n"})}g("tilde","~"),g("backtick","`"),a=(a=(a=(a=(a=(a=(a=(a=(a=(a=(a=(a=(a=(a=(a=(a=(a=(a=function e(t){var r=F(t);if(r.diagramString){r.afterString=r.afterString.rp(/^\n*[ \t]*\[[^\n]+\][ \t]*(?=\n)/,function(e){return e=(e=e.trim()).ss(1,e.length-1),r.caption=z("center",z("div",e,protect('class="imagecaption"'))),""});var n=re(r.diagramString,r.alignmentHint),a=U("captionAbove","diagram");return r.beforeString+(r.caption&&a?r.caption:"")+n+(r.caption&&!a?r.caption:"")+"\n"+e(r.afterString)}return t}(a=(a=a.rp(/(<code\b.*?<\/code>)/gi,t)).rp(/<!--[^-][\s\S]*?-->/g,""))).rp(/<svg( .*?)?>([\s\S]*?)<\/svg>/gi,function(e,t,r){return"<svg"+protect(t)+">"+protect(r)+"</svg>"})).rp(/<style>([\s\S]*?)<\/style>/gi,function(e,t){return z("style",protect(t))})).rp(/<img\s+src=(["'])[\s\S]*?\1\s*>/gi,function(e,t){return"<img "+protect(e.ss(5,e.length-1))+">"})).rp(/(^|[^\\])(`)(.*?(?:\n.*?)?[^\n\\`])`(?!\d)/g,"$1<code>$3</code>")).rp(/\\`/g,"`")).rp(/(<code(?: .*?)?>)([\s\S]*?)<\/code>/gi,function(e,t,r){return protect(t+Q(r)+"</code>")})).rp(/(<pre\b[\s\S]*?<\/pre>)/gi,t)).rp(/(<\w[^ \n<>]*?[ \t]+)(.*?)(?=\/?>)/g,function(e,t,r){return t+protect(r)})).rp(/(\$\$[\s\S]+?\$\$)/g,t)).rp(/((?:[^\w\d]))\$(\S(?:[^\$]*?\S(?!US|Can))??)\$(?![\w\d])/g,"$1\\($2\\)")).rp(/((?:[^\w\d]))\$([ \t][^\$]+?[ \t])\$(?![\w\d])/g,"$1\\($2\\)")).rp(/(\\\([\s\S]+?\\\))/g,t)).rp(/(\\begin\{equation\}[\s\S]*?\\end\{equation\})/g,t)).rp(/(\\begin\{eqnarray\}[\s\S]*?\\end\{eqnarray\})/g,t)).rp(/(\\begin\{equation\*\}[\s\S]*?\\end\{equation\*\})/g,t)).rp(/(?:^|\s*\n)(.+?)\n[ \t]*={3,}[ \t]*\n/g,m(1))).rp(/(?:^|\s*\n)(.+?)\n[ \t]*-{3,}[ \t]*\n/g,m(2));for(var b=6;0<b;--b)a=(a=a.rp(new RegExp(/^\s*/.source+"#{"+b+","+b+"}(?:[ \t])([^\n]+?)#*[ \t]*\n","gm"),m(b))).rp(new RegExp(/^\s*/.source+"\\(#{"+b+","+b+"}\\)(?:[ \t])([^\n]+?)\\(?#*\\)?\\n[ \t]*\n","gm"),"\n</p>\n"+z("div","$1",protect('class="nonumberh'+b+'"'))+"\n<p>\n\n");a=(a=(a=a.rp(/\n[ \t]*((\*|-|_)[ \t]*){3,}[ \t]*\n/g,"\n<hr/>\n")).rp(/\n[ \t]*\+{5,}[ \t]*\n/g,"\n<hr "+protect('class="pagebreak"')+"/>\n")).rp(/^!!![ \t]*([^\s"'><&\:]*)\:?(.*)\n([ \t]{3,}.*\s*\n)*/gm,function(e,t,r){return e=e.trim(),"\n\n"+z("div",((r?z("div",r,protect('class="admonition-title"'))+"\n":"")+e.ss(e.indexOf("\n"))).trim(),protect('class="admonition '+t.toLowerCase().trim()+'"'))+"\n\n"});var f=protect('class="fancyquote"');a=a.rp(/\n>[ \t]*"(.*(?:\n>.*)*)"[ \t]*(?:\n>[ \t]*)?(\n>[ \t]{2,}\S.*)?\n/g,function(e,t,r){return z("blockquote",z("span",t.rp(/\n>/g,"\n"),f)+(r?z("span",r.rp(/\n>/g,"\n"),protect('class="author"')):""),f)});for(var h=!1;h=!1,a=a.rp(/(?:\n>.*){2,}/g,function(e){return h=!0,z("blockquote",e.rp(/\n>/g,"\n"))}),h;);function y(e,t){var r=t.toLowerCase().trim();return r in n||(n[r]=++i),"<sup><a "+protect('href="#endnote-'+r+'"')+">"+n[r]+"</a></sup>"}a=(a=(a=function(e,protect){var t=/(?:\n[ \t]*(?:(?:\|?[ \t\S]+?(?:\|[ \t\S]+?)+\|?)|\|[ \t\S]+\|)(?=\n))/.source,r=new RegExp(t+/\n[ \t]*(?:(?:\|? *\:?-+\:?(?: *\| *\:?-+\:?)+ *\|?|)|\|[\:-]+\|)(?=\n)/.source+t+"+("+/\n[ \t]*\[[^\n\|]+\][ \t]*(?=\n)/.source+")?","g");function p(e){return e.trim().rp(/^\||\|$/g,"")}return e=e.rp(r,function(e){var t=e.split("\n"),r="",n=""===t[0]?1:0,a=t[t.length-1].trim();a=3<a.length&&"["===a[0]&&"]"===a[a.length-1]?(t.pop(),a.ss(1,a.length-1)):void 0;var i=[];p(t[1+n]).rp(/:?-+:?/g,function(e){var t=":"===e[e.length-1];i.push(protect(' style="text-align:'+(":"===e[0]&&t?"center":t?"right":"left")+'"'))});for(var s=t[1+n].trim(),o="|"===s[0],l="|"===s[s.length-1],c="th",d=n;d<t.length;++d){s=t[d].trim(),o||"|"!==s[0]||(s="&nbsp;"+s),l||"|"!==s[s.length-1]||(s+="&nbsp;"),s=p(s);var u=0;r+=z("tr","<"+c+i[0]+"> "+s.rp(/ *\| */g,function(){return" </"+c+"><"+c+i[++u]+"> "})+" </"+c+">")+"\n",d==n&&(++d,c="td")}return r=z("table",r,protect('class="table"')),a&&(a=z("center",z("div",a,protect('class="tablecaption"'))),r=U("captionAbove","table")?a+r:"\n"+r+a),z("div",r,"class='table'")})}(a=(a=(a=(a=a.rp(/[ \t]*\[\^([^\]\n\t ]+)\](?!:)/g,y)).rp(/(\S)[ \t]*\[\^([^\]\n\t ]+)\]/g,function(e,t,r){return t+y(0,r)})).rp(/\n\[#(\S+)\]:[ \t]+((?:[ \t]*\S[^\n]*\n?)*)/g,function(e,t,r){return t=t.trim(),"<div "+protect('class="bib"')+">[<a "+protect('class="target" name="citation-'+t.toLowerCase()+'"')+">&nbsp;</a><b>"+t+"</b>] "+r+"</div>"})).rp(/\[(#[^\)\(\[\]\.#\s]+(?:\s*,\s*#(?:[^\)\(\[\]\.#\s]+))*)\]/g,function(e,t){t=t.split(",");for(var r="[",n=0;n<t.length;++n){var a=t[n].rp(/#| /g,"");r+=z("a",a,protect('href="#citation-'+a.toLowerCase()+'"')),n<t.length-1&&(r+=", ")}return r+"]"}),protect)).rp(/^\[([^\^#].*?)\]:(.*?)$/gm,function(e,t,r){return s[t.toLowerCase().trim()]={link:r.trim(),used:!1},""})).rp(/(?:<|(?!<)\b)(\S+@(\S+\.)+?\S{2,}?)(?:$|>|(?=<)|(?=\s)(?!>))/g,function(e,t){return"<a "+protect('href="mailto:'+t+'"')+">"+t+"</a>"});function v(e,t,r){var n,a;if(r=r||"",/\.(mp4|m4v|avi|mpg|mov|webm)$/i.test(t))n="<video "+protect('class="markdeep" src="'+t+'"'+r+' width="480px" controls="true"')+"/>";else if(/\.(mp3|mp2|ogg|wav|m4a|aac|flac)$/i.test(t))n="<audio "+protect('class="markdeep" controls '+r+'><source src="'+t+'"')+"></audio>";else if(a=t.match(/^https:\/\/(?:www\.)?(?:youtube\.com\/\S*?v=|youtu\.be\/)([\w\d-]+)(&.*)?$/i))n="<iframe "+protect('class="markdeep" src="https://www.youtube.com/embed/'+a[1]+'"'+r+' width="480px" height="300px" frameborder="0" allowfullscreen webkitallowfullscreen mozallowfullscreen')+"></iframe>";else if(a=t.match(/^https:\/\/(?:www\.)?vimeo.com\/\S*?\/([\w\d-]+)$/i))n="<iframe "+protect('class="markdeep" src="https://player.vimeo.com/video/'+a[1]+'"'+r+' width="480px" height="300px" frameborder="0" allowfullscreen webkitallowfullscreen mozallowfullscreen')+"></iframe>";else{var i="markdeep";r=(r=r.rp(/class *= *(["'])([^'"]+)\1/,function(e,t,r){return i+=" "+r,""})).rp(/class *= *([^"' ]+)/,function(e,t){return i+=" "+t,""}),n=z("a",n="<img "+protect('class="'+i+'" src="'+t+'"'+r)+" />",protect('href="'+t+'" target="_blank"'))}return n}a=(a=(a=(a=(a=(a=a.rp(/\b(equation|eqn\.|eq\.)\s*\[([^\s\]]+)\]/gi,function(e,t,r){return t+" \\ref{"+r+"}"})).rp(/\b(figure|fig\.|table|tbl\.|listing|lst\.)\s*\[([^\s\]]+)\](?=\()/gi,function(e){return e+"<span/>"})).rp(/\(http:\/\/g.gravizo.com\/(.*g)\?((?:[^\(\)]|\([^\(\)]*\))*)\)/gi,function(e,t,r){return"(http://g.gravizo.com/"+t+"?"+encodeURIComponent(r)+")"})).rp(/(^|[^!])\[([^\[\]]+?)\]\(("?)([^<>\s"]+?)\3(\s+[^\)]*?)?\)/g,function(e,t,r,n,a,i){return t+"<a "+protect('href="'+a+'"'+(i=i||""))+">"+r+"</a>"+I(a)})).rp(/(^|[^!])\[[ \t]*?\]\(("?)([^<>\s"]+?)\2\)/g,function(e,t,r,n){return t+"<a "+protect('href="'+n+'"')+">"+n+"</a>"})).rp(/(^|[^!])\[([^\[\]]+)\]\[([^\[\]]*)\]/g,function(e,t,r,n){n.trim()||(n=r),n=n.toLowerCase().trim();var a=s[n];return a?(a.used=!0,t+"<a "+protect('href="'+a.link+'"')+">"+r+"</a>"):(console.log("Reference link '"+n+"' never defined"),"?")});var x=[];a=(a=a.rp(/!\[([^\n\]].*?\n?.*?\n?.*?\n?.*?\n?.*?)\]([\[\(])/g,function(e,t,r){for(var n=(x.push(t)-1).toString(o);n.length<c;)n="0"+n;return"!["+n+"]"+r})).rp(/(!\[.*?\])\[([^<>\[\]\s]+?)([ \t][^\n\[\]]*?)?\]/g,function(e,t,r,n){r=r.toLowerCase().trim();var a=s[r];return a?(a.used=!0,t+"("+a.link+(a.attribs||"")+")"):(console.log("Reference image '"+r+"' never defined"),"?")});var _=protect('width="100%"'),w=protect('valign="top"');a=(a=a.rp(/(?:\n(?:[ \t]*!\[.*?\]\(("?)[^<>\s]+?(?:[^\n\)]*?)?\)){2,}[ \t]*)+(?:\n(?:[ \t]*!\[.*?\]\(("?)[^<>\s]+?(?:[^\n\)]*?)?\))[ \t]*)?\n/g,function(e){var t="";return(e=e.split("\n")).forEach(function(e){(e=e.trim())&&(t+=z("tr",e.rp(/[ \t]*!\[.*?\]\([^\)\s]+([^\)]*?)?\)/g,function(e,t){return z("td","\n\n"+e+"\n\n")}),w))}),"\n"+z("table",t,_)+"\n"})).rp(/(\s*)!\[\]\(("?)([^"<>\s]+?)\2(\s[^\)]*?)?\)(\s*)/g,function(e,t,r,n,a,i){var s=v(0,n,a);return Y(t,i)&&(s=z("center",s)),t+s+i});for(var k=!0,C=U("captionAbove","image");k;)k=!1,a=a.rp(/(\s*)!\[(.+?)\]\(("?)([^"<>\s]+?)\3(\s[^\)]*?)?\)(\s*)/,function(e,t,r,n,a,i,s){k=!0;var o="",l=Y(t,s);i&&!l&&(i=(i=i.rp(/((?:max-)?width)\s*:\s*[^;'"]*/g,function(e,t){return o=e+";",t+":100%"})).rp(/((?:max-)?width)\s*=\s*('\S+?'|"\S+?")/g,function(e,t,r){return o=t+":"+r.ss(1,r.length-1)+";",'style="'+t+':100%" '}));var c=v(0,a,i);l?(t+="<center>",s="</center>"+s):o+="float:right;margin:4px 0px 0px 25px;";return r=z("center",z("span",r+I(a),protect('class="imagecaption"'))),t+z("div",(C?r:"")+c+(C?"":r),protect('class="image" style="'+o+'"'))+s});var N=!1,M=RegExp("[0-9a-w]{"+c+","+c+"}","g");function A(e){var t=parseInt(e.ss(1,e.length-1),o);return N=!0,x[t]}for(N=!0;a.indexOf("")+1&&N;)N=!1,a=a.rp(M,A);a=X(a,/\*\*/,"strong",protect('class="asterisk"')),a=X(a,/__/,"strong",protect('class="underscore"')),a=X(a,/\*/,"em",protect('class="asterisk"')),a=(a=X(a,/_/,"em",protect('class="underscore"'))).rp(/\~\~([^~].*?)\~\~/g,z("del","$1")),U("smartQuotes")&&(a=(a=a.rp(/(^|[ \t->])(")(?=\w)/gm,"$1"+q("&ldquo;"))).rp(/([A-Za-z\.,:;\?!=<])(")(?=$|\W)/gm,"$1"+q("&rdquo;")));var E=(a=(a=(a=(a=(a=function(e,protect){e=(e=e.rp(/^(\s*)(?:-\s*)?(?:\[ \]|\u2610)(\s+)/gm,"$1☐$2")).rp(/^(\s*)(?:-\s*)?(?:\[x\]|\u2611)(\s+)/gm,"$1☑$2");for(var t=new RegExp("("+/[:,]\s*\n/.source+"|"+/\n\s*\n/.source+"|<p>s*\n|<br/>s*\n?)"+/((?:[ \t]*(?:\d+\.|-|\+|\*|\u2611|\u2610)(?:[ \t]+.+\n(?:[ \t]*\n)?)+)+)/.source,"gm"),u=!0,p={"+":protect('class="plus"'),"-":protect('class="minus"'),"*":protect('class="asterisk"'),"☑":protect('class="checked"'),"☐":protect('class="unchecked"')},m=protect('class="number"');u;)u=!1,e=e.rp(t,function(e,t,r){var l=t,c=[],d={indentLevel:-1};for(r.split("\n").forEach(function(e){var t=e.rp(/^\s*/,""),r=e.length-t.length,n=p[t[0]],a=!!n;n=n||m;var i=/^\d+\.[ \t]/.test(t),s=""===t,o=i?" "+protect("start="+t.match(/^\d+/)[0]):"";if((i||a)&&(r+=2),d)if(i||a||!(s||d.indentLevel<=r)){if(r!==d.indentLevel)if(-1!==d.indentLevel&&r<d.indentLevel)for(;d&&r<d.indentLevel;)c.pop(),l+="\n</li></"+d.tag+">",d=c[c.length-1];else d={indentLevel:r,tag:i?"ol":"ul",indentChars:e.ss(0,r-2)},c.push(d),l+="\n<"+d.tag+o+">";else-1!==d.indentLevel&&(l+="\n</li>");d?l+="\n"+d.indentChars+"<li "+n+">"+t.rp(/^(\d+\.|-|\+|\*|\u2611|\u2610) /,""):(l+="\n"+e,u=!0)}else l+="\n"+d.indentChars+e;else l+="\n"+e}),l=l.replace(/\s+$/,""),d=c.pop();d;d=c.pop())l+="</li></"+d.tag+">";return l+"\n\n"});return e}(a=function(e,protect){return e=e.rp(new RegExp("("+/^.+\n:(?=[ \t])/.source+"(s*\n|[: \t].+\n)+)+","gm"),function(e){var r=[],n=null;e.split("\n").forEach(function(e,t){0===e.trim().length?n&&(n.definition+="\n"):/\s/.test(e[0])||":"===e[0]?(":"===e[0]&&(e=" "+e.ss(1)),n.definition+=e+"\n"):r.push(n={term:e,definition:""})});var t=0;r.forEach(function(e){t=/\n\s*\n/.test(e.definition.trim())?1/0:Z(t,$(R(e.definition)).length)});var a="",i=U("definitionStyle");if("short"===i||"long"!==i&&t<160){var s=protect("valign=top");r.forEach(function(e){a+=z("tr",z("td",z("dt",e.term))+z("td",z("dd",z("p",e.definition))),s)}),a=z("table",a)}else r.forEach(function(e){a+=z("dt",e.term)+z("dd",z("p",e.definition))});return z("dl",a)})}(a=function(e,protect){var t="("+/^(?:[^\|<>\s-\+\*\d].*[12]\d{3}(?!\d).*?|(?:[12]\d{3}(?!\.).*\d.*?)|(?:\d{1,3}(?!\.).*[12]\d{3}(?!\d).*?))/.source+"):"+/[ \t]+([^ \t\n].*)\n/.source+/(?:[ \t]*\n)?((?:[ \t]+.+\n(?:[ \t]*\n){0,3})*)/.source,_=new RegExp(t,"gm"),w=protect('valign="top"'),k=protect('style="width:100px;padding-right:15px" rowspan="2"'),C=protect('style="padding-bottom:25px"'),N=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"].map(q),M=["jan","feb","mar","apr","may","jun","jul","aug","sep","oct","nov","dec"].map(q),A=M.join("|"),E=["January","February","March","April","May","June","July","August","September","October","November","December"].map(q);try{var S=0;e=e.rp(new RegExp("\n[ \t]*\n("+t+"){2,}","gm"),function(t){++S;var p=[],m=!1;t.rp(_,function(e,t,r,n){var a="",i="",s="",o=!1;"("===(t=t.trim())[0]&&")"===t.slice(-1)&&(t=t.slice(1,-1),o=!0);var l=t.match(RegExp("([0123]?\\d)\\D+([01]?\\d|"+A+")\\D+([12]\\d{3})","i"));if(l)s=l[1],i=l[2],a=l[3];else if(l=t.match(RegExp("([12]\\d{3})\\D+([01]?\\d|"+A+")\\D+([0123]?\\d)","i")))s=l[3],i=l[2],a=l[1];else{if(!(l=t.match(RegExp("("+A+")\\D+([0123]?\\d)\\D+([12]\\d{3})","i"))))throw"Could not parse date";s=l[2],i=l[1],a=l[3]}t=s+" "+q(i)+" "+a;var c=parseInt(i)-1;isNaN(c)&&(c=M.indexOf(i.toLowerCase()));var d=new Date(Date.UTC(parseInt(a),c,parseInt(s),9)),u=d.getUTCDay();return t=N[u]+"<br/>"+t,m=m||0===u||6===u,p.push({date:d,title:r,sourceOrder:p.length,parenthesized:o,text:o?"":z("tr",z("td","<a "+protect('class="target" name="schedule'+S+"_"+d.getUTCFullYear()+"-"+(d.getUTCMonth()+1)+"-"+d.getUTCDate()+'"')+">&nbsp;</a>"+t,k)+z("td",z("b",r)),w)+z("tr",z("td","\n\n"+n,C),w)}),""});var e=U("sortScheduleLists")?p:p.slice(0);p.sort(function(e,t){var r=e.date.getTime(),n=t.date.getTime();return r===n?e.sourceOrder-t.sourceOrder:r-n});var r=864e5,n=(p[p.length-1].date.getTime()-p[0].date.getTime())/r,a=new Date;a=new Date(Date.UTC(a.getUTCFullYear(),a.getUTCMonth(),a.getUTCDate(),9));var i="";if(14<n&&n/p.length<16){var s=protect('colspan="2" width="14%" style="padding-top:5px;text-align:center;font-style:italic"'),o=protect('width="1%" height="30px" style="text-align:right;border:1px solid #EEE;border-right:none;"'),l=protect('width="1%" height="30px" style="color:#BBB;text-align:right;"'),c=protect('width="14%" style="border:1px solid #EEE;border-left:none;"'),d=protect('class="parenthesized"'),u=p[0].date,g=0,b=!m&&U("hideEmptyWeekends"),f=b?function(e){return 0<e.getUTCDay()&&e.getUTCDay()<6}:function(){return!0},h=function(e,t){return G(e.getTime()-t.getTime())<r/2};for(u=new Date(u.getUTCFullYear(),u.getUTCMonth(),1,9);u.getTime()<p[p.length-1].date.getTime();){for(i+="<table "+protect('class="calendar"')+">\n"+z("tr",z("th",E[u.getUTCMonth()]+" "+u.getUTCFullYear(),protect('colspan="14"')))+"<tr>",(b?N.slice(1,6):N).forEach(function(e){i+=z("td",e,s)}),i+="</tr>";0!==u.getUTCDay();)u=new Date(u.getTime()-r);if(1!==u.getDate())for(i+="<tr "+w+">";1!==u.getDate();)f(u)&&(i+="<td "+l+">"+u.getUTCDate()+"</td><td>&nbsp;</td>"),u=new Date(u.getTime()+r);do{if(0===u.getUTCDay()&&(i+="<tr "+w+">"),f(u)){var y="";h(u,a)&&(y=protect('class="today"'));for(var v="",x=p[g];x&&h(x.date,u);x=p[++g])v&&(v+="<br/>"),v+=x.parenthesized?z("span",x.title,d):z("a",x.title,protect('href="#schedule'+S+"_"+u.getUTCFullYear()+"-"+(u.getUTCMonth()+1)+"-"+u.getUTCDate()+'"'));i+=v?z("td",z("b",u.getUTCDate()),o+y)+z("td",v,c+y):"<td "+o+y+"></a>"+u.getUTCDate()+"</td><td "+c+y+"> &nbsp; </td>"}6===u.getUTCDay()&&(i+="</tr>"),u=new Date(u.getTime()+r)}while(1<u.getUTCDate());if(0!==u.getUTCDay()){for(;0!==u.getUTCDay();)f(u)&&(i+="<td "+l+">"+u.getUTCDate()+"</td><td>&nbsp</td>"),u=new Date(u.getTime()+r);i+="</tr>"}i+="</table><br/>\n",u=new Date(Date.UTC(u.getUTCFullYear(),u.getUTCMonth(),1,9))}}return t="",e.forEach(function(e){t+=e.text}),"\n\n"+i+z("table",t,protect('class="schedule"'))+"\n\n"})}catch(e){}return e}(a=(a=(a=(a=(a=(a=(a=(a=(a=(a=(a=(a=(a=(a=(a=a.rp(/(\s|^)<==(\s)/g,"$1⇐$2")).rp(/(\s|^)->(\s)/g,"$1&rarr;$2")).rp(/(\s|^)-->(\s)/g,"$1&xrarr;$2")).rp(/(\s|^)==>(\s)/g,"$1⇒$2")).rp(/(\s|^)<-(\s)/g,"$1&larr;$2")).rp(/(\s|^)<--(\s)/g,"$1&xlarr;$2")).rp(/(\s|^)<==>(\s)/g,"$1⇔$2")).rp(/(\s|^)<->(\s)/g,"$1↔$2")).rp(/([^-!\:\|])---([^->\:\|])/g,"$1&mdash;$2")).rp(/([^-!\:\|])--([^->\:\|])/g,"$1&mdash;$2")).rp(/(\d+\s?)x(?=\s?\d+)/g,"$1&times;")).rp(/([\s\(\[<\|])-(\d)/g,"$1&minus;$2")).rp(/(\d) - (\d)/g,"$1 &minus; $2")).rp(/\^([-+]?\d+)\b/g,"<sup>$1</sup>")).rp(/(^|\s|\b)\\(pagebreak|newpage)(\b|\s|$)/gi,protect('<div style="page-break-after:always"> </div>\n')),protect),protect),protect)).rp(/(\d+?)[ \t-]degree(?:s?)/g,"$1&deg;")).rp(/(?:<p>)?\n\s*\n+(?!<\/p>)/gi,function(e){return/^<p>/i.test(e)?e:"\n\n</p><p>\n\n"})).rp(/<p>[\s\n]*<\/p>/gi,"")).rp(/\n\[\^(\S+)\]: ((?:.+?\n?)*)/g,function(e,t,r){return(t=t.toLowerCase().trim())in n?"\n<div "+protect('class="endnote"')+"><a "+protect('class="target" name="endnote-'+t+'"')+">&nbsp;</a><sup>"+n[t]+"</sup> "+r+"</div>":"\n"})).match(/<h([1-6])>(.*?)<\/h\1>/gi);E&&E.forEach(function(e){var t="<a "+protect('href="#'+O(e=R(e.ss(4,e.length-5)).trim())+'"')+">",r="("+q("section")+"|"+q("subsection")+"|"+q("chapter")+")",n="(\\b"+e.rp(/([\.\[\]\(\)\*\+\?\^\$\\\{\}\|])/g,"\\$1")+")";a=(a=a.rp(RegExp(n+"\\s+"+r,"gi"),t+"$1</a> $2")).rp(RegExp(r+"\\s+"+n,"gi"),"$1 "+t+"$2</a>")});var S={},T={};a=(a=(a=a.rp(RegExp(/($|>)\s*/.source+"("+q("figure")+"|"+q("table")+"|"+q("listing")+"|"+q("diagram")+")"+/\s+\[(.+?)\]:/.source,"gim"),function(e,t,r,n){var a=r.toLowerCase(),i=S[a]=1+(0|S[a]),s=a+"_"+O(n.toLowerCase().trim());return T[s]={number:i,used:!1,source:a+" ["+n+"]"},t+z("a","&nbsp;",protect('class="target" name="'+s+'"'))+z("b",a[0].toUpperCase()+a.ss(1)+"&nbsp;"+i+":",protect('style="font-style:normal;"'))+I(n)})).rp(RegExp("\\b(fig\\.|tbl\\.|lst\\.|"+q("figure")+"|"+q("table")+"|"+q("listing")+"|"+q("diagram")+")\\s+\\[([^\\s\\]]+)\\]","gi"),function(e,t,r){var n=t.toLowerCase();switch(n){case"fig.":n=q("figure").toLowerCase();break;case"tbl.":n=q("table").toLowerCase();break;case"lst.":n=q("listing").toLowerCase()}var a=n+"_"+O(r.toLowerCase().trim()),i=T[a];return i?(i.used=!0,"<a "+protect('href="#'+a+'"')+">"+t+"&nbsp;"+i.number+I(r)+"</a>"):(console.log("Reference to undefined '"+n+" ["+r+"]'"),t+" ?")})).rp(/(?:<|(?!<)\b)(\w{3,6}:\/\/.+?)(?:$|>|(?=<)|(?=\s|\u00A0)(?!<))/g,function(e,t){var r="";return"."==t[t.length-1]&&(t=t.ss(0,t.length-1),r="."),"<a "+("s"!==t[0]&&"p"!==t[0]?protect('href="'+t+'" class="url"'):"")+">"+t+"</a>"+r}),e||(a=a.rp(new RegExp(/^\s*(?:<\/p><p>)?\s*<strong.*?>([^ \t\*].*?[^ \t\*])<\/strong>(?:<\/p>)?[ \t]*\n/.source+/([ {4,}\t][ \t]*\S.*\n)*/.source,"g"),function(e,t){t=t.trim();var r=e.ss(e.indexOf("\n",e.indexOf("</strong>")));return r=r?r.rp(/[ \t]*(\S.*?)\n/g,'<div class="subtitle"> $1 </div>\n'):"",z("title",t=R(t).replace(/[     ]/g,"").replace(/[          ]/g," "))+I(window.location.href,"center")+'<div class="title"> '+t+" </div>\n"+r+'<div class="afterTitles"></div>\n'}));if(a=a.rp(/^\s*<\/p>/,""),!e){var L=function(e,protect,l){var c='<a href="#" class="tocTop">(Top)</a><br/>\n',d="",u=[],p=[0],m=0,g=0,b={};e=e.rp(/<h([1-6])>(.*?)<\/h\1>/gi,function(e,t,r){t=parseInt(t),r=r.trim();for(var n=m;n<t;++n)u[n]="",p[n]=0;p.splice(t,m-t),u.splice(t,m-t),++p[(m=t)-1];var a=p.join("."),i="toc"+a,s=R(l(r)).trim().toLowerCase();b[s]=a,r=r.rp(/<a\s.*>(.*?)<\/a>/g,"$1"),u[m-1]=O(s);var o=u.join("/");return t<=3&&(c+=Array(t).join("&nbsp;&nbsp;")+'<a href="#'+o+'" class="level'+t+'"><span class="tocNumber">'+a+"&nbsp; </span>"+r+"</a><br/>\n",1===t?d+=' &middot; <a href="#'+o+'">'+r+"</a>":++g),z("a","&nbsp;",protect('class="target" name="'+o+'"'))+z("a","&nbsp;",protect('class="target" name="'+i+'"'))+e}),0<d.length&&(d=d.ss(10));var t=p[0],r=t+g,n=e.regexIndexOf(/((<a\s+\S+>&nbsp;<\/a>)\s*)*?<h\d>/i);-1===n&&(n=0);var a='<div class="afterTitles"></div>',i=e.indexOf(a);-1===i?i=0:i+=a.length;var s=U("tocStyle"),o="";switch("auto"!==s&&""!==s||(s=r<4&&t<=1||e.length<2048?"none":t<7&&r/t<2.5?"short":-1===n||r<n/55?"medium":"long"),s){case"none":case"":break;case"short":o='<div class="shortTOC">'+d+"</div>";break;case"medium":o='<div class="mediumTOC"><center><b>'+q("Contents")+"</b></center><p>"+c+"</p></div>";break;case"long":i=n,o='<div class="longTOC"><div class="tocHeader">'+q("Contents")+"</div><p>"+c+"</p></div>";break;default:console.log('markdeepOptions.tocStyle = "'+s+'" specified in your document is not a legal value')}return[e=e.ss(0,i)+o+e.ss(i),b]}(a,protect,function(e){return e.rp(d,p)}),j=L[1];a=(a=L[0]).rp(RegExp("\\b("+q("sec")+"\\.|"+q("section")+"|"+q("subsection")+"|"+q("chapter")+")\\s\\[(.+?)\\]","gi"),function(e,t,r){var n=j[r.toLowerCase().trim()];return n?t+" <a "+protect('href="#toc'+n+'"')+">"+n+"</a>":t+" ?"})}var B=50;for(u=!0;a.indexOf(r)+1&&u&&0<B;)u=!1,a=a.rp(d,p),--B;if(B<=0&&console.log("WARNING: Ran out of iterations while expanding protected substrings"),Object.keys(s).forEach(function(e){s[e].used||console.log("Reference link '["+e+"]' is defined but never used")}),Object.keys(T).forEach(function(e){T[e].used||console.log("'"+T[e].source+"' is never referenced")}),U("linkAPIDefinitions")){var D={};a=(a=(a=a.rp(/<dt><code>([A-Za-z_][A-Za-z_\.0-9:\->]*)([\(\[<])/g,function(e,t,r){var n=t+("<"===r?"":"("===r?"-fcn":"["===r?"-array":r);return D[n]=!0,'<dt><a name="apiDefinition-'+n+'"></a><code >'+t+r})).rp(/<h([1-9])>(.*<code>.*)<\/code>(.*<\/h\1>)/g,"<h$1>$2</code >$3")).rp(/<code>([A-Za-z_][A-Za-z_\.0-9:\->]*)(\(\)|\[\])?<\/code>/g,function(e,t,r){var n=t+(r?"("===r[0]?"-fcn":"["===r[0]?"-array":r[0]:"");return!0===D[n]?z("a",e,'href="#apiDefinition-'+n+'"'):e})}return'<span class="md">'+z("p",a)+"</span>"}function ee(e){if(Array.from)return Array.from(e);for(var t=[],r=0;r<e.length;++r)t[r]=e[r];return t}function u(e){var t=e.split("\n"),r=1/0;if(t.forEach(function(e){if(""!==e.trim()){var t=e.match(/^([ \t]*)/);t&&(r=J(r,t[0].length))}}),0===r)return e;var n="";return t.forEach(function(e){n+=e.ss(r)+"\n"}),n}function te(e){var t=e.charCodeAt(0);return 65<=t&&t<=90||97<=t&&t<=122}function re(e,t){e=(e=(e=(e=function(e){var t=e.split("\n");0<t.length&&""===t[t.length-1]&&t.pop();var r=0;t.forEach(function(e){r=Z(r,ee(e).length)});var n=Array(r+1).join(" "),a="";return t.forEach(function(e){a+=e+n.ss(ee(e).length)+"\n"}),a}(e)).rp(/([a-zA-Z]{2})o/g,"$1")).rp(/o([a-zA-Z]{2})/g,"$1")).rp(/([a-zA-Z\ue004])o([a-zA-Z\ue004])/g,"$1$2");var h=180*Math.atan(.5)/Math.PI,r=">v<^",n="o*◌○◍●",a="()",i="+",s=i+".'",p="░▒▓█",m="◢◣◤◥",o=r+n+a+p+m;function l(e){return i.indexOf(e)+1}function w(e){return-1!==s.indexOf(e)}function k(e){return l(e)||"."===e}function C(e){return l(e)||"'"===e}function c(e){return w(e)||"<"===e||S(e)}function d(e){return w(e)||">"===e||S(e)}function y(e){return p.indexOf(e)+1}function v(e){return m.indexOf(e)+1}function N(e){return"-"===e||l(e)||E(e)}function M(e){return A(e)||E(e)||S(e)}function A(e){return"|"===e||l(e)}function u(e){return"/"===e||l(e)}function g(e){return"\\"===e||l(e)}function E(e){return a.indexOf(e)+1}function S(e){return n.indexOf(e)+1}function T(e,t){if(!(this instanceof T))return new T(e,t);void 0===t&&(void 0===e?e=t=0:e instanceof T?(t=e.y,e=e.x):console.error("Vec2 requires one Vec2 or (x, y) as an argument")),this.x=e,this.y=t,Object.seal(this)}function L(e,t,r,n,a){e instanceof T&&t instanceof T||console.error("Path constructor requires at least two Vec2s"),this.A=e,this.B=t,r&&(this.C=r,this.D=n||r),this.dashed=a||!1,Object.freeze(this)}T.prototype.toString=T.prototype.toSVG=function(){return 8*this.x+","+8*this.y*2+" "};var b=L.prototype;function f(){this._pathArray=[]}b.isVertical=function(){return this.B.x===this.A.x},b.isHorizontal=function(){return this.B.y===this.A.y},b.isDiagonal=function(){return G(this.B.y-this.A.y+(this.B.x-this.A.x))<1e-6},b.isBackDiagonal=function(){return G(this.B.y-this.A.y-(this.B.x-this.A.x))<1e-6},b.isCurved=function(){return void 0!==this.C},b.endsAt=function(e,t){return void 0===t&&(t=e.y,e=e.x),this.A.x===e&&this.A.y===t||this.B.x===e&&this.B.y===t},b.upEndsAt=function(e,t){return void 0===t&&(t=e.y,e=e.x),this.isVertical()&&this.A.x===e&&J(this.A.y,this.B.y)===t},b.diagonalUpEndsAt=function(e,t){return!!this.isDiagonal()&&(void 0===t&&(t=e.y,e=e.x),this.A.y<this.B.y?this.A.x===e&&this.A.y===t:this.B.x===e&&this.B.y===t)},b.diagonalDownEndsAt=function(e,t){return!!this.isDiagonal()&&(void 0===t&&(t=e.y,e=e.x),this.B.y<this.A.y?this.A.x===e&&this.A.y===t:this.B.x===e&&this.B.y===t)},b.backDiagonalUpEndsAt=function(e,t){return!!this.isBackDiagonal()&&(void 0===t&&(t=e.y,e=e.x),this.A.y<this.B.y?this.A.x===e&&this.A.y===t:this.B.x===e&&this.B.y===t)},b.backDiagonalDownEndsAt=function(e,t){return!!this.isBackDiagonal()&&(void 0===t&&(t=e.y,e=e.x),this.B.y<this.A.y?this.A.x===e&&this.A.y===t:this.B.x===e&&this.B.y===t)},b.downEndsAt=function(e,t){return void 0===t&&(t=e.y,e=e.x),this.isVertical()&&this.A.x===e&&Z(this.A.y,this.B.y)===t},b.leftEndsAt=function(e,t){return void 0===t&&(t=e.y,e=e.x),this.isHorizontal()&&this.A.y===t&&J(this.A.x,this.B.x)===e},b.rightEndsAt=function(e,t){return void 0===t&&(t=e.y,e=e.x),this.isHorizontal()&&this.A.y===t&&Z(this.A.x,this.B.x)===e},b.verticalPassesThrough=function(e,t){return void 0===t&&(t=e.y,e=e.x),this.isVertical()&&this.A.x===e&&J(this.A.y,this.B.y)<=t&&Z(this.A.y,this.B.y)>=t},b.horizontalPassesThrough=function(e,t){return void 0===t&&(t=e.y,e=e.x),this.isHorizontal()&&this.A.y===t&&J(this.A.x,this.B.x)<=e&&Z(this.A.x,this.B.x)>=e},b.toSVG=function(){var e='<path d="M '+this.A;return this.isCurved()?e+="C "+this.C+this.D+this.B:e+="L "+this.B,e+='" style="fill:none;"',this.dashed&&(e+=' stroke-dasharray="3,6"'),e+="/>"};var x=f.prototype;function _(n){return function(e,t){for(var r=0;r<this._pathArray.length;++r)if(n.call(this._pathArray[r],e,t))return!0}}function j(){this._decorationArray=[]}x.insert=function(e){this._pathArray.push(e)},x.upEndsAt=_(b.upEndsAt),x.diagonalUpEndsAt=_(b.diagonalUpEndsAt),x.backDiagonalUpEndsAt=_(b.backDiagonalUpEndsAt),x.diagonalDownEndsAt=_(b.diagonalDownEndsAt),x.backDiagonalDownEndsAt=_(b.backDiagonalDownEndsAt),x.downEndsAt=_(b.downEndsAt),x.leftEndsAt=_(b.leftEndsAt),x.rightEndsAt=_(b.rightEndsAt),x.endsAt=_(b.endsAt),x.verticalPassesThrough=_(b.verticalPassesThrough),x.horizontalPassesThrough=_(b.horizontalPassesThrough),x.toSVG=function(){for(var e="",t=0;t<this._pathArray.length;++t)e+=this._pathArray[t].toSVG()+"\n";return e};var B=j.prototype;B.insert=function(e,t,r,n){void 0===r&&(r=t,t=e.y,e=e.x),o.indexOf(r)+1||console.error("Illegal decoration character: "+r);var a={C:T(e,t),type:r,angle:n||0};S(r)?this._decorationArray.push(a):this._decorationArray.unshift(a)},B.toSVG=function(){for(var e="",t=0;t<this._decorationArray.length;++t){var r=this._decorationArray[t],n=r.C;if(E(r.type)){var a=")"===r.type?.75:-.75,i=T(n.x,n.y-.5),s=T(n.x,n.y+.5),o=T(n.x+a,n.y-.5);e+='<path d="M '+s+" C "+T(n.x+a,n.y+.5)+o+i+'" style="fill:none;"/>'}else if(S(r.type)){e+='<circle cx="'+8*n.x+'" cy="'+8*n.y*2+'" r="'+(8-W)+'" class="'+{"*":"closed",o:"open","◌":"dotted","○":"open","◍":"shaded","●":"closed"}[r.type]+'dot"/>'}else if(y(r.type)){var l=Math.round(63.75*(3-p.indexOf(r.type)));e+='<rect x="'+8*(n.x-.5)+'" y="'+8*(n.y-.5)*2+'" width="8" height="16" stroke=none fill="rgb('+l+","+l+","+l+')"/>'}else if(v(r.type)){var c=m.indexOf(r.type),d=.5-(1&c),u=.5-(c>>1);d*=K(u),e+='<polygon points="'+T(n.x+d,n.y-u)+(i=T(n.x+d,n.y+u))+(s=T(n.x-d,n.y+u))+'" style="stroke:none"/>\n'}else{e+='<polygon points="'+T(n.x+1,n.y)+(i=T(n.x-.5,n.y-.35))+(s=T(n.x-.5,n.y+.35))+'" style="stroke:none" transform="rotate('+r.angle+","+n+')"/>\n'}}return e};var D,z,U=(D=e,(z=function(e,t){return void 0===t&&(e instanceof T?(t=e.y,e=e.x):console.error("grid requires either a Vec2 or (x, y)")),0<=e&&e<z.width&&0<=t&&t<z.height?D[t*(z.width+1)+e]:" "})._used=[],z.height=D.split("\n").length,"\n"===D[D.length-1]&&--z.height,D=ee(D),z.width=D.indexOf("\n"),z.setUsed=function(e,t){void 0===t&&(e instanceof T?(t=e.y,e=e.x):console.error("grid requires either a Vec2 or (x, y)")),0<=e&&e<z.width&&0<=t&&t<z.height&&(z._used[t*(z.width+1)+e]=!0)},z.isUsed=function(e,t){return void 0===t&&(e instanceof T?(t=e.y,e=e.x):console.error("grid requires either a Vec2 or (x, y)")),!0===this._used[t*(this.width+1)+e]},z.isSolidVLineAt=function(e,t){void 0===t&&(e=t=e.x);var r=z(e,t-1),n=z(e,t),a=z(e,t+1),i=z(e+1,t-1),s=z(e-1,t-1);return A(n)?k(r)||"^"===r||A(r)||E(r)||C(a)||"v"===a||A(a)||E(a)||S(r)||S(a)||"_"===z(e,t-1)||"_"===s||"_"===i||(k(s)||k(i))&&(C(z(e-1,t+1))||C(z(e+1,t+1))):k(n)||"^"===n?A(a)||E(a)&&"."!==n:C(n)||"v"===n?A(r)||E(r)&&"'"!==n:!!S(n)&&(A(r)||A(a))},z.isSolidHLineAt=function(e,t){void 0===t&&(e=t=e.x);var r=z(e-2,t),n=z(e-1,t),a=z(e+0,t),i=z(e+1,t),s=z(e+2,t);return N(a)||N(n)&&E(a)?N(n)?N(i)||d(i)||N(r)||c(r):c(n)?N(i):N(i)&&(N(s)||d(s)):"<"===a?N(i)&&N(s):">"===a?N(n)&&N(r):!!w(a)&&(N(n)&&N(r)||N(i)&&N(s))},z.isSolidBLineAt=function(e,t){void 0===t&&(e=t=e.x);var r=z(e,t),n=z(e-1,t-1),a=z(e+1,t+1);return"\\"===r?g(a)||C(a)||S(a)||"v"===a||g(n)||k(n)||S(n)||"^"===n||"/"===z(e,t-1)||"/"===z(e,t+1)||"_"===a||"_"===n:"."===r?"\\"===a:"'"===r?"\\"===n:"^"===r?"\\"===a:"v"===r?"\\"===n:w(r)||S(r)||"|"===r?g(n)||g(a):void 0},z.isSolidDLineAt=function(e,t){void 0===t&&(e=t=e.x);var r=z(e,t),n=z(e-1,t+1),a=z(e+1,t-1);return"/"===r&&("\\"===z(e,t-1)||"\\"===z(e,t+1))||(u(r)?u(a)||k(a)||S(a)||"^"===a||"_"===a||u(n)||C(n)||S(n)||"v"===n||"_"===n:"."===r?"/"===n:"'"===r?"/"===a:"^"===r?"/"===n:"v"===r?"/"===a:!(!w(r)&&!S(r)&&"|"!==r)&&(u(n)||u(a)))},z.toString=function(){return D},Object.freeze(z)),I=new f,q=new j;!function(o,e){function t(e,t,r){var n,a,i=K(t.x-e.x),s=K(t.y-e.y);for(n=e.x,a=e.y;n!==t.x||a!==t.y;n+=i,a+=s)if(o(n,a)===r)return 1;return o(n,a)===r}for(var r=0;r<o.width;++r)for(var n=0;n<o.height;++n)if(o.isSolidVLineAt(r,n)){for(var a=T(r,n);o.setUsed(r,n),o.isSolidVLineAt(r,++n););var i=T(r,n-1),s=o(a),l=o(a.x,a.y-1);(w(s)||"-"!==l&&"_"!==l&&"┳"!==l&&"_"!==o(a.x-1,a.y-1)&&"_"!==o(a.x+1,a.y-1)&&!C(l))&&!E(l)||(a.y-=.5);var c=o(i),d=o(i.x,i.y+1);(w(c)||"-"!==d&&"┻"!==d&&!k(d))&&!E(d)&&"_"!==o(i.x-1,i.y)&&"_"!==o(i.x+1,i.y)||(i.y+=.5),a.x===i.x&&a.y===i.y||e.insert(new L(a,i))}else"'"===o(r,n)&&("-"===o(r-1,n)&&"_"===o(r+1,n-1)&&!M(o(r-1,n-1))||"_"===o(r-1,n-1)&&"-"===o(r+1,n)&&!M(o(r+1,n-1)))?e.insert(new L(T(r,n-.5),T(r,n))):"."===o(r,n)&&("_"===o(r-1,n)&&"-"===o(r+1,n)&&!M(o(r+1,n+1))||"-"===o(r-1,n)&&"_"===o(r+1,n)&&!M(o(r-1,n+1)))?e.insert(new L(T(r,n),T(r,n+.5))):"."===o(r,n)&&"-"===o(r-1,n)&&"╱"===o(r+1,n)?e.insert(new L(T(r,n),T(r+.5,n+.5))):"'"===o(r,n)&&"-"===o(r+1,n)&&"╱"===o(r-1,n)&&e.insert(new L(T(r,n),T(r-.5,n-.5)));for(n=0;n<o.height;++n)for(r=0;r<o.width;++r)if(o.isSolidHLineAt(r,n)){for(a=T(r,n);o.setUsed(r,n),o.isSolidHLineAt(++r,n););i=T(r-1,n);"┫"===o(i.x+1,i.y)&&(i.x+=.5),"┣"===o(a.x-1,a.y)&&(a.x-=.5),!w(o(a.x-1,a.y))&&(k(o(a))&&M(o(a.x-1,a.y+1))||C(o(a))&&M(o(a.x-1,a.y-1)))&&++a.x,!w(o(i.x+1,i.y))&&(k(o(i))&&M(o(i.x+1,i.y+1))||C(o(i))&&M(o(i.x+1,i.y-1)))&&--i.x,a.x===i.x&&a.y===i.y||e.insert(new L(a,i))}for(var u=-o.height;u<o.width;++u)for(r=u,n=0;n<o.height;++n,++r)if(o.isSolidBLineAt(r,n)){for(a=T(r,n);o.isSolidBLineAt(++r,++n););if(t(a,i=T(r-1,n-1),"\\")){for(var p=a.x;p<=i.x;++p)o.setUsed(p,a.y+(p-a.x));var m=o(a),g=(s=o(a.x,a.y-1),o(a.x-1,a.y-1));"/"===s||"_"===g||"_"===s||!w(m)&&(N(g)||A(g))?(a.x-=.5,a.y-=.5):S(g)&&(a.x-=.25,a.y-=.25);o(i);var b=o(i.x+1,i.y+1);"/"===o(i.x,i.y+1)||"_"===o(i.x+1,i.y)||"_"===o(i.x-1,i.y)||!w(o(i))&&(N(b)||A(b))?(i.x+=.5,i.y+=.5):S(b)&&(i.x+=.25,i.y+=.25),e.insert(new L(a,i))}}for(u=-o.height;u<o.width;++u)for(r=u,n=o.height-1;0<=n;--n,++r)if(o.isSolidDLineAt(r,n)){for(a=T(r,n);o.isSolidDLineAt(++r,--n););if(t(a,i=T(r-1,n+1),"/")){for(p=a.x;p<=i.x;++p)o.setUsed(p,a.y-(p-a.x));s=o(i.x,i.y-1);var f=o(i.x+1,i.y-1);o(i);"\\"===s||"_"===s||"_"===f||!w(o(i))&&(N(f)||A(f))?(i.x+=.5,i.y-=.5):S(f)&&(i.x+=.25,i.y-=.25);var h=o(a.x-1,a.y+1);m=o(a);"\\"===o(a.x,a.y+1)||"_"===o(a.x-1,a.y)||"_"===o(a.x+1,a.y)||!w(o(a))&&(N(h)||A(h))?(a.x-=.5,a.y+=.5):S(h)&&(a.x-=.25,a.y+=.25),e.insert(new L(a,i))}}for(n=0;n<o.height;++n)for(r=0;r<o.width;++r){k(x=o(r,n))&&(N(o(r-1,n))&&A(o(r+1,n+1))&&(o.setUsed(r-1,n),o.setUsed(r,n),o.setUsed(r+1,n+1),e.insert(new L(T(r-1,n),T(r+1,n+1),T(r+1.1,n),T(r+1,n+1)))),N(o(r+1,n))&&A(o(r-1,n+1))&&(o.setUsed(r-1,n+1),o.setUsed(r,n),o.setUsed(r+1,n),e.insert(new L(T(r+1,n),T(r-1,n+1),T(r-1.1,n),T(r-1,n+1))))),")"!==x&&!S(x)||"."!==o(r-1,n-1)||"'"!==o(r-1,n+1)||(o.setUsed(r,n),o.setUsed(r-1,n-1),o.setUsed(r-1,n+1),e.insert(new L(T(r-2,n-1),T(r-2,n+1),T(r+.6,n-1),T(r+.6,n+1)))),"("!==x&&!S(x)||"."!==o(r+1,n-1)||"'"!==o(r+1,n+1)||(o.setUsed(r,n),o.setUsed(r+1,n-1),o.setUsed(r+1,n+1),e.insert(new L(T(r+2,n-1),T(r+2,n+1),T(r-.6,n-1),T(r-.6,n+1)))),C(x)&&(N(o(r-1,n))&&A(o(r+1,n-1))&&(o.setUsed(r-1,n),o.setUsed(r,n),o.setUsed(r+1,n-1),e.insert(new L(T(r-1,n),T(r+1,n-1),T(r+1.1,n),T(r+1,n-1)))),N(o(r+1,n))&&A(o(r-1,n-1))&&(o.setUsed(r-1,n-1),o.setUsed(r,n),o.setUsed(r+1,n),e.insert(new L(T(r+1,n),T(r-1,n-1),T(r-1.1,n),T(r-1,n-1)))))}for(n=0;n<o.height;++n)for(r=0;r<o.width-2;++r){var y=o(r-1,n);if(!("_"!==o(r,n)||"_"!==o(r+1,n)||te(o(r+2,n))&&"_"!==y||te(y)&&"_"!==o(r+2,n))){var v=o(r-2,n);a=T(r-.5,n+.5);for("|"===y||"|"===o(r-1,n+1)||"."===y||"'"===o(r-1,n+1)?(a.x-=.5,"."!==y||"-"!==v&&"."!==v||"("!==o(r-2,n+1)||(a.x-=.5)):"/"===y&&--a.x,"("===y&&"("===v&&"'"===o(r,n+1)&&"."===o(r,n-1)&&(a.x+=.5),y=v=void 0;o.setUsed(r,n),"_"===o(++r,n););i=T(r-.5,n+.5);var x=o(r,n),_=o(r+1,n);c=o(r,n+1);"|"===x||"|"===c||"."===x||"'"===c?(i.x+=.5,"."!==x||"-"!==_&&"."!==_||")"!==o(r+1,n+1)||(i.x+=.5)):"\\"===x&&(i.x+=1),")"===x&&")"===_&&"'"===o(r-1,n+1)&&"."===o(r-1,n-1)&&(i.x+=-.5),e.insert(new L(a,i))}}}(U,I),function(e,t){for(var r=0;r<e.width;++r)for(var n=0;n<e.height;++n){if(!e.isUsed(r,n))switch(e(r,n)){case"╱":t.insert(new L(T(r-.5,n+.5),T(r+.5,n-.5))),e.setUsed(r,n);break;case"╲":t.insert(new L(T(r-.5,n-.5),T(r+.5,n+.5))),e.setUsed(r,n)}}}(U,I),function(e,t,r){function n(e){return" "===e||/[^a-zA-Z0-9]|[ov]/.test(e)}for(var a=0;a<e.width;++a)for(var i=0;i<e.height;++i){var s=e(a,i),o=i;if(E(s))t.downEndsAt(a,o-.5)&&t.upEndsAt(a,o+.5)&&(r.insert(a,o,s),e.setUsed(a,o));else if(S(s)){var l=e(a,o-1),c=e(a,o+1),d=e(a-1,o),u=e(a+1,o);e(a-2,o),e(a+2,o);(t.rightEndsAt(a-1,o)||t.leftEndsAt(a+1,o)||t.downEndsAt(a,o-1)||t.upEndsAt(a,o+1)||t.upEndsAt(a,o)||t.downEndsAt(a,o)||(m=l,b=d,f=u,(n(g=c)||S(g))&&(n(m)||S(m))&&n(f)&&n(b)))&&(r.insert(a,o,s),e.setUsed(a,o))}else if(y(s))r.insert(a,o,s),e.setUsed(a,o);else if(v(s))r.insert(a,o,s),e.setUsed(a,o);else{var p=0;">"===s&&(t.rightEndsAt(a,o)||t.horizontalPassesThrough(a,o))?(S(e(a+1,o))&&(p=-.5),r.insert(a+p,o,">",0),e.setUsed(a,o)):"<"===s&&(t.leftEndsAt(a,o)||t.horizontalPassesThrough(a,o))?(S(e(a-1,o))&&(p=.5),r.insert(a+p,o,">",180),e.setUsed(a,o)):"^"===s?t.upEndsAt(a,o-.5)?(r.insert(a,o-.5,">",270),e.setUsed(a,o)):t.upEndsAt(a,o)?(r.insert(a,o,">",270),e.setUsed(a,o)):t.diagonalUpEndsAt(a+.5,o-.5)?(r.insert(a+.5,o-.5,">",270+h),e.setUsed(a,o)):t.diagonalUpEndsAt(a+.25,o-.25)?(r.insert(a+.25,o-.25,">",270+h),e.setUsed(a,o)):t.diagonalUpEndsAt(a,o)?(r.insert(a,o,">",270+h),e.setUsed(a,o)):t.backDiagonalUpEndsAt(a,o)?(r.insert(a,o,s,270-h),e.setUsed(a,o)):t.backDiagonalUpEndsAt(a-.5,o-.5)?(r.insert(a-.5,o-.5,s,270-h),e.setUsed(a,o)):t.backDiagonalUpEndsAt(a-.25,o-.25)?(r.insert(a-.25,o-.25,s,270-h),e.setUsed(a,o)):t.verticalPassesThrough(a,o)&&(r.insert(a,o-.5,">",270),e.setUsed(a,o)):"v"===s&&(t.downEndsAt(a,o+.5)?(r.insert(a,o+.5,">",90),e.setUsed(a,o)):t.downEndsAt(a,o)?(r.insert(a,o,">",90),e.setUsed(a,o)):t.diagonalDownEndsAt(a,o)?(r.insert(a,o,">",90+h),e.setUsed(a,o)):t.diagonalDownEndsAt(a-.5,o+.5)?(r.insert(a-.5,o+.5,">",90+h),e.setUsed(a,o)):t.diagonalDownEndsAt(a-.25,o+.25)?(r.insert(a-.25,o+.25,">",90+h),e.setUsed(a,o)):t.backDiagonalDownEndsAt(a,o)?(r.insert(a,o,">",90-h),e.setUsed(a,o)):t.backDiagonalDownEndsAt(a+.5,o+.5)?(r.insert(a+.5,o+.5,">",90-h),e.setUsed(a,o)):t.backDiagonalDownEndsAt(a+.25,o+.25)?(r.insert(a+.25,o+.25,">",90-h),e.setUsed(a,o)):t.verticalPassesThrough(a,o)&&(r.insert(a,o+.5,">",90),e.setUsed(a,o)))}}var m,g,b,f}(U,I,q);var $='<svg class="diagram" xmlns="http://www.w3.org/2000/svg" version="1.1" height="'+8*(U.height+1)*2+'" width="'+8*(U.width+1)+'"';if("floatleft"===t?$+=' style="float:left;margin:15px 30px 15px 0;"':"floatright"===t?$+=' style="float:right;margin:15px 0 15px 30px;"':"center"===t&&($+=' style="margin:0 auto 0 auto;"'),$+='><g transform="translate('+T(1,1)+')">\n',P){$+='<g style="opacity:0.1">\n';for(var R=0;R<U.width;++R)for(var O=0;O<U.height;++O)$+='<rect x="'+(8*(R-.5)+1)+'" + y="'+(8*(O-.5)*2+2)+'" width="6" height="14" style="fill:',U.isUsed(R,O)?$+="red;":" "===U(R,O)?$+="gray;opacity:0.05":$+="blue;",$+='"/>\n';$+="</g>\n"}if($+=I.toSVG(),$+=q.toSVG(),!H){$+='<g transform="translate(0,0)">';for(O=0;O<U.height;++O)for(R=0;R<U.width;++R){var F=U(R,O);/[\u2B22\u2B21]/.test(F)?$+='<text text-anchor="middle" x="'+8*R+'" y="'+(4+8*O*2)+'" style="font-size:20.5px">'+Q(F)+"</text>":" "===F||U.isUsed(R,O)||($+='<text text-anchor="middle" x="'+8*R+'" y="'+(4+8*O*2)+'">'+Q(F)+"</text>")}$+="</g>"}if(V){$+='<g transform="translate(2,2)">\n';for(R=0;R<U.width;++R)for(O=0;O<U.height;++O){" "!==(F=U(R,O))&&($+='<text text-anchor="middle" x="'+8*R+'" y="'+(4+8*O*2)+'" style="fill:#F00;font-family:Menlo,monospace;font-size:12px;text-align:center">'+Q(F)+"</text>")}$+="</g>"}return $=($+="</g></svg>").rp(new RegExp("","g"),"o")}function p(n,e,o){var t=/([^?]+)(?:\?id=(inc\d+)&p=([^&]+))?/.exec(location.href),p=r(t[1]),l=t[2],m=(r(t[3]&&decodeURIComponent(t[3])),0),a=l,g=!1,b=0;function r(e){return e&&e.ss(0,e.lastIndexOf("/")+1)}function i(e,t,r){function a(e){return/^[a-z]{3,6}:\/\//.test(e)?e:"/"===e[0]?s+e.ss(1):i+e}var n,i,s;e?(n=document.body.innerHTML,i=document.baseURI.rp(/\/[^/]+$/,"/"),s=i.match(/[^:/]{3,6}:\/\/[^/]*\//)[0],n=(n=(n=(n=n.rp(/\]\([ \t]*([^#")][^ "\)]+)([ \t\)])/g,function(e,t,r){return"]("+a(t)+r})).rp(/\]\([ \t]*"([^#"][^"]+)"([ \t\)])/g,function(e,t,r){return']("'+a(t)+'"'+r})).rp(/(src|href)=(["'])([^#>][^"'\n>]+)\2/g,function(e,t,r,n){return t+"="+r+a(n)+r})).rp(/(\n\[[^\]>\n \t]:[ \t]*)([^# \t][^ \t]+)"/g,function(e,t,r){return t+a(r)}),parent.postMessage([l,"=",n].join(""),"*")):setTimeout(function(){o(t,r)},1)}function f(e){var r=!1,t=e.data.substring&&e.data.replace(/^(inc\d+)=/,function(e,t){return r=t,""});r&&(document.getElementById(r).outerHTML="\n"+t+"\n",--b<=0&&i(a,n))}for(var h=-1!==navigator.userAgent.indexOf("Firefox")&&-1===navigator.userAgent.indexOf("Seamonkey"),s=0;s<e.length;++s)e[s]=e[s].rp(/(?:^|\s)\((insert|embed)[ \t]+(\S+\.\S*)[ \t]+(height=[a-zA-Z0-9.]+[ \t]+)?here\)\s/g,function(e,t,r,n){var a,i,s="inc"+ ++m,o=r.toLowerCase().rp(/\?.*$/,"").endsWith(".html");if("embed"!==t&&o)return 0===b&&(g=!0,addEventListener("message",f)),++b,'<iframe src="'+r+"?id="+s+"&p="+encodeURIComponent(p)+'" id="'+s+'" style="display:none" content="text/html;charset=UTF-8"></iframe>';var l="iframe",c="src",d=n?' style="'+n.rp(/=/g,":")+'"':"";if(h&&!o&&(l="object",c="data",location.protocol.startsWith("http"))){var u=new XMLHttpRequest;a=s,i=d,u.addEventListener("load",function(){document.getElementById(a).outerHTML=z("iframe","",'class="textinsert" srcdoc="<pre>'+this.responseText.replace(/"/g,"&quot;")+'</pre>"'+i)}),u.overrideMimeType("text/plain; charset=x-user-defined"),u.open("GET",r),u.send()}return z(l,"",'class="textinsert" id="'+s+'" '+c+'="'+r+'"'+d)});if(g)for(s=0;s<e.length;++s)n[s].innerHTML=e[s];else i(a,n,e)}var m="<style>.hljs{display:block;overflow-x:auto;padding:0.5em;background:#fff;color:#000;-webkit-text-size-adjust:none}.hljs-comment{color:#006a00}.hljs-keyword{color:#02E}.hljs-literal,.nginx .hljs-title{color:#aa0d91}.method,.hljs-list .hljs-title,.hljs-tag .hljs-title,.setting .hljs-value,.hljs-winutils,.tex .hljs-command,.http .hljs-title,.hljs-request,.hljs-status,.hljs-name{color:#008}.hljs-envvar,.tex .hljs-special{color:#660}.hljs-string{color:#c41a16}.hljs-tag .hljs-value,.hljs-cdata,.hljs-filter .hljs-argument,.hljs-attr_selector,.apache .hljs-cbracket,.hljs-date,.hljs-regexp{color:#080}.hljs-sub .hljs-identifier,.hljs-pi,.hljs-tag,.hljs-tag .hljs-keyword,.hljs-decorator,.ini .hljs-title,.hljs-shebang,.hljs-prompt,.hljs-hexcolor,.hljs-rule .hljs-value,.hljs-symbol,.hljs-symbol .hljs-string,.hljs-number,.css .hljs-function,.hljs-function .hljs-title,.coffeescript .hljs-attribute{color:#A0C}.hljs-function .hljs-title{font-weight:bold;color:#000}.hljs-class .hljs-title,.smalltalk .hljs-class,.hljs-type,.hljs-typename,.hljs-tag .hljs-attribute,.hljs-doctype,.hljs-class .hljs-id,.hljs-built_in,.setting,.hljs-params,.clojure .hljs-attribute{color:#5c2699}.hljs-variable{color:#3f6e74}.css .hljs-tag,.hljs-rule .hljs-property,.hljs-pseudo,.hljs-subst{color:#000}.css .hljs-class,.css .hljs-id{color:#9b703f}.hljs-value .hljs-important{color:#ff7700;font-weight:bold}.hljs-rule .hljs-keyword{color:#c5af75}.hljs-annotation,.apache .hljs-sqbracket,.nginx .hljs-built_in{color:#9b859d}.hljs-preprocessor,.hljs-preprocessor *,.hljs-pragma{color:#643820}.tex .hljs-formula{background-color:#eee;font-style:italic}.diff .hljs-header,.hljs-chunk{color:#808080;font-weight:bold}.diff .hljs-change{background-color:#bccff9}.hljs-addition{background-color:#baeeba}.hljs-deletion{background-color:#ffc8bd}.hljs-comment .hljs-doctag{font-weight:bold}.method .hljs-id{color:#000}</style>";function g(e){return Array.prototype.slice.call(e)}if(!window.alreadyProcessedMarkdeep){window.alreadyProcessedMarkdeep=!0;var b=-1!==window.location.href.search(/\?.*noformat.*/i);window.markdeep=Object.freeze({format:d,formatDiagram:re,stylesheet:function(){return o+l()+m}});var f='<script type="text/x-mathjax-config">MathJax.Hub.Config({ TeX: { equationNumbers: {autoNumber: "AMS"} } });<\/script><span style="display:none">'+"$$NC{\\n}{\\hat{n}}NC{\\thetai}{\\theta_\\mathrm{i}}NC{\\thetao}{\\theta_\\mathrm{o}}NC{\\d}[1]{\\mathrm{d}#1}NC{\\w}{\\hat{\\omega}}NC{\\wi}{\\w_\\mathrm{i}}NC{\\wo}{\\w_\\mathrm{o}}NC{\\wh}{\\w_\\mathrm{h}}NC{\\Li}{L_\\mathrm{i}}NC{\\Lo}{L_\\mathrm{o}}NC{\\Le}{L_\\mathrm{e}}NC{\\Lr}{L_\\mathrm{r}}NC{\\Lt}{L_\\mathrm{t}}NC{\\O}{\\mathrm{O}}NC{\\degrees}{{^{\\large\\circ}}}NC{\\T}{\\mathsf{T}}NC{\\mathset}[1]{\\mathbb{#1}}NC{\\Real}{\\mathset{R}}NC{\\Integer}{\\mathset{Z}}NC{\\Boolean}{\\mathset{B}}NC{\\Complex}{\\mathset{C}}NC{\\un}[1]{\\,\\mathrm{#1}}$$\n".rp(/NC/g,"\\newcommand")+"</span>\n",h="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.6/MathJax.js?config=TeX-AMS-MML_HTMLorMML",y=function(){var e=document.createElement("script");e.type="text/javascript",e.src=h,document.getElementsByTagName("head")[0].appendChild(e)},v=function(e){return U("detectMath")&&(-1!==e.search(/(?:\$\$[\s\S]+\$\$)|(?:\\begin{)/m)||-1!==e.search(/\\\(.*\\\)/))},w=U("mode");switch(w){case"script":return;case"html":case"doxygen":g(document.getElementsByClassName("diagram")).concat(g(document.getElementsByTagName("diagram"))).forEach(function(e){var t=$(e.innerHTML);t=t.rp(/(:?^[ \t]*\n)|(:?\n[ \t]*)$/g,""),"doxygen"===w&&(t=(t=(t=t.rp(new RegExp("–","g"),"--")).rp(new RegExp("—","g"),"---")).rp(/<a class="el" .*>(.*)<\/a>/g,"$1")),e.outerHTML='<center class="md">'+re(u(t),"")+"</center>"});var k=g(document.getElementsByClassName("markdeep")).concat(g(document.getElementsByTagName("markdeep"))),C=k.map(function(e){return u($(e.innerHTML))});return p(k,C,function(e,t){t=t||e.map(function(e){return u($(e.innerHTML))});for(var r=!1,n=0;n<k.length;++n){var a=k[n],i=document.createElement("div"),s=d(u($(a.innerHTML)),!0);r=r||v(s),i.innerHTML=s,a.parentNode.replaceChild(i,a)}r&&y(),document.head.innerHTML=window.markdeep.stylesheet()+document.head.innerHTML+(r?f:"");var o=document.getElementsByClassName("fallback");for(n=0;n<o.length;++n)o[n].remove()}),window.alreadyProcessedMarkdeep=!0}if(!b){g(document.getElementsByTagName("script")).forEach(function(e){-1!==e.src.search(/markdeep\S*?\.js$/i)&&e.parentNode.removeChild(e)});var N=parseInt(U("scrollThreshold"));document.addEventListener("scroll",function(){var e=document.body,t=e.classList,r="scrolled";N<e.scrollTop?t.add(r):t.remove(r)}),document.body&&(document.body.style.visibility="hidden")}var M=c(document.body);if(b){M=(M=M.rp(/<!-- Markdeep:.+$/gm,"")+'\x3c!-- Markdeep: --\x3e<style class="fallback">body{visibility:hidden;white-space:pre;font-family:monospace}</style><script src="markdeep.min.js"><\/script><script src="https://casual-effects.com/markdeep/latest/markdeep.min.js?"><\/script><script>window.alreadyProcessedMarkdeep||(document.body.style.visibility="visible")<\/script>').rp(/</g,"&lt;").rp(/>/g,"&gt;"),document.body.innerHTML=z("pre",M);for(var A=document.getElementsByClassName("fallback"),E=0;E<A.length;++E)A[E].remove();return}p([document.body],[M],function(e,t){!function(e){var t=d(e=e||c(document.body),!1);t+='<div id="mdContextMenu" style="visibility:hidden"></div>',document.addEventListener("contextmenu",function(e){var t=null;try{var r=e.target.tagName.match(/^H(\d)$/);if(!r)return;for(var n=e.target;n&&!n.classList.contains("md");)n=n.parentElement;if(!n)return;var a=parseInt(r[1])||1;if(!(t=document.getElementById("mdContextMenu")))return;var i=["Section","Subsection"][Math.min(a-1,1)],s=e.target.previousElementSibling.previousElementSibling,o=e.target.innerText.trim(),l=o.toLowerCase(),c=s.name,d=location+"#"+c,u=d;17<u.length&&(u=d.ss(0,7)+"&hellip;"+location.pathname.ss(location.pathname.length-8)+"#"+c);var p=z("div","Visit URL &ldquo;"+u+"&rdquo;",'onclick="(location=&quot;'+d+"&quot;)&&(document.getElementById('mdContextMenu').style.visibility='hidden')\"");return p+=z("div","Copy URL &ldquo;"+u+"&rdquo;",'onclick="navigator.clipboard.writeText(&quot;'+d+"&quot)&&(document.getElementById('mdContextMenu').style.visibility='hidden')\""),p+=z("div","Copy Markdeep &ldquo;"+o+" "+i.toLowerCase()+"&rdquo;","onclick=\"navigator.clipboard.writeText('"+o+" "+i.toLowerCase()+"')&&(document.getElementById('mdContextMenu').style.visibility='hidden')\""),p+=z("div","Copy Markdeep &ldquo;"+i+" ["+l+"]&rdquo;","onclick=\"navigator.clipboard.writeText('"+i+" ["+l+"]')&&(document.getElementById('mdContextMenu').style.visibility='hidden')\""),p+=z("div","Copy HTML &ldquo;&lt;a href=&hellip;&gt;&rdquo;","onclick=\"navigator.clipboard.writeText('&lt;a href=&quot;"+d+"&quot;&gt;"+o+"&lt;/a&gt;')&&(document.getElementById('mdContextMenu').style.visibility='hidden')\""),t.innerHTML=p,t.style.visibility="visible",t.style.left=e.pageX+"px",t.style.top=e.pageY+"px",e.preventDefault(),!1}catch(e){console.log(e),t&&(t.style.visibility="hidden")}},!1),document.addEventListener("mousedown",function(e){var t=document.getElementById("mdContextMenu");if(t){for(var r=e.target;r;r=r.parentElement)if(r===t)return;t.style.visibility="hidden"}}),document.addEventListener("keydown",function(e){if(27===e.keyCode){var t=document.getElementById("mdContextMenu");t&&(t.style.visibility="hidden")}});var r=v(t);r&&(t=f+t),t+='<div class="markdeepFooter"><i>formatted by <a href="https://casual-effects.com/markdeep" style="color:#999">Markdeep&nbsp;1.10&nbsp;&nbsp;</a></i><div style="display:inline-block;font-size:13px;font-family:\'Times New Roman\',serif;vertical-align:middle;transform:translate(-3px,-1px)rotate(135deg);">&#x2712;</div></div>';var n=1e3<e.length,a='<meta charset="UTF-8"><meta http-equiv="content-type" content="text/html;charset=UTF-8"><meta name="viewport" content="width=600, initial-scale=1"><style>body{max-width:680px;margin:auto;padding:20px;text-align:justify;line-height:140%; -webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-smoothing:antialiased;color:#222;font-family:Palatino,Georgia,"Times New Roman",serif}</style>'+o+l()+m;if(n&&(a+=z("style","div.title { padding-top: 40px; } div.afterTitles { height: 15px; }")),-1!==window.location.href.search(/\?.*export.*/i)){var i=a+document.head.innerHTML+t;r&&(i+='<script src="'+h+'"><\/script>'),document.body.innerHTML=z("pre",Q(i))}else document.head.innerHTML=a+document.head.innerHTML,document.body.innerHTML=t,r&&y();document.body.id="md",document.body.style.visibility="visible";var s=window.location.href.indexOf("#");-1<s?setTimeout(function(){var e=document.getElementsByName(window.location.href.substring(s+1));0<e.length&&e[0].scrollIntoView(),window.markdeepOptions&&(window.markdeepOptions.onLoad||Math.cos)()},100):window.markdeepOptions&&window.markdeepOptions.onLoad&&setTimeout(window.markdeepOptions.onLoad,100)}(t&&t[0])})}}();
- - - -
- -
- - - -
- - -
- - -
-
- - - - - -
-
- -
-
- - -
- - - - - - -
- - - You can’t perform that action at this time. -
- - - - - - - - - - - - - - - - -
- - - - +/**See https://casual-effects.com/markdeep for @license and documentation. +markdeep.min.js 1.17 (C) 2024 Morgan McGuire +highlight.min.js 11.9.0 (C) 2022 Ivan Sagalaev https://highlightjs.org */ +!function(){"use strict";var e=String.prototype;e.rp=e.replace,e.ss=e.substring,e.endsWith||(e.endsWith=function(e,t){return(void 0===t||t>this.length)&&(t=this.length),this.ss(t-e.length,t)===e}),e.regexIndexOf=function(e,t){var n=this.ss(t||0).search(e);return n>=0?n+(t||0):n};var t=!1,n=t,a=n,r=2,i="*",s=Array(6).join(i);function o(e,t,n){return"<"+e+(n?" "+n:"")+">"+t+""}"function"!=typeof Object.assign&&Object.defineProperty(Object,"assign",{value:function(e,t){if(null==e)throw new TypeError("Cannot convert undefined or null to object");for(var n=Object(e),a=1;add{margin-top:-8px; margin-bottom:8px}.md dl>table{margin:35px 0 30px}.md code{page-break-inside:avoid;} @media print{.md .listing code{white-space:pre-wrap}}.md .endnote{font-size:13px;line-height:15px;padding-left:10px;text-indent:-10px}.md .bib{padding-left:80px;text-indent:-80px;text-align:left}.markdeepFooter{font-size:9px;text-align:right;padding-top:80px;color:#999}.md .mediumTOC{float:right;font-size:12px;line-height:15px;border-left:1px solid #CCC;padding-left:15px;margin:15px 0px 15px 25px}.md .mediumTOC .level1{font-weight:600}.md .longTOC .level1{font-weight:600;display:block;padding-top:12px;margin:0 0 -20px}.md .shortTOC{text-align:center;font-weight:bold;margin-top:15px;font-size:14px}.md .img-attrib-container .img-attrib{font-size:50%;line-height:120%;writing-mode:vertical-rl;position:absolute;bottom:0;right:0;padding:8px 4px;color:#FFF;background-color:rgba(0,0,0,.3)}.md .img-attrib-container .img-attrib a{color:#FFF;text-decoration:none}.md .admonition{position:relative;margin:1em 0;padding:.4rem 1rem;border-radius:.2rem;border-left:2.5rem solid rgba(68,138,255,.4);background-color:rgba(68,138,255,.15);}.md .admonition-title{font-weight:bold;border-bottom:solid 1px rgba(68,138,255,.4);padding-bottom:4px;margin-bottom:4px;margin-left: -1rem;padding-left:1rem;margin-right:-1rem;border-color:rgba(68,138,255,.4)}.md .admonition.tip{border-left:2.5rem solid rgba(50,255,90,.4);background-color:rgba(50,255,90,.15)}.md .admonition.tip::before{content:\"\\24d8\";font-weight:bold;font-size:"+(u?"200%;":"150%;")+'position:relative;top:3px;color:rgba(26,128,46,.8);left:-2.95rem;display:block;width:0;height:0}.md .admonition.tip>.admonition-title{border-color:rgba(50,255,90,.4)}.md .admonition.warn,.md .admonition.warning{border-left:2.5rem solid rgba(255,145,0,.4);background-color:rgba(255,145,0,.15)}.md .admonition.warn::before,.md .admonition.warning::before{content:"\\26A0";font-weight:bold;'+(u?"":"font-size:150%;")+'position:relative;top:2px;color:rgba(128,73,0,.8);left:-2.95rem;display:block;width:0;height:0}.md .admonition.warn>.admonition-title,.md .admonition.warning>.admonition-title{border-color:rgba(255,145,0,.4)}.md .admonition.error{border-left: 2.5rem solid rgba(255,23,68,.4);background-color:rgba(255,23,68,.15)}.md .admonition.error>.admonition-title{border-color:rgba(255,23,68,.4)}.md .admonition.error::before{content: "\\2612";font-family:"Arial";font-size:'+(u?"150%;":"200%;")+'position:relative;color:rgba(128,12,34,.8);top:-2px;left:-3rem;display:block;width:0;height:0}.md .admonition p:last-child{margin-bottom:0}.md li.checked,.md li.unchecked{list-style:none;overflow:visible;text-indent:-1.2em}.md li.checked:before,.md li.unchecked:before{content:"\\2611";display:block;float:left;width:1em;font-size:120%}.md li.unchecked:before{content:"\\2610"}'),m={name:"French",keyword:{table:"tableau",figure:"figure",listing:"liste",diagram:"diagramme",contents:"Table des mati\xe8res",sec:"sec",section:"section",subsection:"paragraphe",chapter:"chapitre",Monday:"lundi",Tuesday:"mardi",Wednesday:"mercredi",Thursday:"jeudi",Friday:"vendredi",Saturday:"samedi",Sunday:"dimanche",January:"Janvier",February:"F\xe9vrier",March:"Mars",April:"Avril",May:"Mai",June:"Juin",July:"Juillet",August:"Ao\xfbt",September:"Septembre",October:"Octobre",November:"Novembre",December:"D\xe9cembre",jan:"janv.",feb:"f\xe9vr.",mar:"mars",apr:"avril",may:"mai",jun:"juin",jul:"juil.",aug:"ao\xfbt",sep:"sept.",oct:"oct.",nov:"nov.",dec:"d\xe9c.","“":"« ","&rtquo;":" »"}},p={name:"Spanish",keyword:{table:"Tabla",figure:"Figura",listing:"Listado",diagram:"Diagrama",contents:"Tabla de Contenidos",sec:"sec",section:"Secci\xf3n",subsection:"Subsecci\xf3n",chapter:"Cap\xedtulo",Monday:"Lunes",Tuesday:"Martes",Wednesday:"Mi\xe9rcoles",Thursday:"Jueves",Friday:"Viernes",Saturday:"S\xe1bado",Sunday:"Domingo",January:"Enero",February:"Febrero",March:"Marzo",April:"Abril",May:"Mayo",June:"Junio",July:"Julio",August:"Agosto",September:"Septiembre",October:"Octubre",November:"Noviembre",December:"Diciembre",jan:"ene",feb:"feb",mar:"mar",apr:"abr",may:"may",jun:"jun",jul:"jul",aug:"ago",sep:"sept",oct:"oct",nov:"nov",dec:"dic","“":"« ","&rtquo;":" »"}},b={name:"Catalan",keyword:{table:"Taula",figure:"Figura",listing:"Llistat",diagram:"Diagrama",contents:"Taula de Continguts",sec:"sec",section:"Secci\xf3",subsection:"Subsecci\xf3",chapter:"Cap\xedtol",Monday:"Dilluns",Tuesday:"Dimarts",Wednesday:"Dimecres",Thursday:"Dijous",Friday:"Divendres",Saturday:"Dissabte",Sunday:"Diumenge",January:"Gener",February:"Febrer",March:"Mar\xe7",April:"Abril",May:"Maig",June:"Juny",July:"Juliol",August:"Agost",September:"Setembre",October:"Octubre",November:"Novembre",December:"Desembre",jan:"gen",feb:"feb",mar:"mar",apr:"abr",may:"mai",jun:"jun",jul:"jul",aug:"ago",sep:"set",oct:"oct",nov:"nov",dec:"des","“":"« ","&rtquo;":" »"}},h={mode:"markdeep",detectMath:!0,lang:{keyword:{}},tocStyle:"auto",tocDepth:3,hideEmptyWeekends:!0,autoLinkImages:!0,showLabels:!1,sortScheduleLists:!0,definitionStyle:"auto",linkAPIDefinitions:!1,inlineCodeLang:!1,scrollThreshold:90,captionAbove:{diagram:!1,image:!1,table:!1,listing:!1},smartQuotes:!0},f={name:"English",keyword:{}},_={en:f,ru:{name:"Russian",keyword:{table:"\u0442\u0430\u0431\u043b\u0438\u0446\u0430",figure:"\u0440\u0438\u0441\u0443\u043d\u043e\u043a",listing:"\u043b\u0438\u0441\u0442\u0438\u043d\u0433",diagram:"\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u0430",contents:"\u0421\u043e\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u0435",sec:"\u0441\u0435\u043a",section:"\u0440\u0430\u0437\u0434\u0435\u043b",subsection:"\u043f\u043e\u0434\u0440\u0430\u0437\u0434\u0435\u043b",chapter:"\u0433\u043b\u0430\u0432\u0430",Monday:"\u043f\u043e\u043d\u0435\u0434\u0435\u043b\u044c\u043d\u0438\u043a",Tuesday:"\u0432\u0442\u043e\u0440\u043d\u0438\u043a",Wednesday:"\u0441\u0440\u0435\u0434\u0430",Thursday:"\u0447\u0435\u0442\u0432\u0435\u0440\u0433",Friday:"\u043f\u044f\u0442\u043d\u0438\u0446\u0430",Saturday:"\u0441\u0443\u0431\u0431\u043e\u0442\u0430",Sunday:"\u0432\u043e\u0441\u043a\u0440\u0435\u0441\u0435\u043d\u044c\u0435",January:"\u044f\u043d\u0432\u0430\u0440\u044cr",February:"\u0444\u0435\u0432\u0440\u0430\u043b\u044c",March:"\u043c\u0430\u0440\u0442",April:"\u0430\u043f\u0440\u0435\u043b\u044c",May:"\u043c\u0430\u0439",June:"\u0438\u044e\u043d\u044c",July:"\u0438\u044e\u043b\u044c",August:"\u0430\u0432\u0433\u0443\u0441\u0442",September:"\u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044c",October:"\u043e\u043a\u0442\u044f\u0431\u0440\u044c",November:"\u043d\u043e\u044f\u0431\u0440\u044c",December:"\u0434\u0435\u043a\u0430\u0431\u0440\u044c",jan:"\u044f\u043d\u0432",feb:"\u0444\u0435\u0432\u0440",mar:"\u043c\u0430\u0440\u0442",apr:"\u0430\u043f\u0440",may:"\u043c\u0430\u0439",jun:"\u0438\u044e\u043d\u044c",jul:"\u0438\u044e\u043b\u044c",aug:"\u0430\u0432\u0433",sep:"\u0441\u0435\u043d\u0442",oct:"\u043e\u043a\u0442",nov:"\u043d\u043e\u044f\u0431\u0440\u044c",dec:"\u0434\u0435\u043a","“":"\xab","”":"\xbb"}},fr:m,"fr-AD":m,"fr-BE":m,"fr-CA":m,"en-CA":f,"en-VI":f,pl:{name:"Polish",keyword:{table:"tabela",figure:"ilustracja",listing:"wykaz",diagram:"diagram",contents:"Spis tre\u015bci",sec:"rozdz.",section:"rozdzia\u0142",subsection:"podrozdzia\u0142",chapter:"kapitu\u0142a",Monday:"Poniedzia\u0142ek",Tuesday:"Wtorek",Wednesday:"\u015aroda",Thursday:"Czwartek",Friday:"Pi\u0105tek",Saturday:"Sobota",Sunday:"Niedziela",January:"Stycze\u0144",February:"Luty",March:"Marzec",April:"Kwiecie\u0144",May:"Maj",June:"Czerwiec",July:"Lipiec",August:"Sierpie\u0144",September:"Wrzesie\u0144",October:"Pa\u017adziernik",November:"Listopad",December:"Grudzie\u0144",jan:"sty",feb:"lut",mar:"mar",apr:"kwi",may:"maj",jun:"cze",jul:"lip",aug:"sie",sep:"wrz",oct:"pa\u017a",nov:"lis",dec:"gru","“":"„","”":"”"}},bg:{name:"Bulgarian",keyword:{table:"\u0442\u0430\u0431\u043b\u0438\u0446\u0430",figure:"\u0444\u0438\u0433\u0443\u0440\u0430",listing:"\u0441\u043f\u0438\u0441\u044a\u043a",diagram:"\u0434\u0438\u0430\u0433\u0440\u0430\u043c\u0430",contents:"c\u044a\u0434\u044a\u0440\u0436\u0430\u043d\u0438\u0435",sec:"\u0441\u0435\u043a",section:"\u0440\u0430\u0437\u0434\u0435\u043b",subsection:"\u043f\u043e\u0434\u0440\u0430\u0437\u0434\u0435\u043b",chapter:"\u0433\u043b\u0430\u0432\u0430",Monday:"\u043f\u043e\u043d\u0435\u0434\u0435\u043b\u043d\u0438\u043a",Tuesday:"\u0432\u0442\u043e\u0440\u043d\u0438\u043a",Wednesday:"\u0441\u0440\u044f\u0434\u0430",Thursday:"\u0447\u0435\u0442\u0432\u044a\u0440\u0442\u044a\u043a",Friday:"\u043f\u0435\u0442\u044a\u043a",Saturday:"\u0441\u044a\u0431\u043e\u0442\u0430",Sunday:"\u043d\u0435\u0434\u0435\u043b\u044f",January:"\u044f\u043d\u0443\u0430\u0440\u0438",February:"\u0444\u0435\u0432\u0440\u0443\u0430\u0440\u0438",March:"\u043c\u0430\u0440\u0442",April:"\u0430\u043f\u0440\u0438\u043b",May:"\u043c\u0430\u0439",June:"\u044e\u043d\u0438",July:"\u044e\u043b\u0438",August:"\u0430\u0432\u0433\u0443\u0441\u0442",September:"\u0441\u0435\u043f\u0442\u0435\u043c\u0432\u0440\u0438",October:"\u043e\u043a\u0442\u043e\u043c\u0432\u0440\u0438",November:"\u043d\u043e\u0435\u043c\u0432\u0440\u0438",December:"\u0434\u0435\u043a\u0435\u043c\u0432\u0440\u0438",jan:"\u044f\u043d",feb:"\u0444\u0435\u0432\u0440",mar:"\u043c\u0430\u0440\u0442",apr:"\u0430\u043f\u0440",may:"\u043c\u0430\u0439",jun:"\u044e\u043d\u0438",jul:"\u044e\u043b\u0438",aug:"\u0430\u0432\u0433",sep:"\u0441\u0435\u043f\u0442",oct:"\u043e\u043a\u0442",nov:"\u043d\u043e\u0435\u043c",dec:"\u0434\u0435\u043a","“":"„","”":"”"}},de:{name:"German",keyword:{table:"Tabelle",figure:"Abbildung",listing:"Auflistung",diagram:"Diagramm",contents:"Inhaltsverzeichnis",sec:"Kap",section:"Kapitel",subsection:"Unterabschnitt",chapter:"Kapitel",Monday:"Montag",Tuesday:"Dienstag",Wednesday:"Mittwoch",Thursday:"Donnerstag",Friday:"Freitag",Saturday:"Samstag",Sunday:"Sonntag",January:"Januar",February:"Februar",March:"M\xe4rz",April:"April",May:"Mai",June:"Juni",July:"Juli",August:"August",September:"September",October:"Oktober",November:"November",December:"Dezember",jan:"Jan",feb:"Feb",mar:"M\xe4r",apr:"Apr",may:"Mai",jun:"Jun",jul:"Jul",aug:"Aug",sep:"Sep",oct:"Okt",nov:"Nov",dec:"Dez","“":"„","”":"“"}},hu:{name:"Hungarian",keyword:{table:"t\xe1bl\xe1zat",figure:"\xe1bra",listing:"lista",diagram:"diagramm",contents:"Tartalomjegyz\xe9k",sec:"fej",section:"fejezet",subsection:"alfejezet",chapter:"fejezet",Monday:"h\xe9tf\u0151",Tuesday:"kedd",Wednesday:"szerda",Thursday:"cs\xfct\xf6rt\xf6k",Friday:"p\xe9ntek",Saturday:"szombat",Sunday:"vas\xe1rnap",January:"janu\xe1r",February:"febru\xe1r",March:"m\xe1rcius",April:"\xe1prilis",May:"m\xe1jus",June:"j\xfanius",July:"j\xfalius",August:"augusztus",September:"szeptember",October:"okt\xf3ber",November:"november",December:"december",jan:"jan",feb:"febr",mar:"m\xe1rc",apr:"\xe1pr",may:"m\xe1j",jun:"j\xfan",jul:"j\xfal",aug:"aug",sep:"szept",oct:"okt",nov:"nov",dec:"dec","“":"„","”":"”"}},sv:{name:"Swedish",keyword:{table:"tabell",figure:"figur",listing:"lista",diagram:"diagram",contents:"Inneh\xe5llsf\xf6rteckning",sec:"sek",section:"sektion",subsection:"sektion",chapter:"kapitel",Monday:"m\xe5ndag",Tuesday:"tisdag",Wednesday:"onsdag",Thursday:"torsdag",Friday:"fredag",Saturday:"l\xf6rdag",Sunday:"s\xf6ndag",January:"januari",February:"februari",March:"mars",April:"april",May:"maj",June:"juni",July:"juli",August:"augusti",September:"september",October:"oktober",November:"november",December:"december",jan:"jan",feb:"feb",mar:"mar",apr:"apr",may:"maj",jun:"jun",jul:"jul",aug:"aug",sep:"sep",oct:"okt",nov:"nov",dec:"dec","“":"”","”":"”"}},pt:{name:"Portugese",keyword:{table:"tabela",figure:"figura",listing:"lista",diagram:"diagrama",contents:"conte\xfado",sec:"sec",section:"sec\xe7\xe3o",subsection:"subsec\xe7\xe3o",chapter:"cap\xedtulo",Monday:"Segunda-feira",Tuesday:"Ter\xe7a-feira",Wednesday:"Quarta-feira",Thursday:"Quinta-feira",Friday:"Sexta-feira",Saturday:"S\xe1bado",Sunday:"Domingo",January:"Janeiro",February:"Fevereiro",March:"Mar\xe7o",April:"Abril",May:"Maio",June:"Junho",July:"Julho",August:"Agosto",September:"Setembro",October:"Outubro",November:"Novembro",December:"Dezembro",jan:"jan",feb:"fev",mar:"mar",apr:"abr",may:"mai",jun:"jun",jul:"jul",aug:"ago",sep:"set",oct:"oct",nov:"nov",dec:"dez","“":"«","&rtquo;":"»"}},ja:{name:"Japanese",keyword:{table:"\u8868",figure:"\u56f3",listing:"\u4e00\u89a7",diagram:"\u56f3",contents:"\u76ee\u6b21",sec:"\u7bc0",section:"\u7bc0",subsection:"\u9805",chapter:"\u7ae0",Monday:"\u6708",Tuesday:"\u706b",Wednesday:"\u6c34",Thursday:"\u6728",Friday:"\u91d1",Saturday:"\u571f",Sunday:"\u65e5",January:"1\u6708",February:"2\u6708",March:"3\u6708",April:"4\u6708",May:"5\u6708",June:"6\u6708",July:"7\u6708",August:"8\u6708",September:"9\u6708",October:"10\u6708",November:"11\u6708",December:"12\u6708",jan:"1\u6708",feb:"2\u6708",mar:"3\u6708",apr:"4\u6708",may:"5\u6708",jun:"6\u6708",jul:"7\u6708",aug:"8\u6708",sep:"9\u6708",oct:"10\u6708",nov:"11\u6708",dec:"12\u6708","“":"\u300c","”":"\u300d"}},it:{name:"Italian",keyword:{table:"tabella",figure:"figura",listing:"lista",diagram:"diagramma",contents:"indice",sec:"sez",section:"sezione",subsection:"paragrafo",chapter:"capitolo",Monday:"luned\xec",Tuesday:"marted\xec",Wednesday:"mercoled\xec",Thursday:"gioved\xec",Friday:"venerd\xec",Saturday:"sabato",Sunday:"domenica",January:"Gennaio",February:"Febbraio",March:"Marzo",April:"Aprile",May:"Maggio",June:"Giugno",July:"Luglio",August:"Agosto",September:"Settembre",October:"Ottobre",November:"Novembre",December:"Dicembre",jan:"gen",feb:"feb",mar:"mar",apr:"apr",may:"mag",jun:"giu",jul:"lug",aug:"ago",sep:"set",oct:"ott",nov:"nov",dec:"dic","“":"“","&rtquo;":"”"}},lt:{name:"Lithuanian",keyword:{table:"lentel\u0117",figure:"paveiksl\u0117lis",listing:"s\u0105ra\u0161as",diagram:"diagrama",contents:"Turinys",sec:"sk",section:"skyrius",subsection:"poskyris",chapter:"skyrius",Monday:"pirmadienis",Tuesday:"antradienis",Wednesday:"tre\u010diadienis",Thursday:"ketvirtadienis",Friday:"penktadienis",Saturday:"\u0161e\u0161tadienis",Sunday:"sekmadienis",January:"Sausis",February:"Vasaris",March:"Kovas",April:"Balandis",May:"Gegu\u017e\u0117",June:"Bir\u017eelis",July:"Liepa",August:"Rugpj\u016btis",September:"Rugs\u0117jis",October:"Spalis",November:"Lapkritis",December:"Gruodis",jan:"saus",feb:"vas",mar:"kov",apr:"bal",may:"geg",jun:"bir\u017e",jul:"liep",aug:"rugpj",sep:"rugs",oct:"spal",nov:"lapkr",dec:"gruod","“":"„","&rtquo;":"“"}},cs:{name:"Czech",keyword:{table:"Tabulka",figure:"Obr\xe1zek",listing:"Seznam",diagram:"Diagram",contents:"Obsah",sec:"kap.",section:"kapitola",subsection:"podkapitola",chapter:"kapitola",Monday:"pond\u011bl\xed",Tuesday:"\xfater\xfd",Wednesday:"st\u0159eda",Thursday:"\u010dtvrtek",Friday:"p\xe1tek",Saturday:"sobota",Sunday:"ned\u011ble",January:"leden",February:"\xfanor",March:"b\u0159ezen",April:"duben",May:"kv\u011bten",June:"\u010derven",July:"\u010dervenec",August:"srpen",September:"z\xe1\u0159\xed",October:"\u0159\xedjen",November:"listopad",December:"prosinec",jan:"led",feb:"\xfano",mar:"b\u0159e",apr:"dub",may:"kv\u011b",jun:"\u010dvn",jul:"\u010dvc",aug:"srp",sep:"z\xe1\u0159",oct:"\u0159\xedj",nov:"lis",dec:"pro","“":"„","”":"“"}},es:p,"es-ES":p,"ca-ES":b,"es-CO":p,"es-US":p,"en-US":f,ca:b};[].slice.call(document.getElementsByTagName("meta")).forEach((function(e){var t=e.getAttribute("lang");if(t){var n=_[t];n&&(h.lang=n)}}));var y=Math.max,x=Math.min,v=Math.abs,w=Math.sign||function(e){return+e===e?0===e?e:e>0?1:-1:NaN};function E(e,t){if(window.markdeepOptions&&void 0!==window.markdeepOptions[e]){var n=window.markdeepOptions[e];return t?void 0!==(n=n[t])?n:h[e][t]:window.markdeepOptions[e]}return void 0!==h[e]?t?h[e][t]:h[e]:void console.warn('Illegal option: "'+e+'"')}function N(e,t){if(E("showLabels")){var n=" {\xa0"+e+"\xa0}";return t?o(t,n):n}return""}function M(e){return E("lang").keyword[e]||E("lang").keyword[e.toLowerCase()]||e}function A(e){return String(e).rp(/&/g,"&").rp(//g,">").rp(/"/g,""")}function k(e){return e.rp(/</g,"<").rp(/>/g,">").rp(/"/g,'"').rp(/'/g,"'").rp(/–/g,"\u2013").rp(/—/g,"---").rp(/&/g,"&")}function C(e){return e.rp(/<.*?>/g,"")}function S(e){return encodeURI(e.rp(/\s/g,"").toLowerCase())}function T(){for(var e="",t=1;t<=6;++t){e+=".md h"+t+"::before {\ncontent:";for(var n=1;n<=t;++n)e+="counter(h"+n+') "'+(n|<\/ftp:.*>|<\/[^ "\t\n>]+@[^ "\t\n>]+>/gi,"")).rp(/<(https?|ftp): (.*?)>/gi,(function(e,t,n){var a="<"+t+"://"+n.rp(/=""\s/g,"/");return'=""'===a.ss(a.length-3)&&(a=a.ss(0,a.length-3)),(a=a.rp(/"/g,""))+">"}))).rp(/";function H(e){return Array.prototype.slice.call(e)}if(!window.alreadyProcessedMarkdeep){window.alreadyProcessedMarkdeep=!0;var G=-1!==window.location.href.search(/\?.*noformat.*/i);window.markdeep=Object.freeze({format:j,formatDiagram:$,langTable:_,stylesheet:function(){return g+T()+P}});var Z='