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
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ set(sources
src/scene.cpp
src/preview.cpp
src/utilities.cpp
src/gltf-loader.cc
)

list(SORT headers)
Expand Down
331 changes: 0 additions & 331 deletions INSTRUCTION.md

This file was deleted.

125 changes: 119 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,126 @@
CUDA Path Tracer
================

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

* (TODO) YOUR NAME HERE
* Tested on: (TODO) Windows 22, i7-2222 @ 2.22GHz 22GB, GTX 222 222MB (Moore 2222 Lab)
* Jiarui Yan
* [LinkedIn](https://www.linkedin.com/in/jiarui-yan-a06bb5197?lipi=urn%3Ali%3Apage%3Ad_flagship3_profile_view_base_contact_details%3BvRlITiOMSt%2B9Mgg6SZFKDQ%3D%3D), [personal website](https://jiaruiyan.pb.online/), [twitter](https://twitter.com/JerryYan1997), etc.
* Tested on: Windows 10 Home, i7-9700K @ 3.60GHz 16GB DDR4 RAM, RTX 2070 SUPER 8GB Dedicated GPU memory (Personal desktop)

### (TODO: Your README)

*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.
## Features

* Refraction with Frensel effects

* Physically-based depth-of-field

* Stochastic Sampled Antialiasing

* Arbitrary mesh loading and rendering with tinygltf

* Better hemisphere sampling methods (Jittered)

* Texture mapping

* Direct lighting

* Path continuation/termination using Stream Compaction

* Material sort

## Result (Small Yellow Duck)

![pic 1](./img/title_image.png)

### Refraction with Frensel effects

For refraction with Frensel effects, we want a refractive material to have reflection effects because in our life, material normally has both reflective effect and refractive effect. Here is an example for the glass ball. As we can see, we can see reflection on the glass ball from the light source and the focused light on the floor and refraction of the red wall can also be seen on this ball.

![pic 1](./img/refl_refra.png)

### Physically-based depth-of-field

For the depth of field, it uses the thin lens camera model from [PBRT 6.2.3]. Here in the example, I gradually increase the focal length. At first, it is not clear because the focal length is in front of the duck model. Then, the duck becomes clear as the focal length is increased. Finally, it becomes unclear again as the focal length gets away from the suitable one. Besides, in order to make this process clear, this rendering is done on direct light integrator, which can give us a relative good result quickly.

![pic 1](./img/DOF_Duck.gif)

### Stochastic Sampled Antialiasing

As for antialiasing, this project use Stochastic Sampled Antialiasing method. It basically means for each iteration, shooting light from the camera would be jittered by a random number. In this way, undesirable artifacts can be avoided. Here is an example. As we can see, the left hand side rendering has less artifacts than the right hand sided one. In order to make it more obvious, let's take a look at the right wall.

![pic 1](./img/anti_aliasing_overall.png)

As we can see from the zoomed view of the right wall, the right hand side rendering has clear pixel like wall, while the left hand side one is much more smooth than its counterpart. It is achieved by Stochastic Sampled Antialiasing, because for each pixel it can average a little bit different color from adjacency instead of color from one hit point.

![pic 1](./img/anti_aliasing_detail_comparsion.png)

### Arbitrary mesh loading and rendering with tinygltf

For this project, I use gltf2.0 as my mesh format. Meanwhile, I also use a thrid party library named "tinygltf" to facilicate my work. Here is an example of loading an arbitrary mesh. All the models and textures used in this project are got from the official github page of the gltf2.0.

Another things needs to be noted is that I put the absolute path into the scene file for loading a target mesh. Therefore, if you want to build this project on your machine and try your own 3D models, please remember change these paths in the scene file. Besides, I didn't include mine in this project and they are just from the offical site and they have same names.

![pic 1](./img/arbitrary_mesh.png)

### Better hemisphere sampling methods (Jittered)

Besides, I also implement two sampling methods in this project. They are Jittered sampling and Random sampling. Jittered sampling's sampling directions would concentrate more to the top of hemisphere, which can help integrator get more good sampling information by comare to the totally random one. Here is an comparsion between them. As we can see, the Jittered one's shadow is relative small. Meanwhile, although they have difference at first, they are almost same finally.

![pic 1](./img/sampling_method.png)

### Texture mapping

As for texturing mapping, tinygltf has already prodvide useful API to load .gltf files with textures. Besides, .gltf file would also provide UV information for triangles. Therefore, we can map texture color on the mesh by finding the corresponding pixel indices calculated from UV coordinates.

![pic 1](./img/texture_mapping.png)

### Direct lighting

As you can see from the rendering before, there are some relatively unclear images with lots of noise. They are produced by navie integrator. One good thing about the navie integrator is that it can provude global illumation. However, owing to the fact that a path would bounce in the scene for lots of times before it hits the light source, it also takes lots of time to get a clear rendering result. Normally, in order to get a clear result, it will take 3000 iterations. However, for direct lighting integrator, 30 iterations is normally enough, because at first bounce, it directly finds a point on the light source. As a result, it is really fast but it doesn't have indirect light color. Here is an example compares their results when the render only runs 30 iterations.

![pic 1](./img/integrators.png)

## Performance analysis

### Why material sort can affect rendering performance

Material sort can be beneficial to the performance because expensive BSDF computations lead to longer computation for some rays. As a result, the rays' BSDF computation that takes less time would just wait for the expensive ones. Therefore, sort them can help performance. However, according to my experiment, when I conduct tests for app with material sort and without matierial sort under 3000 iterations, the material sort one takes 104709 milliseconds, and the no material sort one takes 50011
milliseconds. I think this is caused by the fact that BSDF computation now is happened in a large kernel and it is done by if-else branches, which means different BSDF computation now just have no difference.

### Cache first bounce influence for different max ray depths

The experiments are conducted on the same number of iterations which is 3000 iterations for different max ray depths.

![pic 1](./img/Performance.PNG)

As we can see from the graph above, with more depth, the performance is natually going to be bad. Besides, the experiments using first bounce cache always perform better than their counterparts that don't use it.

### Performance gain for bounding box of the mesh

The experiments are confucted on the duck scenario with different iterations. The integrator here is naive integrator.

![pic 1](./img/BoundingBoxPerformance.PNG)

As we can see from the analysis above, with more iterations the performance gain of the bounding box is more appearant. This is caused by the fact that for each iteration the bounding box can contribute a little bit performance gain. Therefore, after several iterations, the gain would be appearant.

### Effects of stream compaction within a single iteration

Here, I am going to explore the effect of stream compaction within a single iteration. This method is benefical to the perforamnce, because it can significantly reduce the number of tested path within an iteration. Here is a plot to illustrate it.

![pic 1](./img/RemainLightNum.PNG)

As we can see from the plot above, the number of remaining light that pass the stream compation early termination would be smaller and smaller. Finally, there will be no light that needs interestion or shading tests. So, we can conclude that with stream compaction can be really helpful to the scenarios that require several depth.

### Different effects of an open scene and a closed scene under stream compaction

For different scene, the early termination may has different effect. For an open scene, a light is easier to bounce out of the scene than an closed scene. Therefore, we can expect that even though the number of light at first would be same, the number of remaining light in the open scene would be less than the counterpart number in the closed scene. Here is the graph illustrates this.

![pic 1](./img/RemainLightNumDiffScene.PNG)

## Blooper

![pic 1](./img/blooper.png)

## Third party software

* [tinygltf](https://github.com/syoyo/tinygltf/)
Binary file added data/data.xlsx
Binary file not shown.
Binary file added data/data2.xlsx
Binary file not shown.
166 changes: 166 additions & 0 deletions external/include/gltf-loader.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
#ifndef EXAMPLE_GLTF_LOADER_H_
#define EXAMPLE_GLTF_LOADER_H_

#include <stdexcept>
#include <string>
#include <vector>

#include "material.h"
#include "mesh.h"

namespace example {

/// Adapts an array of bytes to an array of T. Will advace of byte_stride each
/// elements.
template <typename T>
struct arrayAdapter {
/// Pointer to the bytes
const unsigned char *dataPtr;
/// Number of elements in the array
const size_t elemCount;
/// Stride in bytes between two elements
const size_t stride;

/// Construct an array adapter.
/// \param ptr Pointer to the start of the data, with offset applied
/// \param count Number of elements in the array
/// \param byte_stride Stride betweens elements in the array
arrayAdapter(const unsigned char *ptr, size_t count, size_t byte_stride)
: dataPtr(ptr), elemCount(count), stride(byte_stride) {}

/// Returns a *copy* of a single element. Can't be used to modify it.
T operator[](size_t pos) const {
if (pos >= elemCount)
throw std::out_of_range(
"Tried to access beyond the last element of an array adapter with "
"count " +
std::to_string(elemCount) + " while getting elemnet number " +
std::to_string(pos));
return *(reinterpret_cast<const T *>(dataPtr + pos * stride));
}
};

/// Interface of any adapted array that returns ingeger data
struct intArrayBase {
virtual ~intArrayBase() = default;
virtual unsigned int operator[](size_t) const = 0;
virtual size_t size() const = 0;
};

/// Interface of any adapted array that returns float data
struct floatArrayBase {
virtual ~floatArrayBase() = default;
virtual float operator[](size_t) const = 0;
virtual size_t size() const = 0;
};

/// An array that loads interger types, returns them as int
template <class T>
struct intArray : public intArrayBase {
arrayAdapter<T> adapter;

intArray(const arrayAdapter<T> &a) : adapter(a) {}
unsigned int operator[](size_t position) const override {
return static_cast<unsigned int>(adapter[position]);
}

size_t size() const override { return adapter.elemCount; }
};

template <class T>
struct floatArray : public floatArrayBase {
arrayAdapter<T> adapter;

floatArray(const arrayAdapter<T> &a) : adapter(a) {}
float operator[](size_t position) const override {
return static_cast<float>(adapter[position]);
}

size_t size() const override { return adapter.elemCount; }
};

#pragma pack(push, 1)

template <typename T>
struct v2 {
T x, y;
};
/// 3D vector of floats without padding
template <typename T>
struct v3 {
T x, y, z;
};

/// 4D vector of floats without padding
template <typename T>
struct v4 {
T x, y, z, w;
};

#pragma pack(pop)

using v2f = v2<float>;
using v3f = v3<float>;
using v4f = v4<float>;
using v2d = v2<double>;
using v3d = v3<double>;
using v4d = v4<double>;

struct v2fArray {
arrayAdapter<v2f> adapter;
v2fArray(const arrayAdapter<v2f> &a) : adapter(a) {}

v2f operator[](size_t position) const { return adapter[position]; }
size_t size() const { return adapter.elemCount; }
};

struct v3fArray {
arrayAdapter<v3f> adapter;
v3fArray(const arrayAdapter<v3f> &a) : adapter(a) {}

v3f operator[](size_t position) const { return adapter[position]; }
size_t size() const { return adapter.elemCount; }
};

struct v4fArray {
arrayAdapter<v4f> adapter;
v4fArray(const arrayAdapter<v4f> &a) : adapter(a) {}

v4f operator[](size_t position) const { return adapter[position]; }
size_t size() const { return adapter.elemCount; }
};

struct v2dArray {
arrayAdapter<v2d> adapter;
v2dArray(const arrayAdapter<v2d> &a) : adapter(a) {}

v2d operator[](size_t position) const { return adapter[position]; }
size_t size() const { return adapter.elemCount; }
};

struct v3dArray {
arrayAdapter<v3d> adapter;
v3dArray(const arrayAdapter<v3d> &a) : adapter(a) {}

v3d operator[](size_t position) const { return adapter[position]; }
size_t size() const { return adapter.elemCount; }
};

struct v4dArray {
arrayAdapter<v4d> adapter;
v4dArray(const arrayAdapter<v4d> &a) : adapter(a) {}

v4d operator[](size_t position) const { return adapter[position]; }
size_t size() const { return adapter.elemCount; }
};

///
/// Loads glTF 2.0 mesh
///
bool LoadGLTF(const std::string &filename, float scale,
std::vector<Mesh<float> > *meshes,
std::vector<Material> *materials, std::vector<Texture> *textures);

} // namespace example

#endif // EXAMPLE_GLTF_LOADER_H_
Loading