**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)