|
In this homework, we implemented a renderer that uses a pathtracer algorithm. First, we implemented ray generation and scene intersection. We generated rays and checked if they intersected our scene. Then, we implemented a bounding volume hierarchy to speed up the path tracer. Then, we implemented several lighting options, direct illumination and global illumination. Direct illumination is when there is one source of light, and global illumination is when there is light constantly being reflected. In global illumination, we implemented recursive methods, which was pretty interesting. But the rendering time was very long, since there are thousands of primitives and we are, again, checking light bounces recursively. Finally, we implemented adaptive sampling, to reduce noise without increasing samples.
This homework was pretty interesting because it's how computer graphics are used in a lot of other things, like game engines or films/animation. So seeing it in practice was pretty interesting. It was also very tedious! But our results ended up being very realistic, so it was worth it! A lot of times, when we view objects we don't really consider things like ambient lighting, but once we add that in, we kind of see how our images somehow become a lot more realistic. Very cool.
To generate the rays, we had to take normalized image coordinates and output a ray in the world space. To do this, we followed 3 main steps.
x_n = 2 * a * x_m - a
y_n = 2 * b * y_m - b
To implement raytrace_pixel, we sampled camera rays and traced them through the scene, returning the average of them. We followed these steps:
We had to implement ray-triangle intersection. To do this, we used the Moller Trumbore formula from lecture. Essentially, we used the edges of the triangle and cross/dot products to check if the ray would hit within the bounds of the triangle.
In our code, that means we compute u, v, and t from those products, skip the hit if the determinant is basically zero, and check that u and v stay inside the triangle and t stays between min_t and max_t. If there is a hit, we also get the three vertex normals using u and v.
We could've used something similar to homework 1, where we check if a point is inside a triangle via three line equations, but I think that would've been a bit harder to implement with rays, and also Moller Trumbore seemed like the faster, simpler option.
We had to implement ray-sphere intersection. We used the formulae from lecture to implement this, via the quadratic formula/discriminant. We created a helper that found the solutions and ordered them (from small to large), and used the helper to implement checking for intersections and updating the Intersection data.
The helper gives us the two t values in order and then we pick the first one that lies on the ray segment and use it to set the intersection.
|
|
|
PART 1: We had to construct a BVH from the given vector of primitives and max leaf size configuration. We followed these steps:
PART 2: We had to check if a ray intersected a bounding box and at what points (of entry and exit). We used the equations from lecture for this, the ray and axis-aligned plane intersection formula and the ray and axis-aligned box intersection method. PART 3: We checked if a ray intersects any primitives in the BVH. We followed this basic algorithm:
|
|
|
|
Comparing the maxplanck.dae scene with the CBlucy.dae scene, the rendering times for these moderately complex geometries (the first with tens of thousands of triangles and the second with hundreds of thousands of triangles), we can see that the rendering times differ greatly with and without BVH acceleration. For the maxplanck.dae scene, the rendering time with BVH acceleration was 0.0373s. Without BVH accleration, the rendering time was 11.1565s. For the CBlucy.dae scene, the rendering time with BVH acceleration was 0.0225s. Without BVH acceleration, the rendering time was 60.9923s. For the cow.dae scene, the rendering time with BVH acceleration was 0.0302s. Without BVH acceleration, the rendering time was 1.3293s. For the blob.dae scene, the rendering time with BVH acceleration was 0.0468s. Without BVH acceleration, the rendering time was 104.7974s. From these results, we can observe that rendering with BVH acceleration decreases rendering time by at least a multiple of 2. Rendering without BVH acceleration siginificantly lengthens the rendering time, especially with scenes that are composed of moderately complex geometries such as maxplanck.dae and CBlucy.dae.
We had two different implementations of the direct lighting function.
| Uniform Hemisphere Sampling | Light Sampling |
|---|---|
|
|
|
|
|
|
|
|
Here, we display the CBbunny scene with at least one area light. Under light sampling, the noise levels in soft shadows when rendering decreases in area as the number of light rays increases.
The results between uniform hemisphere sampling and lighting sampling is in uniform hemisphere sampling, since rays are distributed fairly in all directions no matter where the light source is located. This diminishes illumination and causes the image to look darker, with more shadows, and more noise. Compared to lighting sampling, lighting sampling specifically direct rays towards where the light source is located which effectively takes in the direct illumination. This causes a brighter image with shadows that are not overpowering and reduces noise. The distribution of rays while taking into account the location of the light source is the main difference between uniform hemisphere sampling and lighting sampling. By using lighting sampling, images will look much brighter and higher quality.
one_bounce_radiance (direct lighting at this hit).depth is at most 1, we stop and return that only (no more indirect bounces).sample_f on the BSDF to get an incoming direction wi, the PDF, and the BRDF value f. We build a new ray in world space from the hit point (with a small epsilon offset) and set its depth to r.depth - 1.at_least_one_bounce_radiance to get Li at the next surface, and add the indirect term f * Li * cos(theta) / pdf (using the cosine of wi in local coordinates). If the bounce misses, we keep only the direct term we already computed.isAccumBounces is true, we add that indirect term to the direct lighting; if false, we replace the result with just the indirect term (for the “single bounce only” mode).
|
|
|
|
Here, direct illumination is more brighter whereas indirect illumination has softer color bleeding in the ceiling.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Here, we see that as max_ray_depth increase, the image gets darker. There is more indirect lighting and softer shadows. The rendering time also increases due to the amount of additional bounces needed.
|
|
|
|
|
|
|
|
|
|
|
When samples per pixel is 1, the image has a lot of noise. As the samples per pixel increases, variance is halved at samples per pixel = 2 and moving forward. The image improves in quality, looking more smooth, less noise, and more clean. This shows the impact of convergence rate of Monte Carlo.
Adaptive sampling is when we try to stop continuing rays on pixels that are already considered steady and continue sampling rays that are noisy instead to improve the quality of the image and reduce noise. The implementation was to take several camera rays per pixel and stopping once the pixel color gets steady. For every sample, radiance is calculated and brightness is tracked to estimate mean and variance. Every other sample, we check the noise to make sure it's small enough compared to mean. If it is, stop sampling, if not, continue sampling. Lastly, samples are averaged.
|
|
|
|