Update gitattributes
[opengl.git] / assignment-5 / report.latex
1 \documentclass{article}
2 \usepackage{fullpage}
3 \usepackage{graphicx}
4 \usepackage{minted}
5 \begin{document}
6 \title{CS4052 Project - Physically Based Rendering}
7 \author{Luke Lau 15336810}
8 \maketitle
9
10
11 \section{Physically Based Rendering}
12 The project uses a physically based lighting model, which computes per pixel lighting by estimating
13 how light rays interact with surfaces at a microscopic level, called the microfacet model. 
14
15 \begin{center}
16 \includegraphics[width=7cm]{microfacets_light_rays}
17 \includegraphics[width=3.5cm]{surface_reaction}
18 \end{center}
19
20 There are two main properties that determine a material's appearance: roughness and metalness. 
21 The former determines how much light gets caught in the bumps of the surface and doesn't get
22 reflected back. The latter is used for approximating how metals absorb refracted light without
23 scattering.
24
25 \begin{center}
26 \end{center}
27
28 There are 7 texture maps used when rendering each material. 4 are defined in the model and 3 are
29 precomputed at the start of the program.
30 \begin{minted}{glsl}
31 uniform sampler2D albedoMap;
32 uniform sampler2D normalMap;
33 uniform sampler2D metallicRoughnessMap;
34 uniform sampler2D aoMap;
35 uniform samplerCube irradianceMap;
36 uniform samplerCube prefilterMap;
37 uniform sampler2D brdfMap;
38 \end{minted}
39
40 The Albedo map provides the base colour underneath the diffuse and specular layers, the normal
41 map is just a standard normal map, and similarly for the ambient occlusion (ao) map.
42
43 The model in the program is loaded from the GlTF2 format, a trendy new model format designed for
44 easy transportation of physically based models and materials, maintained by the Khronos group (the
45 same group behind OpenGL and Vulkan).
46 Because it is physically based, it defines a metallic roughness map texture.
47 The metallic value is stored in the blue channel of the texture whilst the roughness value is stored in the green channel (Inside the source code there is a small python script,
48 \texttt{metallicRoughness.py} which combines two separate metallic/roughness maps into one suitable
49 for the GlTF2 format. AssImp recently added partial support for GlTF2 models so I was able to read
50 in the metallic/roughness texture into a 2 channel texture, and access it from within the shader
51 like so:
52
53 \begin{minted}{glsl}
54 float metallic = texture(metallicRoughnessMap, texCoords).b;
55 float roughness = texture(metallicRoughnessMap, texCoords).g;
56 \end{minted}
57
58 The irradiance, prefilter and BRDF maps are all precomputed based off the skybox in order to provide
59 accurate, image based lighting (IBL) that correctly reflects the contents of the environment. The
60 skybox itself is loaded in from an equirectangular HDR file, and so needs to be gamma corrected and
61 converted to a cubemap first. \texttt{skyboxfrag.glsl} handles the gamma
62 correction and \texttt{equirectangularfrag.glsl} takes care of converting the projection. 
63
64 It needs to be in the HDR format since the precomputed maps are based off of physical properties,
65 and the equations that are involved work in terms of flux. 
66
67 \begin{figure}
68         \begin{equation}
69         L_0(p,\omega_0) =
70         \int_\Omega(k_d \frac{c}{\pi} +
71                                 k_s \frac{DFG}{4(\omega_0 \cdot n)
72                                                                 (\omega_i \cdot n)})
73                            L_i(p,\omega_i)n \cdot \omega_i d\omega_i
74         \end{equation}
75         \label{eq:reflectance}
76         \caption{The reflectance equation}
77 \end{figure}
78
79 The irradiance map solves the $k_d \frac{c}{\pi}$ (diffuse) part of figure~\ref{eq:reflectance}, and is
80 calculated in \texttt{irradiancefrag.glsl}. It approximates the integral by taking a finite number
81 of samples, and does this for each possible incoming angle of light, storing the result into a
82 cubemap texture that can easily be looked up later.
83
84 The same idea applies for the prefilter and BRDF maps: solve a computationally expensive integral
85 and shove it into a texture that can be quickly referenced in the main fragment shader. They
86 precompute the integrals in the $k_s$ (specular) side of the figure~\ref{eq:reflectance}, but unlike
87 the $k_d$ term, this term is not constant and depends on the viewing angle, $\omega_i$. It would be
88 crazy to precompute this integral for every single combination of viewing angle and incoming light
89 angle, so instead this is approximated with the split-sum-approximation\cite{splitsum}. This results
90 in the equation in figure~\ref{eq:splitsum}, which has two terms: a filter for convoluting
91 (\texttt{prefilterfrag.glsl}), and the BRDF equation (\texttt{brdffrag.glsl}).
92
93 \begin{figure}
94         \begin{equation}
95                 L_o(p,\omega_o) = \int_\Omega L_i(p, \omega_i) d \omega_i \ast
96                 \int_\Omega f_r(p, \omega_i, \omega_o)n \cdot \omega_i d \omega_i
97         \end{equation}
98         \label{eq:splitsum}
99         \caption{The split sum approximation}
100 \end{figure}
101
102 The nice thing about generating all these maps is that they can all reuse the same vertex shader,
103 \texttt{skyboxvert.glsl}, since they all are cubemap shaped!
104 \texttt{pbrfrag.glsl} then combines all the maps to give the final result.
105
106 \begin{figure}
107         \centering
108         \includegraphics[width=0.3\textwidth]{skyboxLoft}
109         \includegraphics[width=0.3\textwidth]{skyboxDesert}
110         \includegraphics[width=0.3\textwidth]{skyboxFactory}
111         \caption{Different skyboxes having different effects on lighting. Note how this lighting model
112         gives reflection for free in the leftmost sphere.}
113 \end{figure}
114
115 \begin{figure}
116         \centering
117         \includegraphics[width=0.45\textwidth]{gunModel}
118         \includegraphics[width=0.45\textwidth]{bulletReflection}
119         \caption{A complex, PBR based model. Even the bullets reflect the environment!}
120 \end{figure}
121
122 \begin{figure}
123         \centering
124         \includegraphics[width=0.45\textwidth]{normalMaps}
125         \includegraphics[width=0.45\textwidth]{normalMaps2}
126         \caption{Normal mapping and ambient occlusion providing depth to the materials.}
127 \end{figure}
128
129
130 \begin{figure}
131         \centering
132         \includegraphics[width=\textwidth]{fresnelEffect}
133         \caption{The Fresnel effect, where the edges of the sphere that have a sharper viewing angle
134         cause the light to be reflected more strongly.}
135 \end{figure}
136
137 \section{Bone Based Animation}
138
139 Since I wanted to take advantage of the GlTF2 model format as much as possible, I
140 decided to load in the animations via the Assimp library. Many models typically describe their
141 animations via a skeletal bone system, where each vertex has a weight associated with each bone in
142 the model, affecting how much it is transformed by when the corresponding bone moves.
143
144 The first step of this was to add bones to the model I had created in blender. I created two bones,
145 one for each swinging sphere, and then assigned the vertex weights of the two spheres and the bottom
146 vertices of the ropes to their respective bones.
147
148 The bones could then be animated using keyframes. By specifying their resting position and apex, I
149 could then use Bezier curves to interpolate between them in such a way that gave the appearance of
150 momentum. Thankfully, the GlTF2 exporter in Blender bakes the Bezier interpolation between the
151 keyframes into many subdivided keyframes, so I was able to just linearly interpolate between them in
152 my program.
153
154 \begin{figure}
155         \centering
156         \includegraphics[width=0.45\textwidth]{blenderBone}
157         \includegraphics[width=0.45\textwidth]{blenderDopeSheet}
158         \caption{Adding bones and animating them in Blender.}
159 \end{figure}
160
161 The program needed to calculate the transformation of each bone at each frame in time of the
162 animation, and bind the transformations into a uniform array of transformation matrices. The vertex
163 shader is then able to use these matrices, alongside the weights for each vertex to calculate its
164 new animated position:
165
166 \begin{minted}{glsl}
167 in ivec4 boneIds;
168 in vec4 boneWeights;
169 // 16 possible total bones and 1 identity transformation if not connected
170 uniform mat4 bones[16 + 1];
171
172 void main() {
173         // A vertex can be influenced by up to 4 bones
174         mat4 boneTrans = bones[boneIds.x] * boneWeights.x;
175         boneTrans += bones[boneIds.y] * boneWeights.y;
176         boneTrans += bones[boneIds.z] * boneWeights.z;
177         boneTrans += bones[boneIds.w] * boneWeights.w;
178
179         mat4 bonedModel = boneTrans * model;
180         // ...
181 }
182 \end{minted}
183
184 \section{Tweaks to AssImp}
185 Since my models were based in the GlTF2 format, I had to build the Assimp library from source as
186 there were features such as animation that were only available in the HEAD branch and not the latest
187 stable release. The support was only partial, so I also found myself adding and fixing some quirks
188 with the library, mainly relating to animations. My changes and pull requests for them can be found
189 here on GitHub:
190
191 https://github.com/assimp/assimp/pull/2243
192 https://github.com/assimp/assimp/pull/2244
193
194 \begin{thebibliography}{9}
195         \bibitem{splitsum}
196                 Brian Karis, Epic Games,
197                 \textit{Real Shading in Unreal Engine 4}, \\
198                 https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf
199 \end{thebibliography}
200
201 \end{document}