**Implementation Details, Advice, and Examples for Assignment 3** Ray Tracing Pipeline ======================================================================== To start, you will complete the higher-level functions of a raytracer to get a better intuition of what a raytracer is doing at a high level, before diving into implementing more of the details. For this part, you will fill in code for the `traceRay` and `calculateColor` functions. I would recommend skimming over the struct definitions located at the top of the `fragmentShader.frag` file before continuing. I would also read over the following sections 1.1 through 1.4 to get more familiar with the code and what you have available to you. Then, go down to the `traceRay` function and follow the steps in the comments. `traceRay` ----- Before filling out the missing parts of the `traceRay` function, you will only see a black scene. At a high level, the `traceRay` function takes a ray and... 1. Traces the ray into the scene to find an intersection using `rayIntersectScene` 2. If no intersection, terminate the ray and return the final result color (do not trace further) 3. Otherwise, compute the color at the point of intersection using `calculateColor` 4. Accumulate the color into a final result color 5. If appropriate, reflect/refract the ray through the intersected object and repeat from step (1) Note that a material is neither refractive nor reflective if its reflectivity is "equal to zero" (read: less than `EPS`). This is a sufficient case for terminating further tracing of the ray! ![](./example-images/initial.png border="1") You can use your mouse and keyboard to interact with the scene. Since plane intersection code is given, we are able to render the walls of the box in this scene with their diffuse material colors. A note on `rayIntersectScene` ----- In `traceRay`, you are asked to use the provided `rayIntersectScene` function. If you look through the GLSL code, you may be wondering why the only reference to it is the following line:
float rayIntersectScene(Ray ray, out Material out_mat, out Intersection out_intersect);
This is a forward declaration of the function, whose body is actually dynamically generated as a string in JavaScript and then appended to the GLSL source code before the GLSL compiler is called, hence the need for the declaration. The reason we do this is purely for performance -- it allows us to hardcode the geometry of the scene into the function, which is more efficient than parsing a XML scene specification directly within GLSL. A note on `EPS` and `INFINITY` ----- At the top of `fragmentShader.frag`, we define a constant called `EPS`, which is set to a small floating point number. Similarly, `INFINITY` is set to a large floating point number. Comparing two floating point numbers for equality is only reliable within some margin due to the IEEE floating point specification, which can't represent all floating point numbers exactly. So to check if two floats are equal, we usually will look for abs(a - b) < EPS. In raytracers, we often do a lot of floating point math in order to compute intersections with objects in the scene as precisely as possible. When we are interested in checking for equality with zero, we will look for abs(a) < EPS to mean "equal to zero". Lastly, we will use "greater than or equal to `INFINITY`" to mean infinity, or out of the bounds of our scene. This will be used within the raytracer to mean that a ray does not intersect anything in the scene. `calculateColor` ----- Currently, the `calculateColor` function just returns the diffuse color associated with the hit material and does not take into account any of the lighting in the scene. Add direct lighting from all of the lights in the scene by looping over the `MAX_LIGHTS` lights in the scene and calling the function `getLightContribution` with the appropriate arguments (see the function signature above `calculateColor`). All of the lights in the scene are stored in a uniform GLSL array called `lights` (i.e. the `i`th light is available at `lights[i]`). Accumulate the contribution of each light in the scene into the variable `outputColor` and return the total, which represents the direct lighting contribution. **NOTE**: Be sure to break your loop if you ever reach `numLights` number of lights. This is because GLSL forbids looping up to this number. (`numLights` is a uniform GLSL variable). After completing this, you should see a brighter box: ![](./example-images/direct.png border="1") Congratulations. You can now fire rays into a scene, intersect with planes, and compute direct lighting within the scene! Ray Intersection ======================================================================== Next, let's add the ability to intersect with other types of objects. For the various ray-object intersection functions, we recommend that you use the provided `findIntersectionWithPlane` and `rayGetOffset` helper functions. Be sure to read through these so you understand how they work, and so that you get an idea of how to implement the other intersection functions. Each intersection function should fill out the `Intersection` object passed into it with information about the intersection, if one exists, by storing the position of the intersection in the `position` field and the normal vector (**normalized** to unit length) at the point of intersection in the `normal` field. Be careful to return the **closest** (i.e. earliest) intersection with the object. You may find the provided helper function `chooseCloserIntersection` useful for this. These functions should return `INFINITY` if the ray does not intersect the object. Note that intersections at time `t` less than `EPS` should not count as true intersections, since these are usually false intersections with the surface that the ray is originating from. Recall that we treat values less than `EPS` as being equivalent to zero. This usually comes up when tracing secondary rays through the scene. *Debugging note:* As you move forward, note that visual artifacts later on are commonly caused by not properly returning the closest intersection or not properly calculating the surface normal at the point of intersection, among other things. Intersecting Triangles, Spheres, and Boxes ----- Refer to lecture slides for the math you should implement to compute these intersections. For triangle intersection, feel free to use any of the 3 approaches discussed in lecture. For axis-aligned box intersection, we recommend the following simple approach: * Create a helper function that takes the bounding points of the box and some arbitrary point and returns whether or not that point is inside the box (**read**: within a distance `EPS` to the box). * Iterate over the sides of the box. For each, intersect with the relevant plane, and then call your helper function to determine whether or not that intersection is within the box. Note the similarity of this strategy to ray-triangle intersection. * Return the closest of these intersections that is greater than `EPS`. Feel free to implement a more optimized approach if you are interested and/or have time. For example, only intersect your ray with the front-facing (i.e. camera-visible) faces of the box. Here's the `default.json` scene with sphere and box intersections. Two of the spheres have reflective/refractive materials (both appear as mirror reflective materials for now, until you implement transmission rays below), while the third is diffuse. The box is also diffuse. ![](./example-images/sphere-box.png border="1") Here's the `mesh.json` scene which showcases triangle intersections. ![](./example-images/tri.png border="1") Intersecting Cylinders and Cones ----- For cylinders and cones, we provide high-level code to guide your approach in `findIntersectionWithCylinder` and `findIntersectionWithCone`, respectively. These functions call helper functions which are left to you to fill in. For the cylinder, we formulate the problem as an open cylinder with 2 discs as its end caps. For the cone, we formulate the problem as an open cone with a single disc as its end cap. Here's the same `default.json` scene, but now with support for cylinder and cone intersections. ![](./example-images/cone-cyl.png border="1") Shadows ===== Hard (Simple) Shadows ----- The function `getLightContribution` that you used earlier returns black (`vec3(0.0, 0.0, 0.0)`) if the point in the scene in question cannot "see" the light (i.e. an object obstructs it). To do so, it calls the function `pointInShadow`, which currently always returns `false`, hence no shadows. Implement the `pointInShadow` function which takes in a 3D position in the scene and a vector towards the light from that position, and returns `true` if the point cannot see the light, and `false` otherwise. **Hint**: You might want to use the `rayIntersectScene` function. ![](./example-images/shadows.png border="1") Soft Shadows ----- To implement soft shadows, you will need to cast multiple rays from the point to **an area** around the light. Randomly sample points with uniform density on the surface of a sphere around the light using the approach described [here](http://mathworld.wolfram.com/SpherePointPicking.html) in order to create your rays. Feel free to experiment with the radius of this sphere around the light. Also feel free to experiment with the number of points that you choose to sample. How many samples do you need in order to get good soft shadows? For each of these randomly sampled points, cast a ray from the point in question to the sampled point. The fraction of the rays that make it through to the point (i.e. can "see" the light) is the fractional contribution of that light to the point in question. **How do I generate a uniformly random number?** Unfortunately, GLSL does not provide any built-in psuedorandom number generators, so you will have to use a simple hash function that behaves like a random number generator. Take a look [here](https://stackoverflow.com/questions/4200224/random-noise-functions-for-glsl) and [here](https://thebookofshaders.com/13/) for some examples and further discussion on implementing randomness and noise. You are welcome to use code from online sources to get "randomness" into your program, but be sure to leave a comment citing where you found it. Implement soft shadows by completing the `softShadowRatio` function, which returns a float instead of a boolean like `pointInShadow` does. This float represents the fractional contribution of the light, which you should then use in your `getLightContribution` function to compute a a more accurate light contribution, rather than simply returning black when the point is in shadow. We provide a global variable `SOFT_SHADOWS` defined at the top that you can use in your `getLightContribution` function to toggle between the simple, hard shadows and the soft shadows. Set it to 0 to mean false and 1 to mean true. You are free to reorganize and modify the `getLightContribution` function as needed to get your soft shadows working. The example below shows soft shadows with various other features also implemented, including transmission rays (refraction) along with some special materials and specular highlights from the Phong lighting model. ![](./example-images/softshadow.png border="1") Transmission Rays (Refraction) ===== Implement transmission rays to handle refraction of rays through materials such as glass by completing the `calcReflectionVector` function. By default, this function reflects the incoming ray regardless of whether the material is reflective or refractive (hence why both spheres in the default scene appear mirror-like, but the left one is actually glass). You should overwrite this behavior to handle refraction. Use [Snell's law](https://en.wikipedia.org/wiki/Snell%27s_law#Vector_form) in its vector form to compute the outgoing refraction direction. When implementing Snell's law, you will need to be mindful of the value of `eta` and the direction of the normal vector. The handling of `eta` is given to you, so that it contains the correct value for when you are inside of the object. Note that the normal vector passed into this function is **already** negated for you if it is inside of the object. See the `traceRay` function again for how and when this happens. **NOTE**: You may **NOT** use GLSL's built-in `refract` function. However, you may find it useful to use this function verify and debug your own implementation. *Debugging tip:* If your glass sphere (left) is black, then you might not be properly returning the **closest valid** intersection for your spheres. In the example below, the left sphere now properly exhibits its glass properties. ![](./example-images/glass.png border="1") Total Internal Reflection ----- You will not likely run into total internal reflection for the provided scenes. However, you should check for it and handle it appropriately. It is acceptable to simply return a zero vector direction for total internal reflection, which essentially kills the ray. Materials ===== Checkerboard ----- The cylinder and the floor use the diffuse checkerboard material. By default, they will be solid colors. Add a checkerboard pattern to these objects by filling out the appropriate block of code in `calculateSpecialDiffuseColor`. A checkerboard pattern can be created by returning one of two colors based on its position in space. You can choose a size for each "tile" in space and then decide what tile the point of intersection is in, which determines which color should be returned. If you follow the approach suggested in the precept slides, you might find it helpful to add an offset of `EPS` to your coordinates before applying the `floor` operation, in order to counter floating-point imprecision when the ray intersects with the plane. Phong ----- Add the specular Phong term to complete the Phong reflection model in the light calculation by filling out the appropriate block of code in `getLightContribution`. Be careful to use normalized vectors when appropriate. Also be careful of negative numbers -- it might be a good idea to clamp values to zero (see the computation for the diffuse component). Don't forget to account for light attenuation -- light falls off over distance. We provide a variable called `attenuation` for this. Special ----- Add a diffuse material of your choice! The back wall of the box in the default scene uses this special material. By default, it is a solid color. One suggestion is a parametric noise texture, such as Perlin noise. You are welcome to use someone else's GLSL implementation of noise, but be sure to cite your sources. Example ----- Here's an example of the default scene using an implementation of a checkerboard material and Perlin noise, along with the Phong term. Notice the specular highlights on the sphere and cone. ![](./example-images/materials.png border="1") Debugging Tips ===== * Speckling or noise are usually caused by floating point imprecision. It may help to add some "padding" of `EPS` to your computations to nudge the numbers in correct direction. * Always check your normal vectors. In general, it is good practice to keep these normalized, since many computations depend on this fact. * Weird shading or patterns on your objects could mean that the normal vectors themselves are not in the correct direction. * Objects appearing black could be caused by normal vectors not being in the correct direction, or the intersections with them not being correct at all. Remember that you should return the closest intersection greater than `EPS` (i.e. the closest intersection that is in front of the ray), when there is more than 1 candidate for intersection. * This list is, of course, not exhaustive. Other Resources for A3 ===== Lectures * ["Rendering & Ray casting"](../../lectures/13-ray.pdf) * ["Lighting & reflectance"](../../lectures/14-light.pdf) * ["Global illumination"](../../lectures/15-global.pdf) Precepts * ["GLSL & Ray tracing I"](../../precepts/07-glsl.pdf) * ["Ray tracing II"](../../precepts/08-raytracer.pdf)