Fix metal roughness texture on streaked metal material
[opengl.git] / assignment-5 / report.latex
1 \documentclass{article}
2 \usepackage{fullpage}
3 \usepackage{graphicx}
4 \usepackage{minted}
5 \usepackage{hyperref}
6 \begin{document}
7 \title{CS4052 Project - Physically Based Rendering}
8 \author{Luke Lau 15336810}
9 \maketitle
10
11 \section{Physically Based Rendering}
12 This 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, known as 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 There are 7 texture maps used when rendering each material. 4 are defined in the model and 3 are
26 precomputed at the start of the program.
27 \begin{minted}{glsl}
28 uniform sampler2D albedoMap;
29 uniform sampler2D normalMap;
30 uniform sampler2D metallicRoughnessMap;
31 uniform sampler2D aoMap;
32 uniform samplerCube irradianceMap;
33 uniform samplerCube prefilterMap;
34 uniform sampler2D brdfMap;
35 \end{minted}
36
37 The Albedo map provides the base colour underneath the diffuse and specular layers, the normal
38 map is just a standard normal map, and similarly for the ambient occlusion (ao) map.
39
40 The Newton's Cradle is loaded from a GlTF2 model, a trendy new model format designed for PBR materials,
41 and is maintained by the Khronos group (the same group behind OpenGL and Vulkan).
42 Because it is physically based, the specification expects a metallic-roughness map texture.
43 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,
44 \texttt{metallicRoughness.py} which combines two separate metallic/roughness maps into one suitable
45 for the GlTF2 format. AssImp recently added partial support for GlTF2 models so I was able to read
46 in the metallic/roughness texture into a 2 channel texture, and access it from within the shader
47 like so:
48
49 \begin{minted}{glsl}
50 float metallic = texture(metallicRoughnessMap, texCoords).b;
51 float roughness = texture(metallicRoughnessMap, texCoords).g;
52 \end{minted}
53
54 The irradiance, prefilter and BRDF maps are all precomputed based off the skybox in order to provide
55 accurate, image based lighting (IBL) that correctly reflects the contents of the environment. The
56 skybox itself is loaded in from an equirectangular HDR file, and so needs to be gamma corrected and
57 converted to a cubemap first. \texttt{skyboxfrag.glsl} handles the gamma
58 correction and \texttt{equirectangularfrag.glsl} takes care of converting the projection. 
59
60 It needs to be in the HDR format since the precomputed maps are based off of physical properties,
61 and the equations that are involved work in terms of flux. 
62
63 \begin{figure}
64         \begin{equation}
65         L_0(p,\omega_0) =
66         \int_\Omega(k_d \frac{c}{\pi} +
67                                 k_s \frac{DFG}{4(\omega_0 \cdot n)
68                                                                 (\omega_i \cdot n)})
69                            L_i(p,\omega_i)n \cdot \omega_i d\omega_i
70         \end{equation}
71         \label{eq:reflectance}
72         \caption{The reflectance equation}
73 \end{figure}
74
75 The irradiance map solves the $k_d \frac{c}{\pi}$ (diffuse) part of figure~\ref{eq:reflectance}, and is
76 calculated in \texttt{irradiancefrag.glsl}. It approximates the integral by taking a finite number
77 of samples, and does this for each possible incoming angle of light, storing the result into a
78 cubemap texture that can easily be looked up later.
79
80 The same idea applies for the prefilter and BRDF maps: solve a computationally expensive integral
81 and shove it into a texture that can be quickly referenced in the main fragment shader. They
82 precompute the integrals in the $k_s$ (specular) side of the figure~\ref{eq:reflectance}, but unlike
83 the $k_d$ term, this term is not constant and depends on the viewing angle, $\omega_i$. It would be
84 crazy to precompute this integral for every single combination of viewing angle and incoming light
85 angle, so instead this is approximated with the split-sum-approximation\cite{splitsum}. This results
86 in the equation in figure~\ref{eq:splitsum}, which has two terms: a filter for convoluting
87 (\texttt{prefilterfrag.glsl}), and the bidirectional reflective distribution function (BRDF) (\texttt{brdffrag.glsl}).
88
89 The BRDF is a function that takes in the incoming light direction, the outgoing view direction,
90 the surface normal and the roughness of the material, and returns how much of the incoming light
91 is reflected back at that viewing angle.
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 \begin{figure}
130         \centering
131         \includegraphics[width=\textwidth]{fresnelEffect}
132         \caption{The Fresnel effect, where the edges of the sphere that have a sharper viewing angle
133         cause the light to be reflected more strongly.}
134 \end{figure}
135
136 \section{Bone Based Animation}
137
138 Since I wanted to take advantage of the GlTF2 model format as much as possible, I
139 decided to load in the animations via the Assimp library. Many models typically describe their
140 animations via a skeletal bone system, where each vertex has a weight associated with each bone in
141 the model, affecting how much it is transformed by when the corresponding bone moves.
142
143 The first step was to add bones to the model I had created in blender. I created two bones,
144 one for each swinging sphere, and then assigned the vertex weights of the two spheres and the bottom
145 vertices of the ropes to their respective bones.
146
147 The bones could then be animated using keyframes. By specifying the starting and apex, I
148 could use Bezier curves to interpolate between them in such a way that gave the appearance of
149 momentum. Thankfully, the GlTF2 exporter in Blender bakes the Bezier interpolation into many
150 subdivided keyframes, so I was able to just linearly interpolate between them in my program.
151
152 \begin{figure}
153         \centering
154         \includegraphics[width=0.45\textwidth]{blenderBone}
155         \includegraphics[width=0.45\textwidth]{blenderDopeSheet}
156         \caption{Adding bones and animating them in Blender.}
157 \end{figure}
158
159 The program needed to calculate the transformation of each bone at each frame in time of the
160 animation, and bind the transformations into a uniform array of transformation matrices. The vertex
161 shader is then able to use these matrices, alongside the weights for each vertex to calculate its
162 new animated position:
163
164 \begin{minted}{glsl}
165 in ivec4 boneIds;
166 in vec4 boneWeights;
167 // 16 possible total bones and 1 identity transformation if not connected
168 uniform mat4 bones[16 + 1];
169
170 void main() {
171         // A vertex can be influenced by up to 4 bones
172         mat4 boneTrans = bones[boneIds.x] * boneWeights.x;
173         boneTrans += bones[boneIds.y] * boneWeights.y;
174         boneTrans += bones[boneIds.z] * boneWeights.z;
175         boneTrans += bones[boneIds.w] * boneWeights.w;
176
177         mat4 bonedModel = boneTrans * model;
178         // ...
179 }
180 \end{minted}
181
182 \section{Tweaks to AssImp}
183 Since my models were based in the GlTF2 format, I had to build the Assimp library from source as
184 there were features such as animation that were only available in the HEAD branch and not the latest
185 stable release. The support was only partial, so I also found myself adding and fixing some quirks
186 with the library, mainly relating to animations. My changes and pull requests for them can be found
187 here on GitHub:
188
189 \url{https://github.com/assimp/assimp/pull/2243}
190 \url{https://github.com/assimp/assimp/pull/2244}
191
192 \begin{thebibliography}{9}
193         \bibitem{splitsum}
194                 Brian Karis, Epic Games,
195                 \textit{Real Shading in Unreal Engine 4}, \\
196                 \url{https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf}
197 \end{thebibliography}
198
199 \end{document}