Main is also changed: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight #include "camera.h" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ ... int main() { // Image const auto aspect_ratio = 16.0 / 9.0; const int image_width = 400; const int image_height = static_cast(image_width / aspect_ratio); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight const int samples_per_pixel = 100; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ // World hittable_list world; world.add(make_shared(point3(0,0,-1), 0.5)); world.add(make_shared(point3(0,-100.5,-1), 100)); // Camera ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight camera cam; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ // 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) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight color pixel_color(0, 0, 0); for (int s = 0; s < samples_per_pixel; ++s) { auto u = (i + random_double()) / (image_width-1); auto v = (j + random_double()) / (image_height-1); ray r = cam.get_ray(u, v); pixel_color += ray_color(r, world); } write_color(std::cout, pixel_color, samples_per_pixel); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ } } std::cerr << "\nDone.\n"; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [main-multi-sample]: [main.cc] Rendering with multi-sampled pixels]
Zooming into the image that is produced, we can see the difference in edge pixels. ![Image 6: Before and after antialiasing ](../images/img-1.06-antialias-before-after.png class=pixel) Diffuse Materials ==================================================================================================== 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 mix and match geometry and materials (so we can assign a material to multiple spheres, or vice versa) or if 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. A Simple Diffuse Material --------------------------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: ![Figure [light-bounce]: Light ray bounces](../images/fig-1.08-light-bounce.jpg)
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.) (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.)There are two unit radius spheres tangent to the hit point $p$ of a surface. These two spheres have a center of $(\mathbf{P} + \mathbf{n})$ and $(\mathbf{P} - \mathbf{n})$, where $\mathbf{n}$ is the normal of the surface. 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. Select the tangent unit radius sphere that is on the same side of the surface as the ray origin. Pick a random point $\mathbf{S}$ inside 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-vec]: Generating a random diffuse bounce ray](../images/fig-1.09-rand-vec.jpg)
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. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class vec3 { public: ... 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]: [vec3.h] vec3 random utility functions] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ vec3 random_in_unit_sphere() { while (true) { auto p = vec3::random(-1,1); if (p.length_squared() >= 1) continue; return p; } } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [random-in-unit-sphere]: [vec3.h] The random_in_unit_sphere() function]
Then update the `ray_color()` function to use the new random direction generator: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ color ray_color(const ray& r, const hittable& world) { hit_record rec; if (world.hit(r, 0, infinity, rec)) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight point3 target = rec.p + rec.normal + random_in_unit_sphere(); return 0.5 * ray_color(ray(rec.p, target - rec.p), world); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ } vec3 unit_direction = unit_vector(r.direction()); auto t = 0.5*(unit_direction.y() + 1.0); return (1.0-t)*color(1.0, 1.0, 1.0) + t*color(0.5, 0.7, 1.0); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [ray-color-random-unit]: [main.cc] ray_color() using a random ray direction]
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++ highlight color ray_color(const ray& r, const hittable& world, int depth) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ hit_record rec; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight // If we've exceeded the ray bounce limit, no more light is gathered. if (depth <= 0) return color(0,0,0); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ if (world.hit(r, 0, infinity, rec)) { point3 target = rec.p + rec.normal + random_in_unit_sphere(); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight return 0.5 * ray_color(ray(rec.p, target - rec.p), world, depth-1); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ } vec3 unit_direction = unit_vector(r.direction()); auto t = 0.5*(unit_direction.y() + 1.0); return (1.0-t)*color(1.0, 1.0, 1.0) + t*color(0.5, 0.7, 1.0); } ... int main() { // Image const auto aspect_ratio = 16.0 / 9.0; const int image_width = 400; const int image_height = static_cast(image_width / aspect_ratio); const int samples_per_pixel = 100; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight const int max_depth = 50; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ ... // 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) { color pixel_color(0, 0, 0); for (int s = 0; s < samples_per_pixel; ++s) { auto u = (i + random_double()) / (image_width-1); auto v = (j + random_double()) / (image_height-1); ray r = cam.get_ray(u, v); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight pixel_color += ray_color(r, world, max_depth); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ } write_color(std::cout, pixel_color, samples_per_pixel); } } std::cerr << "\nDone.\n"; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [ray-color-depth]: [main.cc] ray_color() with depth limiting]
This gives us: ![Image 7: First render of a diffuse sphere](../images/img-1.07-first-diffuse.png class=pixel)
Using Gamma Correction for Accurate Color Intensity ----------------------------------------------------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, color pixel_color, int samples_per_pixel) { auto r = pixel_color.x(); auto g = pixel_color.y(); auto b = pixel_color.z(); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight // Divide the color by the number of samples and gamma-correct for gamma=2.0. auto scale = 1.0 / samples_per_pixel; r = sqrt(scale * r); g = sqrt(scale * g); b = sqrt(scale * b); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ // Write the translated [0,255] value of each color component. out << static_cast(256 * clamp(r, 0.0, 0.999)) << ' ' << static_cast(256 * clamp(g, 0.0, 0.999)) << ' ' << static_cast(256 * clamp(b, 0.0, 0.999)) << '\n'; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [write-color-gamma]: [color.h] write_color(), with gamma correction]
That yields light grey, as we desire: ![Image 8: Diffuse sphere, with gamma correction ](../images/img-1.08-gamma-correct.png class=pixel)
Fixing Shadow Acne -------------------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: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ if (world.hit(r, 0.001, infinity, rec)) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [reflect-tolerance]: [main.cc] Calculating reflected ray origins with tolerance] This gets rid of the shadow acne problem. Yes it is really called that.
True Lambertian Reflection ---------------------------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. This distribution 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 random points on the surface of the unit sphere, offset along the surface normal. Picking random points on the unit sphere can be achieved by picking random points _in_ the unit sphere, and then normalizing those. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ inline vec3 random_in_unit_sphere() { ... } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight vec3 random_unit_vector() { return unit_vector(random_in_unit_sphere()); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [random-unit-vector]: [vec3.h] The random_unit_vector() function] ![Figure [rand-unitvec]: Generating a random unit vector](../images/fig-1.10-rand-unitvec.png)
This `random_unit_vector()` is a drop-in replacement for the existing `random_in_unit_sphere()` function. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ color ray_color(const ray& r, 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 color(0,0,0); if (world.hit(r, 0.001, infinity, rec)) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight point3 target = rec.p + rec.normal + random_unit_vector(); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ return 0.5 * ray_color(ray(rec.p, target - rec.p), world, depth-1); } vec3 unit_direction = unit_vector(r.direction()); auto t = 0.5*(unit_direction.y() + 1.0); return (1.0-t)*color(1.0, 1.0, 1.0) + t*color(0.5, 0.7, 1.0); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [ray-color-unit-sphere]: [main.cc] ray_color() with replacement diffuse]
After rendering we get a similar image: ![Image 9: Correct rendering of Lambertian spheres ](../images/img-1.09-correct-lambertian.png class=pixel) 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 less pronounced after the change 2. Both spheres are lighter in appearance after the change 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.
An Alternative Diffuse Formulation -----------------------------------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: 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) 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.
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. 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). ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 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; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [random-in-hemisphere]: [vec3.h] The random_in_hemisphere(normal) function]
Plugging the new formula into the `ray_color()` function: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ color ray_color(const ray& r, 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 color(0,0,0); if (world.hit(r, 0.001, infinity, rec)) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight point3 target = rec.p + random_in_hemisphere(rec.normal); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ return 0.5 * ray_color(ray(rec.p, target - rec.p), world, depth-1); } vec3 unit_direction = unit_vector(r.direction()); auto t = 0.5*(unit_direction.y() + 1.0); return (1.0-t)*color(1.0, 1.0, 1.0) + t*color(0.5, 0.7, 1.0); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [ray-color-hemisphere]: [main.cc] ray_color() with hemispherical scattering] Gives us the following image: ![Image 10: Rendering of diffuse spheres with hemispherical scattering ](../images/img-1.10-rand-hemispherical.png class=pixel)
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.
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: 1. Produce a scattered ray (or say it absorbed the incident ray). 2. If scattered, say how much the ray should be attenuated.This suggests the abstract class: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ #ifndef MATERIAL_H #define MATERIAL_H #include "rtweekend.h" struct hit_record; class material { public: virtual bool scatter( const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered ) const = 0; }; #endif ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [material-initial]: [material.h] The material class]
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; 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: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight #include "rtweekend.h" class material; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ struct hit_record { point3 p; vec3 normal; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight shared_ptr mat_ptr; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ double t; bool front_face; inline 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; } }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [hit-with-material]: [hittable.h] Hit record with added material pointer]
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 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 `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.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: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class sphere : public hittable { public: sphere() {} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight sphere(point3 cen, double r, shared_ptr m) : center(cen), radius(r), mat_ptr(m) {}; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ virtual bool hit( const ray& r, double t_min, double t_max, hit_record& rec) const override; public: point3 center; double radius; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight shared_ptr mat_ptr; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ }; bool sphere::hit(const ray& r, double t_min, double t_max, hit_record& rec) const { ... 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 rec.mat_ptr = mat_ptr; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ return true; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [sphere-material]: [sphere.h] Ray-sphere intersection with added material information]
Modeling Light Scatter and Reflectance ---------------------------------------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 color& a) : albedo(a) {} virtual 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; } public: color albedo; }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [lambertian-initial]: [material.h] The lambertian material class]
Note we could just as well only scatter with some probability $p$ and have attenuation be $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. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class vec3 { ... bool near_zero() const { // Return true if the vector is close to zero in all dimensions. const auto s = 1e-8; return (fabs(e[0]) < s) && (fabs(e[1]) < s) && (fabs(e[2]) < s); } ... }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [vec3-near-zero]: [vec3.h] The vec3::near_zero() method] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class lambertian : public material { public: lambertian(const color& a) : albedo(a) {} virtual 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: color albedo; }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [lambertian-catch-zero]: [material.h] Lambertian scatter, bullet-proof]
Mirrored Light Reflection --------------------------For smooth metals the ray won’t be randomly scattered. The key math is: how does a ray get reflected from a metal mirror? Vector math is our friend here: ![Figure [reflection]: Ray reflection](../images/fig-1.11-reflection.jpg)
The reflected ray direction in red is just $\mathbf{v} + 2\mathbf{b}$. In our design, $\mathbf{n}$ is a unit vector, but $\mathbf{v}$ may not be. The length of $\mathbf{b}$ should be $\mathbf{v} \cdot \mathbf{n}$. Because $\mathbf{v}$ points in, we will need a minus sign, yielding: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ vec3 reflect(const vec3& v, const vec3& n) { return v - 2*dot(v,n)*n; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [vec3-reflect]: [vec3.h] vec3 reflection function]
The metal material just reflects rays using that formula: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class metal : public material { public: metal(const color& a) : albedo(a) {} virtual bool scatter( const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered ) const override { 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: color albedo; }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [metal-material]: [material.h] Metal material with reflectance function]
We need to modify the `ray_color()` function to use this: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ color ray_color(const ray& r, 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 color(0,0,0); if (world.hit(r, 0.001, infinity, rec)) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight ray scattered; color attenuation; if (rec.mat_ptr->scatter(r, rec, attenuation, scattered)) return attenuation * ray_color(scattered, world, depth-1); 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)*color(1.0, 1.0, 1.0) + t*color(0.5, 0.7, 1.0); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [ray-color-scatter]: [main.cc] Ray color with scattered reflectance]
A Scene with Metal Spheres ---------------------------Now let’s add some metal spheres to our scene: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ ... ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight #include "material.h" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ ... int main() { // Image const auto aspect_ratio = 16.0 / 9.0; const int image_width = 400; const int image_height = static_cast(image_width / aspect_ratio); const int samples_per_pixel = 100; const int max_depth = 50; // World hittable_list world; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight auto material_ground = make_shared(color(0.8, 0.8, 0.0)); auto material_center = make_shared(color(0.7, 0.3, 0.3)); auto material_left = make_shared(color(0.8, 0.8, 0.8)); auto material_right = make_shared(color(0.8, 0.6, 0.2)); world.add(make_shared(point3( 0.0, -100.5, -1.0), 100.0, material_ground)); world.add(make_shared(point3( 0.0, 0.0, -1.0), 0.5, material_center)); world.add(make_shared(point3(-1.0, 0.0, -1.0), 0.5, material_left)); world.add(make_shared(point3( 1.0, 0.0, -1.0), 0.5, material_right)); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ // Camera camera cam; // 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) { color pixel_color(0, 0, 0); for (int s = 0; s < samples_per_pixel; ++s) { auto u = (i + random_double()) / (image_width-1); auto v = (j + random_double()) / (image_height-1); ray r = cam.get_ray(u, v); pixel_color += ray_color(r, world, max_depth); } write_color(std::cout, pixel_color, samples_per_pixel); } } std::cerr << "\nDone.\n"; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [scene-with-metal]: [main.cc] Scene with metal spheres]
Which gives: ![Image 11: Shiny metal](../images/img-1.11-metal-shiny.png class=pixel)
Fuzzy Reflection -----------------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-1.12-reflect-fuzzy.jpg)
The bigger the 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. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class metal : public material { public: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight metal(const color& a, double f) : albedo(a), fuzz(f < 1 ? f : 1) {} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ virtual bool scatter( const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered ) const override { vec3 reflected = reflect(unit_vector(r_in.direction()), rec.normal); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight scattered = ray(rec.p, reflected + fuzz*random_in_unit_sphere()); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ attenuation = albedo; return (dot(scattered.direction(), rec.normal) > 0); } public: color albedo; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight double fuzz; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [metal-fuzz]: [material.h] Metal material fuzziness]
We can try that out by adding fuzziness 0.3 and 1.0 to the metals: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ int main() { ... // World auto material_ground = make_shared(color(0.8, 0.8, 0.0)); auto material_center = make_shared(color(0.7, 0.3, 0.3)); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight auto material_left = make_shared(color(0.8, 0.8, 0.8), 0.3); auto material_right = make_shared(color(0.8, 0.6, 0.2), 1.0); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ ... } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [metal-fuzz-spheres]: [main.cc] Metal spheres with fuzziness] ![Image 12: Fuzzed metal](../images/img-1.12-metal-fuzz.png class=pixel) Dielectrics ==================================================================================================== Clear materials such as water, glass, and diamonds 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. 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!): ![Image 13: Glass first](../images/img-1.13-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. 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: ![Figure [refraction]: Ray refraction](../images/fig-1.13-refraction.jpg)
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 perpendicular to $\mathbf{n'}$ and parallel to $\mathbf{n'}$: $$ \mathbf{R'} = \mathbf{R'}_{\bot} + \mathbf{R'}_{\parallel} $$ If we solve for $\mathbf{R'}_{\bot}$ and $\mathbf{R'}_{\parallel}$ we get: $$ \mathbf{R'}_{\bot} = \frac{\eta}{\eta'} (\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: $$ \mathbf{a} \cdot \mathbf{b} = |\mathbf{a}| |\mathbf{b}| \cos\theta $$ If we restrict $\mathbf{a}$ and $\mathbf{b}$ to be unit vectors: $$ \mathbf{a} \cdot \mathbf{b} = \cos\theta $$ We can now rewrite $\mathbf{R'}_{\bot}$ in terms of known quantities: $$ \mathbf{R'}_{\bot} = \frac{\eta}{\eta'} (\mathbf{R} + (\mathbf{-R} \cdot \mathbf{n}) \mathbf{n}) $$ 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 = fmin(dot(-uv, n), 1.0); vec3 r_out_perp = etai_over_etat * (uv + cos_theta*n); vec3 r_out_parallel = -sqrt(fabs(1.0 - r_out_perp.length_squared())) * n; return r_out_perp + r_out_parallel; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [refract]: [vec3.h] Refraction function]
And the dielectric material that always refracts is: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class dielectric : public material { public: dielectric(double index_of_refraction) : ir(index_of_refraction) {} virtual 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 refraction_ratio = rec.front_face ? (1.0/ir) : ir; vec3 unit_direction = unit_vector(r_in.direction()); vec3 refracted = refract(unit_direction, rec.normal, refraction_ratio); scattered = ray(rec.p, refracted); return true; } public: double ir; // Index of Refraction }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [dielectric]: [material.h] Dielectric material class that always refracts]
Now we'll update the scene to change the left and center spheres to glass: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ auto material_ground = make_shared(color(0.8, 0.8, 0.0)); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight auto material_center = make_shared(1.5); auto material_left = make_shared(1.5); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ auto material_right = make_shared(color(0.8, 0.6, 0.2), 1.0); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [two-glass]: [main.cc] Changing left and center spheres to glass]
This gives us the following result: ![Image 14: Glass sphere that always refracts ](../images/img-1.14-glass-always-refract.png class=pixel) Total Internal Reflection -------------------------- 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'$: $$ \sin\theta' = \frac{\eta}{\eta'} \cdot \sin\theta $$ If the ray is inside glass and outside is air ($\eta = 1.5$ and $\eta' = 1.0$): $$ \sin\theta' = \frac{1.5}{1.0} \cdot \sin\theta $$ 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 solution does not exist, the glass cannot refract, and therefore must reflect the ray: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ if (refraction_ratio * sin_theta > 1.0) { // Must Reflect ... } else { // Can Refract ... } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [dielectric]: [material.h] 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.We can solve for `sin_theta` using the trigonometric qualities: $$ \sin\theta = \sqrt{1 - \cos^2\theta} $$ and $$ \cos\theta = \mathbf{R} \cdot \mathbf{n} $$ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ double cos_theta = fmin(dot(-unit_direction, rec.normal), 1.0); double sin_theta = sqrt(1.0 - cos_theta*cos_theta); if (refraction_ratio * sin_theta > 1.0) { // Must Reflect ... } else { // Can Refract ... } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [dielectric]: [material.h] Determining if the ray can refract]
And the dielectric material that always refracts (when possible) is: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class dielectric : public material { public: dielectric(double index_of_refraction) : ir(index_of_refraction) {} virtual 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 refraction_ratio = rec.front_face ? (1.0/ir) : ir; vec3 unit_direction = unit_vector(r_in.direction()); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight double cos_theta = fmin(dot(-unit_direction, rec.normal), 1.0); double sin_theta = sqrt(1.0 - cos_theta*cos_theta); bool cannot_refract = refraction_ratio * sin_theta > 1.0; vec3 direction; if (cannot_refract) direction = reflect(unit_direction, rec.normal); else direction = refract(unit_direction, rec.normal, refraction_ratio); scattered = ray(rec.p, direction); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ return true; } public: double ir; // Index of Refraction }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [dielectric]: [material.h] Dielectric material class with reflection]
Attenuation is always 1 -- the glass surface absorbs nothing. If we try that out with these parameters: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ auto material_ground = make_shared(color(0.8, 0.8, 0.0)); auto material_center = make_shared(color(0.1, 0.2, 0.5)); auto material_left = make_shared(1.5); auto material_right = make_shared(color(0.8, 0.6, 0.2), 0.0); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [scene-dielectric]: [main.cc] Scene with dielectric and shiny sphere] We get: ![Image 15: Glass sphere that sometimes refracts ](../images/img-1.15-glass-sometimes-refract.png class=pixel)
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++ class dielectric : public material { public: dielectric(double index_of_refraction) : ir(index_of_refraction) {} virtual 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 refraction_ratio = rec.front_face ? (1.0/ir) : ir; vec3 unit_direction = unit_vector(r_in.direction()); double cos_theta = fmin(dot(-unit_direction, rec.normal), 1.0); double sin_theta = sqrt(1.0 - cos_theta*cos_theta); bool cannot_refract = refraction_ratio * sin_theta > 1.0; vec3 direction; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight if (cannot_refract || reflectance(cos_theta, refraction_ratio) > random_double()) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ direction = reflect(unit_direction, rec.normal); else direction = refract(unit_direction, rec.normal, refraction_ratio); scattered = ray(rec.p, direction); return true; } public: double ir; // Index of Refraction ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight private: static double reflectance(double cosine, double ref_idx) { // Use Schlick's approximation for reflectance. auto r0 = (1-ref_idx) / (1+ref_idx); r0 = r0*r0; return r0 + (1-r0)*pow((1 - cosine),5); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [glass]: [material.h] Full glass material]
Modeling a Hollow Glass Sphere -------------------------------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: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ world.add(make_shared(point3( 0.0, -100.5, -1.0), 100.0, material_ground)); world.add(make_shared(point3( 0.0, 0.0, -1.0), 0.5, material_center)); world.add(make_shared(point3(-1.0, 0.0, -1.0), 0.5, material_left)); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight world.add(make_shared(point3(-1.0, 0.0, -1.0), -0.4, material_left)); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ world.add(make_shared(point3( 1.0, 0.0, -1.0), 0.5, material_right)); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [scene-hollow-glass]: [main.cc] Scene with hollow glass sphere]
This gives: ![Image 16: A hollow glass sphere](../images/img-1.16-glass-hollow.png class=pixel)
Positionable Camera ==================================================================================================== 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. Camera Viewing Geometry ------------------------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-1.14-cam-view-geom.jpg)
This implies $h = \tan(\frac{\theta}{2})$. Our camera now becomes: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class camera { public: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight camera( double vfov, // vertical field-of-view in degrees double aspect_ratio ) { auto theta = degrees_to_radians(vfov); auto h = tan(theta/2); auto viewport_height = 2.0 * h; auto viewport_width = aspect_ratio * viewport_height; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ auto focal_length = 1.0; origin = point3(0, 0, 0); horizontal = vec3(viewport_width, 0.0, 0.0); vertical = vec3(0.0, viewport_height, 0.0); lower_left_corner = origin - horizontal/2 - vertical/2 - vec3(0, 0, focal_length); } ray get_ray(double u, double v) const { return ray(origin, lower_left_corner + u*horizontal + v*vertical - origin); } private: point3 origin; point3 lower_left_corner; vec3 horizontal; vec3 vertical; }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [camera-fov]: [camera.h] Camera with adjustable field-of-view (fov)]
When calling it with camera `cam(90, aspect_ratio)` and these spheres: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ int main() { ... // World ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight auto R = cos(pi/4); hittable_list world; auto material_left = make_shared(color(0,0,1)); auto material_right = make_shared(color(1,0,0)); world.add(make_shared(point3(-R, 0, -1), R, material_left)); world.add(make_shared(point3( R, 0, -1), R, material_right)); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ // Camera ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight camera cam(90.0, aspect_ratio); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ // Render std::cout << "P3\n" << image_width << " " << image_height << "\n255\n"; for (int j = image_height-1; j >= 0; --j) { ... ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [scene-wide-angle]: [main.cc] Scene with wide-angle camera] gives: ![Image 17: A wide-angle view](../images/img-1.17-wide-view.png class=pixel)
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.) 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. ![Figure [cam-view-dir]: Camera view direction](../images/fig-1.15-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. ![Figure [cam-view-up]: Camera view up direction](../images/fig-1.16-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. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class camera { public: camera( ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight point3 lookfrom, point3 lookat, vec3 vup, ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ double vfov, // vertical field-of-view in degrees double aspect_ratio ) { auto theta = degrees_to_radians(vfov); auto h = tan(theta/2); auto viewport_height = 2.0 * h; auto viewport_width = aspect_ratio * viewport_height; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight auto w = unit_vector(lookfrom - lookat); auto u = unit_vector(cross(vup, w)); auto v = cross(w, u); origin = lookfrom; horizontal = viewport_width * u; vertical = viewport_height * v; lower_left_corner = origin - horizontal/2 - vertical/2 - w; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight ray get_ray(double s, double t) const { return ray(origin, lower_left_corner + s*horizontal + t*vertical - origin); } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ private: point3 origin; point3 lower_left_corner; vec3 horizontal; vec3 vertical; }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [camera-orient]: [camera.h] Positionable and orientable camera]We'll change back to the prior scene, and use the new viewpoint: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ hittable_list world; auto material_ground = make_shared(color(0.8, 0.8, 0.0)); auto material_center = make_shared(color(0.1, 0.2, 0.5)); auto material_left = make_shared(1.5); auto material_right = make_shared(color(0.8, 0.6, 0.2), 0.0); world.add(make_shared(point3( 0.0, -100.5, -1.0), 100.0, material_ground)); world.add(make_shared(point3( 0.0, 0.0, -1.0), 0.5, material_center)); world.add(make_shared(point3(-1.0, 0.0, -1.0), 0.5, material_left)); world.add(make_shared(point3(-1.0, 0.0, -1.0), -0.45, material_left)); world.add(make_shared(point3( 1.0, 0.0, -1.0), 0.5, material_right)); camera cam(point3(-2,2,1), point3(0,0,-1), vec3(0,1,0), 90, aspect_ratio); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [scene-free-view]: [main.cc] Scene with alternate viewpoint] to get: ![Image 18: A distant view](../images/img-1.18-view-distant.png class=pixel) And we can change field of view: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ camera cam(point3(-2,2,1), point3(0,0,-1), vec3(0,1,0), 20, aspect_ratio); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [change-field-view]: [main.cc] Change field of view] to get: ![Image 19: Zooming in](../images/img-1.19-view-zoom.png class=pixel)
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 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 projection point and the plane where everything is in perfect focus the _focus distance_. Be aware that the focus distance is not the same as the focal length -- the _focal length_ is the distance between the projection point and the image plane. 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 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 want defocus blur. 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-1.17-cam-lens.jpg)
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 lens, and send them toward the focus plane (`focus_dist` away from the lens), where everything on that plane is in perfect focus. ![Figure [cam-film-plane]: Camera focus plane](../images/fig-1.18-cam-film-plane.jpg)
Generating Sample Rays ----------------------- 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`). ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ 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; } } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [rand-in-unit-disk]: [vec3.h] Generate random point inside unit disk] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ class camera { public: camera( point3 lookfrom, point3 lookat, vec3 vup, double vfov, // vertical field-of-view in degrees ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight double aspect_ratio, double aperture, double focus_dist ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ ) { auto theta = degrees_to_radians(vfov); auto h = tan(theta/2); auto viewport_height = 2.0 * h; auto viewport_width = aspect_ratio * viewport_height; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight w = unit_vector(lookfrom - lookat); u = unit_vector(cross(vup, w)); v = cross(w, u); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ origin = lookfrom; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight horizontal = focus_dist * viewport_width * u; vertical = focus_dist * viewport_height * v; lower_left_corner = origin - horizontal/2 - vertical/2 - focus_dist*w; lens_radius = aperture / 2; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ } ray get_ray(double s, double t) const { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight vec3 rd = lens_radius * random_in_unit_disk(); vec3 offset = u * rd.x() + v * rd.y(); return ray( origin + offset, lower_left_corner + s*horizontal + t*vertical - origin - offset ); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ } private: point3 origin; point3 lower_left_corner; vec3 horizontal; vec3 vertical; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight vec3 u, v, w; double lens_radius; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [camera-dof]: [camera.h] Camera with adjustable depth-of-field (dof)]Using a big aperture: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ point3 lookfrom(3,3,2); point3 lookat(0,0,-1); vec3 vup(0,1,0); auto dist_to_focus = (lookfrom-lookat).length(); auto aperture = 2.0; camera cam(lookfrom, lookat, vup, 20, aspect_ratio, aperture, dist_to_focus); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [scene-camera-dof]: [main.cc] Scene camera with depth-of-field] We get: ![Image 20: Spheres with depth-of-field](../images/img-1.20-depth-of-field.png class=pixel)
Where Next? ==================================================================================================== A Final Render ---------------First let’s make the image on the cover of this book -- lots of random spheres: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight hittable_list random_scene() { hittable_list world; auto ground_material = make_shared(color(0.5, 0.5, 0.5)); world.add(make_shared(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(); 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 sphere_material; if (choose_mat < 0.8) { // diffuse auto albedo = color::random() * color::random(); sphere_material = make_shared(albedo); world.add(make_shared(center, 0.2, sphere_material)); } else if (choose_mat < 0.95) { // metal auto albedo = color::random(0.5, 1); auto fuzz = random_double(0, 0.5); sphere_material = make_shared(albedo, fuzz); world.add(make_shared(center, 0.2, sphere_material)); } else { // glass sphere_material = make_shared(1.5); world.add(make_shared(center, 0.2, sphere_material)); } } } } auto material1 = make_shared(1.5); world.add(make_shared(point3(0, 1, 0), 1.0, material1)); auto material2 = make_shared(color(0.4, 0.2, 0.1)); world.add(make_shared(point3(-4, 1, 0), 1.0, material2)); auto material3 = make_shared(color(0.7, 0.6, 0.5), 0.0); world.add(make_shared(point3(4, 1, 0), 1.0, material3)); return world; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ int main() { // Image ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight const auto aspect_ratio = 3.0 / 2.0; const int image_width = 1200; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ const int image_height = static_cast(image_width / aspect_ratio); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight const int samples_per_pixel = 500; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ const int max_depth = 50; // World ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight auto world = random_scene(); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ // Camera ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight point3 lookfrom(13,2,3); point3 lookat(0,0,0); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ vec3 vup(0,1,0); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight auto dist_to_focus = 10.0; auto aperture = 0.1; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ camera cam(lookfrom, lookat, vup, 20, aspect_ratio, aperture, dist_to_focus); // Render std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n"; for (int j = image_height-1; j >= 0; --j) { ... } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Listing [scene-final]: [main.cc] Final scene]
This gives: ![Image 21: Final scene](../images/img-1.21-book1-final.jpg)
An interesting thing you might note is the glass balls don’t really have shadows which makes them look like they are floating. This is not a bug -- you don’t see glass balls much in real life, where they also look a bit strange, and indeed seem to float on cloudy days. A point on the big sphere 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, 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. 2. 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. 3. Surface Textures -- This lets you paste images on like wall paper. Pretty easy and a good thing to do. 4. Solid textures -- Ken Perlin has his code online. Andrew Kensler has some very cool info at his blog. 5. 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. 6. 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. Have fun, and please send me your cool images! (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 in One Weekend” - **Author**: Peter Shirley - **Editors**: Steve Hollasch, Trevor David Black - **Version/Edition**: v3.2.2 - **Date**: 2020-10-31 - **URL (series)**: https://ift.tt/2NsReLd - **URL (book)**: https://ift.tt/2mnbWku Snippets --------- ### Markdown ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [_Ray Tracing in One Weekend_](https://ift.tt/2mnbWku) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ### HTML ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Ray Tracing in One Weekend ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ### LaTeX and BibTex ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~\cite{Shirley2020RTW1} @misc{Shirley2020RTW1, title = {Ray Tracing in One Weekend}, author = {Peter Shirley}, year = {2020}, month = {October}, note = {\small \texttt{https://raytracing.github.io/books/RayTracingInOneWeekend.html}}, url = {https://ift.tt/2mnbWku} } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ### BibLaTeX ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ \usepackage{biblatex} ~\cite{Shirley2020RTW1} @online{Shirley2020RTW1, title = {Ray Tracing in One Weekend}, author = {Peter Shirley}, year = {2020}, month = {October} url = {https://ift.tt/2mnbWku} } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ### 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://ift.tt/2VkRIWx [Steve Hollasch]: https://ift.tt/2VmFYTC [Trevor David Black]: https://ift.tt/37mvWqXfrom Hacker News https://ift.tt/2mnbWku
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.