Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 70 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,76 @@ CUDA Denoiser For CUDA Path Tracer

**University of Pennsylvania, CIS 565: GPU Programming and Architecture, Project 4**

* (TODO) YOUR NAME HERE
* Tested on: (TODO) Windows 22, i7-2222 @ 2.22GHz 22GB, GTX 222 222MB (Moore 2222 Lab)
* Lindsay Smith
* [LinkedIn](https://www.linkedin.com/in/lindsay-j-smith/), [personal website](https://lindsays-portfolio-d6aa5d.webflow.io/).
* Tested on: Windows 10, i7-11800H 144Hz 16GB RAM, GeForce RTX 3060 512GB SSD (Personal Laptop)

### (TODO: Your README)
This project builds upon the [Pathtracer](https://github.com/lsmith24/Project3-CUDA-Path-Tracer) I previously implemented by adding a denoiser to provide clearer images. The algorithm used
to do this denoising is detailed in ["Edge-Avoiding À-Trous Wavelet Transform for fast Global Illumination Filtering"](https://jo.dreggn.org/home/2010_atrous.pdf).
The idea is to blur the image while preserving the edges. This will give the appearance of a denoised image because large noisy areas will get smoothed over, but the edges
and shapes will remain intact. We can use the position, normals, and color of the scene elements to apply weights to the blur that will determine how much denoising should be applied. Although a true Gaussian blur is effective, it requires a large amount of computation. By implementing the À-Trous Wavelet transform we are able to spread out
the coordinates of the Gaussian kernel. This results in a larger blur and far fewer iterations than a true Gaussian.

*DO NOT* leave the README to the last minute! It is a crucial part of the
project, and we will not be able to grade you without a good README.
Here we can see the difference between the images where there is no denoising, a blur with no edge detection, and the blur with edge detection. All of these images
went through 10 iterations of the pathtracer.

| raw pathtraced image | simple blur | blur guided by G-buffers |
|---|---|---|
|![](img/noDenoise10samples.png)|![](img/myBlur.png)|![](img/myDenoised.png)|

We can also see here the visualization of the position and normals of the objects. This gives us some more insight into how the per-pixel normals and positions are
able to help us with the weights for edge detection.

| per-pixel normals | per-pixel position |
|---|---|
|![](img/myNormals.png)|![](img/myPosition.png)|

# Performance Analysis

By timing our regular pathtracer, as well as our denoiser we are able to see how varying different aspects of the pathtracer and denoiser affect runtime. The first thing I looked at was filter size. The filter size of the denoiser increases the time it takes for the denoiser to run. This makes sense because the denoiser will have to consider a larger area of the image each time. The graph is a logarithmic curve due to the fact that the À-Trous algorithm depends on a log2 of the filter size.

![](img/filterGraph.png)

The next thing I looked at was how the resolution impacts performance. We can see that as the resolution increases the runtime also increases. This makes sense because an image with a higher resolution will have more pixels that the pathtracer and denoiser need to look at with each iteration.

![](img/resolutionGraph.png)

I also found that on average the denoiser adds about 4ms to the time it takes to produce the image. It is interesting though that we can produce nice images overall much faster with the denoiser because the number of iterations of the pathtracer can be brought down substantially. It is a bit hard to quanitify this because the number of iterations needed to produce a good image depends heavily on the complexity of the scene.

## Qualitative Analysis

Another interesting thing to note is that the appearance of our image does not scale uniformly with filter size. The difference in the images when using a large filter size
becomes essentially non-existent. We can see from these images that the difference between a filter of size 20 and size 50 is much more substantial than the difference between a filter size of 100 to 120.

| filter size = 20 | filter size = 50 | filter size = 100 | filter size = 120 |
|---|---|---|---|
|![](img/filter20.png)|![](img/filter50.png)|![](img/filter100.png)|![](img/filter120.png)|

If we continue to increase the filter size even further we start to see no difference at all.

| filter size = 200 | filter size = 300 |
|---|---|
|![](img/filter200.png)|![](img/filter300.png)|

Another thing we can see through looking at the images produced is that certain materials are much more compatible with the denoiser. For example, a diffused surface works
very well with the denoiser because the surface is essentially one color. The surface getting blurred is effective at making it look better, and there is not really any information being lost. This is not the case however with refractive and specular materials. These materials generally create specific reflections of light or the surrounding scene. When these reflections get blurred out by the denoiser they no longer truly look like the material they are supposed to be.This is especially noticeable in the refractive material objects where the contrast between the object and the reflections are not as great and the entire interior of the object gets blurred. It no longer looks like glass because the visuals that make it refractive are not seen.

It is also interesting to note that with a more complicated scene it takes more than 10 iterations of the pathtracer to be able to produce a good denoised image. To obtain this denoised result I required 100 iterations of the pathtracer first. Of course this is still significantly less iterations than the 5,000 we used to produce images with just the Pathtracer. We can see the difference that more iterations of the pathtracer make in these images. Both use a filter size of 200.

| iterations = 10 | iterations = 100 |
|---|---|
|![](img/iter10.png)|![](img/iter100.png)|

We can also see how the amount of light in the scene impacts the effectiveness of the denoiser. A darker scene makes it much more difficult for the denoiser to produce a good image. The same filter size, iterations, and weights were used to produce both of these images, but the lack of light in the second one makes it appear much noisier.

| Good Lighting | Poor Lighting |
|---|---|
|![](img/goodLighting.png)|![](img/badLighting.png)|

## Bloopers

Incorrect clamp value for position and normals

| Position | Normals |
|---|---|
|![](img/positionBlooper.png)|![](img/NormalsBlooper.png)|
Binary file added img/NormalsBlooper.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/anti_aliasing_cover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/badLighting.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/filter100.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/filter120.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/filter20.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/filter200.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/filter300.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/filter50.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/filterGraph.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/goodLighting.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/highPosWeight.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/iter10.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/iter100.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/iter30.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/myBlur.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/myDenoised.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/myNormals.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/myPosition.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/noDenoise10samples.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
Binary file added img/positionBlooper.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/resolutionGraph.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion scenes/cornell.txt
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ EMITTANCE 0
CAMERA
RES 800 800
FOVY 45
ITERATIONS 5000
ITERATIONS 30
DEPTH 8
FILE cornell
EYE 0.0 5 10.5
Expand Down
138 changes: 120 additions & 18 deletions src/interactions.h
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
#pragma once

#include "intersections.h"
#include "glm/glm.hpp"
#include "glm/gtx/norm.hpp"

// CHECKITOUT
/**
* Computes a cosine-weighted random direction in a hemisphere.
* Used for diffuse lighting.
*/
__host__ __device__
glm::vec3 calculateRandomDirectionInHemisphere(
glm::vec3 normal, thrust::default_random_engine &rng) {
glm::vec3 normal, thrust::default_random_engine& rng) {
thrust::uniform_real_distribution<float> u01(0, 1);

float up = sqrt(u01(rng)); // cos(theta)
Expand All @@ -23,9 +26,11 @@ glm::vec3 calculateRandomDirectionInHemisphere(
glm::vec3 directionNotNormal;
if (abs(normal.x) < SQRT_OF_ONE_THIRD) {
directionNotNormal = glm::vec3(1, 0, 0);
} else if (abs(normal.y) < SQRT_OF_ONE_THIRD) {
}
else if (abs(normal.y) < SQRT_OF_ONE_THIRD) {
directionNotNormal = glm::vec3(0, 1, 0);
} else {
}
else {
directionNotNormal = glm::vec3(0, 0, 1);
}

Expand All @@ -41,22 +46,119 @@ glm::vec3 calculateRandomDirectionInHemisphere(
}

/**
* Simple ray scattering with diffuse and perfect specular support.
* Scatter a ray with some probabilities according to the material properties.
* For example, a diffuse surface scatters in a cosine-weighted hemisphere.
* A perfect specular surface scatters in the reflected ray direction.
* In order to apply multiple effects to one surface, probabilistically choose
* between them.
*
* The visual effect you want is to straight-up add the diffuse and specular
* components. You can do this in a few ways. This logic also applies to
* combining other types of materias (such as refractive).
*
* - Always take an even (50/50) split between a each effect (a diffuse bounce
* and a specular bounce), but divide the resulting color of either branch
* by its probability (0.5), to counteract the chance (0.5) of the branch
* being taken.
* - This way is inefficient, but serves as a good starting point - it
* converges slowly, especially for pure-diffuse or pure-specular.
* - Pick the split based on the intensity of each material color, and divide
* branch result by that branch's probability (whatever probability you use).
*
* This method applies its changes to the Ray parameter `ray` in place.
* It also modifies the color `color` of the ray in place.
*
* You may need to change the parameter list for your purposes!
*/

__host__ __device__
float schlickEquation(float ior, float n, float cos) {
float r0 = (n - ior) / (n + ior);
r0 = r0 * r0;
return r0 + (1.f - r0) * glm::pow(1.f - cos, 5.f);
}

//__host__ __device__
//bool refractHelper(Ray& ray, glm::vec3& normal, glm::vec3& refract, float n) {
// /*glm::vec3 normalized = glm::normalize(ray.direction);
// float dot = glm::dot(normalized, normal);*/
//
// float d = 1.0 - n * n * (1.0 - dot * dot);
// if (d >= 1.f) {
// refract = n * (normalized - normal * dot) - normal * glm::sqrt(d);
// return true;
// }
// return false;
//}

__host__ __device__
void refractScatter(PathSegment& path, const Material& m, glm::vec3 intersect, glm::vec3 normal, thrust::default_random_engine& rng) {
thrust::uniform_real_distribution<float> u01(0, 1);
float num = u01(rng);
float n = 1.f;
float probability;
glm::vec3 normal2 = normal;
float ior = m.indexOfRefraction;

float cos = glm::clamp(glm::dot(path.ray.direction, normal), -1.f, 1.f);

if (cos >= 0.f) {
normal2 = -normal;
n = ior;
ior = 1.f;
}
else {
cos = glm::abs(cos);
}

glm::vec3 reflect = glm::normalize(glm::reflect(path.ray.direction, normal2));
float x = n / ior;
float sin = glm::sqrt(glm::max(0.f, 1.f - cos * cos));

if (x * sin < 1.f) {
//schlick equation
probability = schlickEquation(ior, n, cos);

if (num < probability) {
path.ray.direction = reflect;
}
else {
path.ray.direction = glm::refract(path.ray.direction, normal2, x);
}
}
else {
path.ray.direction = reflect;
}

path.ray.origin = intersect + (path.ray.direction * 0.01f);
path.color *= m.specular.color;
}

__host__ __device__
void scatterRay(
PathSegment & pathSegment,
glm::vec3 intersect,
glm::vec3 normal,
const Material &m,
thrust::default_random_engine &rng) {
glm::vec3 newDirection;
if (m.hasReflective) {
newDirection = glm::reflect(pathSegment.ray.direction, normal);
} else {
newDirection = calculateRandomDirectionInHemisphere(normal, rng);
}

pathSegment.ray.direction = newDirection;
pathSegment.ray.origin = intersect + (newDirection * 0.0001f);
PathSegment& pathSegment,
glm::vec3 intersect,
glm::vec3 normal,
const Material& m,
thrust::default_random_engine& rng) {

glm::vec3 dir_diffuse = calculateRandomDirectionInHemisphere(normal, rng);
glm::vec3 dir_specular = glm::normalize(glm::reflect(pathSegment.ray.direction, normal)); //not sure if have to normalize here

//specular
if (m.hasReflective > 0) {
pathSegment.ray.direction = dir_specular;
pathSegment.ray.origin = intersect + 0.0001f * normal;
pathSegment.color *= m.specular.color;
}
else if (m.hasRefractive > 0) {
//refractive (glass, water, etc)
refractScatter(pathSegment, m, intersect, normal, rng);
}
else {
//diffuse
pathSegment.ray.direction = dir_diffuse;
pathSegment.ray.origin = intersect + 0.0001f * normal;
pathSegment.color *= m.color;
}
}
32 changes: 27 additions & 5 deletions src/intersections.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
#include "sceneStructs.h"
#include "utilities.h"

#define BOUNDING_BOX 0

/**
* Handy-dandy hash function that provides seeds for random number generation.
*/
Expand All @@ -19,6 +21,7 @@ __host__ __device__ inline unsigned int utilhash(unsigned int a) {
return a;
}

// CHECKITOUT
/**
* Compute a point at parameter value `t` on ray `r`.
* Falls slightly short so that it doesn't intersect the object it's hitting.
Expand All @@ -34,6 +37,7 @@ __host__ __device__ glm::vec3 multiplyMV(glm::mat4 m, glm::vec4 v) {
return glm::vec3(m * v);
}

// CHECKITOUT
/**
* Test intersection between a ray and a transformed cube. Untransformed,
* the cube ranges from -0.5 to 0.5 in each axis and is centered at the origin.
Expand All @@ -44,9 +48,9 @@ __host__ __device__ glm::vec3 multiplyMV(glm::mat4 m, glm::vec4 v) {
* @return Ray parameter `t` value. -1 if no intersection.
*/
__host__ __device__ float boxIntersectionTest(Geom box, Ray r,
glm::vec3 &intersectionPoint, glm::vec3 &normal, bool &outside) {
glm::vec3& intersectionPoint, glm::vec3& normal, bool& outside) {
Ray q;
q.origin = multiplyMV(box.inverseTransform, glm::vec4(r.origin , 1.0f));
q.origin = multiplyMV(box.inverseTransform, glm::vec4(r.origin, 1.0f));
q.direction = glm::normalize(multiplyMV(box.inverseTransform, glm::vec4(r.direction, 0.0f)));

float tmin = -1e38f;
Expand Down Expand Up @@ -87,6 +91,7 @@ __host__ __device__ float boxIntersectionTest(Geom box, Ray r,
return -1;
}

// CHECKITOUT
/**
* Test intersection between a ray and a transformed sphere. Untransformed,
* the sphere always has radius 0.5 and is centered at the origin.
Expand All @@ -97,7 +102,7 @@ __host__ __device__ float boxIntersectionTest(Geom box, Ray r,
* @return Ray parameter `t` value. -1 if no intersection.
*/
__host__ __device__ float sphereIntersectionTest(Geom sphere, Ray r,
glm::vec3 &intersectionPoint, glm::vec3 &normal, bool &outside) {
glm::vec3& intersectionPoint, glm::vec3& normal, bool& outside) {
float radius = .5;

glm::vec3 ro = multiplyMV(sphere.inverseTransform, glm::vec4(r.origin, 1.0f));
Expand All @@ -121,10 +126,12 @@ __host__ __device__ float sphereIntersectionTest(Geom sphere, Ray r,
float t = 0;
if (t1 < 0 && t2 < 0) {
return -1;
} else if (t1 > 0 && t2 > 0) {
}
else if (t1 > 0 && t2 > 0) {
t = min(t1, t2);
outside = true;
} else {
}
else {
t = max(t1, t2);
outside = false;
}
Expand All @@ -139,3 +146,18 @@ __host__ __device__ float sphereIntersectionTest(Geom sphere, Ray r,

return glm::length(r.origin - intersectionPoint);
}

__host__ __device__ float triangleIntersectionTest(Geom triangle, Ray r, glm::vec3& intersectionPoint, glm::vec3& normal, bool& outside) {
glm::vec3 pt1 = glm::vec3(triangle.transform * glm::vec4(triangle.triangle.pt1.pos, 1.0f));
glm::vec3 pt2 = glm::vec3(triangle.transform * glm::vec4(triangle.triangle.pt2.pos, 1.0f));
glm::vec3 pt3 = glm::vec3(triangle.transform * glm::vec4(triangle.triangle.pt3.pos, 1.0f));

glm::vec3 inter;
bool intersects = glm::intersectRayTriangle(r.origin, r.direction, pt1, pt2, pt3, inter);
if (!intersects) return -1.f;

float z = 1.0f - inter.x - inter.y;
intersectionPoint = inter.x * pt1 + inter.y * pt2 + z * pt3;
normal = glm::normalize(glm::cross(pt2 - pt1, pt3 - pt1));
return inter.z;
}
Loading