The modern raytracing algorithm was developed by Arthur Appel in 1968. While intuitively one might think of tracing rays from light sources to the camera,
in practice this is not efficient, as most rays from light sources do not reach the camera. Therefore, the common approach is to trace rays from the camera
position \(s\) (center of perspective projection) through the center of each pixel \(P\) of the projection plane raster grid into the scene.
position \(S\) (center of perspective projection) through the center of each pixel \(P\) of the projection plane raster grid into the scene.
Such a ray can be defined as:
\[X(t) = P + t \cdot d \quad \quad t\geq 0 \]
where
\[d = \frac{P - s}{||P - s||}\]
is the normalized direction vector from the camera position \(s\) to the pixel center \(P\).
\[d = \frac{P - S}{||P - S||}\]
is the normalized direction vector from the camera position \(S\) to the pixel center \(P\).
When a ray intersects an object, the color of that pixel is determined based on the material properties of the object and the lighting conditions.
When a ray intersects an object, the color of the intersection point is determined based on the material properties of the object and the lighting conditions.
We often use local illumination models (e.g., Phong, Blinn-Phong) to compute the color \(C_{\text{local}}\) at the intersection point.
To determine the light conditions, a so called <b>shadow ray</b> is cast from the intersection point towards the light sources to
check for light visibility. If the shadow ray intersects another object before reaching the light source, the point is in shadow.
In such case, we either completely skip the color calculations for this pint or only ambient light is considered (if we assume
illumination model with ambient component). Otherwise, the local color contribution is computed using the chosen illumination model.
<br><br>
However, the calculation does not stop here. To account for global illumination effects, additional <b>secondary</b> rays are traced from the intersection point.
These rays can be of different types, depending on the desired effects:
<ul>
<li><b>Reflection rays:</b> These rays are traced in the reflection direction \(R\) of the incident ray
reflected with respect to the surface normal \(N\). This is not to be confused with light reflection vector in Phong lighting model.
The color contribution from the reflection ray is computed recursively by tracing the ray further into the scene.
They are typically computed for most of the objects in the scene.</li>
<li><b>Refraction rays:</b> These rays are traced through transparent or translucent materials to simulate light bending
and transmission. Similar to reflection rays, the color contribution from refraction rays is computed recursively.
However, refraction rays are only computed for transparent or translucent objects.</li>
</ul>
The recursion continues until a maximum recursion depth is reached. A recursion branch can terminate early if the ray does not intersect any objects.
The animation below illustrates the raytracing process with primary, shadow, reflection, and refraction rays up to depth 3.
At each intersection we cast shadow rays to determine light visibility and then proceed to cast reflection
and if applicable (i.e., for transparent objects) refraction rays for transparent objects.
In this way we build a ray-tracing tree, where each node represents a ray intersection and its color contribution.
The edges represent the secondary rays traced from each intersection point.
The final color of the pixel is obtained by combining the color contributions from all the rays traced from the camera through that pixel.
For example, to get the final color \(A\) at the first intersection point in the figure below, we
combine the local color \(C_{\text{localA}}\) with the color contributions from the reflection ray
\(\color{#00B050}C_{R}\) (i.e., point \(B\)) and refraction ray \(\color{#B88C00}C_{T}\) (i.e., point \(C\)). The reflection and refraction contributions are typically weighted
by reflection and refraction coefficients (\(\color{#00B050}k_r\) and \(\color{#B88C00}k_t\)), which represent the material's reflectance and transmittance properties.
Assuming we have the light direction \(L\), surface normal \(N\), and their angle \(\alpha\),
we can compute two new vectors \(\color{#1220A5}u\) and \(\color{#8F3AA9}v\). Vector \(\color{#1220A5}u\) is the projection of \(L\) onto the plane defined by normal \(N\):
\[{\color{#1220A5}u} = {\color{#FF4F4F}L} - {\color{#FB0DF0} N \cos \alpha } ={\color{#FF4F4F}L} - {\color{#FB0DF0} N (N \cdot L) } \]
Vector \(\color{#8F3AA9}v\) is the projection of refraction ray \(T\) onto the normal \(N\) (in opposite direction).
While we do not know the \(T\) ray direction, we know we can express \(\color{#8F3AA9}v\) using cosine of refraction angle \(\alpha'\).
And from Snell's law, we know how to compute \(\cos \alpha'\) using \(\cos \alpha\):
The recursive function <i>traceRay</i> computes the color for a given ray by finding the closest intersection with scene objects,
casts shadow rays to determine light visibility (one ray for each light in the scene), computes local illumination, and traces reflection and refraction rays if applicable:
<b>foreach</b> light in Scene.lights: //cast shadow ray to test light visibility
L = unitVector(light.position - Q)
<b>if</b> L⋅N>0 and firstIntersectionRayScene(Q, L, Scene) is light.position:
lighting += light //add light if its visible from Q
C = localPhongLightingModel(M,Q,-d, normalAt(Q, Scene), lighting) //local illumination
if depth ≥ MAX_DEPTH: return C
C +=M.k<sub>r</sub> ⨀ traceRay(Q, reflectionVector(N,-d), Scene, depth + 1) //reflection
C +=M.k<sub>t</sub> ⨀ traceRay(Q, refractionVector(N,-d,M.n<sub>12</sub>), Scene, depth + 1) //refraction
<b>return</b> C
</code></pre>
</div>
<h2id="improvements">Raytracing improvements</h2>
<h6>Speed-up techniques</h6>
The pseudocode above includes a function <i>firstIntersectionRayScene</i>, which finds the closest intersection of a ray with all objects in the scene.
A naive implementation would test the ray against all objects (i.e., all triangles) in the scene, which can be computationally expensive for complex scenes.
Therefore, various acceleration structures are used to speed up the ray-scene intersection tests.
As a first simple speed-up technique we can use <b>axis-aligned bounding boxes (AABBs)</b> to quickly eliminate large
groups of objects that do not intersect with a given ray. In axis-aligned bounding box, the edges of the box are aligned with the coordinate axes.
The bounding box is defined by two points: the minimum \(L\) and maximum \(H\) corners, which represent the smallest and largest
coordinates of the enclosed object along each axis.
For even better performance, especially in scenes with a large number of objects, we can use hierarchical acceleration structures,
such as <b>Bounding Volume Hierarchies (BVH)</b> or <b>space partitioning</b>.
The idea of a BVH is to recursively group objects into a tree structure, where each node represents a bounding volume that encloses its child nodes or objects.
The root node represents the bounding volume for the entire scene, and the leaf nodes represent individual objects.
The bounding volumes can be AABBs, OBBs, bounding spheres, or other shapes. Each type has its own advantages and disadvantages (e.g.,
tightness of fit vs. computational cost of intersection tests).
The example below illustrates a BVH structure for a simple scene with several objects:
An alsternative approach to BVH is <b>space partitioning</b>, where the 3D space is recursively divided into smaller regions using planes or grids.
Common space partitioning structures include <b>BSP-trees</b> and <b>octrees</b> (resp. quad-tress for 2D scenes) that we have discussed in previous lectures.
During raytracing, the ray is tested against the bounding volumes or space partitions first.
If the ray intersects a bounding volume or partition, we proceed to test the ray against the child nodes or objects within that volume or partition.
If the ray does not intersect the bounding volume or partition, we can skip testing the objects within it, thus reducing the number of intersection tests and improving performance.