LearnOpenGL

Translation in progress of learnopengl.com.
git clone https://git.mtkn.jp/LearnOpenGL
Log | Files | Refs

Diffuse-irradiance.html (40528B)


      1     <h1 id="content-title">Diffuse irradiance</h1>
      2 <h1 id="content-url" style='display:none;'>PBR/IBL/Diffuse-irradiance</h1>
      3 <p>
      4   IBL, or <def>image based lighting</def>, is a collection of techniques to light objects, not by direct analytical lights as in the <a href="https://learnopengl.com/PBR/Lighting" target="_blank">previous</a> chapter, but by treating the surrounding environment as one big light source. This is generally accomplished by manipulating a cubemap environment map (taken from the real world or generated from a 3D scene) such that we can directly use it in our lighting equations: treating each cubemap texel as a light emitter. This way we can effectively capture an environment's global lighting and general feel, giving objects a better sense of <em>belonging</em> in their environment.
      5 </p>
      6 
      7 <p>
      8   As image based lighting algorithms capture the lighting of some (global) environment, its input is  considered a more precise form of ambient lighting, even a crude approximation of global illumination. This makes IBL interesting for PBR as objects look significantly more physically accurate when we take the environment's lighting into account.
      9 </p>
     10 
     11 <p>
     12   To start introducing IBL into our PBR system let's again take a quick look at the reflectance equation:
     13 </p>
     14 
     15 
     16 \[
     17  	L_o(p,\omega_o) = \int\limits_{\Omega} 
     18     	(k_d\frac{c}{\pi} + k_s\frac{DFG}{4(\omega_o \cdot n)(\omega_i \cdot n)})
     19     	L_i(p,\omega_i) n \cdot \omega_i  d\omega_i
     20 \]
     21 
     22 <p>
     23   As described before, our main goal is to solve the integral of all incoming light directions \(w_i\) over the hemisphere \(\Omega\) . Solving the integral in the previous chapter was easy as we knew beforehand the exact few light directions \(w_i\) that contributed to the integral.
     24   This time however, <strong>every</strong> incoming light direction \(w_i\) from the surrounding environment could potentially have some radiance making it less trivial to solve the integral. This gives us two main requirements for solving the integral:  
     25 </p>
     26 
     27 <ul>
     28   <li>We need some way to retrieve the scene's radiance given any direction vector \(w_i\).</li>
     29   <li>Solving the integral needs to be fast and real-time.</li>
     30 </ul>
     31 
     32 <p>
     33   Now, the first requirement is relatively easy. We've already hinted it, but one way of representing an environment or scene's irradiance is in the form of a (processed) environment cubemap. Given such a cubemap, we can visualize every texel of the cubemap as one single emitting light source. By sampling this cubemap with any direction vector \(w_i\), we retrieve the scene's radiance from that direction.
     34 </p>
     35 
     36 <p>
     37   Getting the scene's radiance given any direction vector \(w_i\) is then as simple as:
     38 </p>
     39 
     40 <pre><code>
     41 vec3 radiance = texture(_cubemapEnvironment, w_i).rgb;  
     42 </code></pre>
     43 
     44 <p>
     45   Still, solving the integral requires us to sample the environment map from not just one direction, but all possible directions \(w_i\) over the hemisphere \(\Omega\) which is far too expensive for each fragment shader invocation. To solve the integral in a more efficient fashion we'll want to <em>pre-process</em> or <def>pre-compute</def> most of the computations. For this we'll have to delve a bit deeper into the reflectance equation:
     46 </p>
     47 
     48 \[
     49  	L_o(p,\omega_o) = \int\limits_{\Omega} 
     50     	(k_d\frac{c}{\pi} + k_s\frac{DFG}{4(\omega_o \cdot n)(\omega_i \cdot n)})
     51     	L_i(p,\omega_i) n \cdot \omega_i  d\omega_i
     52 \]
     53       
     54 <p>
     55   Taking a good look at the reflectance equation we find that the diffuse \(k_d\) and specular \(k_s\) term of the BRDF are independent from each other and we can split the integral in two:
     56       </p>
     57       
     58 \[
     59  	L_o(p,\omega_o) = 
     60 		\int\limits_{\Omega} (k_d\frac{c}{\pi}) L_i(p,\omega_i) n \cdot \omega_i  d\omega_i
     61 		+ 
     62 		\int\limits_{\Omega} (k_s\frac{DFG}{4(\omega_o \cdot n)(\omega_i \cdot n)})
     63 			L_i(p,\omega_i) n \cdot \omega_i  d\omega_i
     64 \]
     65       
     66 <p>
     67   By splitting the integral in two parts we can focus on both the diffuse and specular term individually; the focus of this chapter being on the diffuse integral. 
     68 </p>
     69 
     70 <p>
     71   Taking a closer look at the diffuse integral we find that the diffuse lambert term is a constant term (the color \(c\), the refraction ratio \(k_d\), and \(\pi\) are constant over the integral) and not dependent on any of the integral variables. Given this, we can move the constant term out of the diffuse integral:
     72 </p>
     73       
     74 \[
     75       L_o(p,\omega_o) = 
     76 		k_d\frac{c}{\pi} \int\limits_{\Omega} L_i(p,\omega_i) n \cdot \omega_i  d\omega_i
     77 \]
     78       
     79 <p>
     80   This gives us an integral that only depends on \(w_i\) (assuming \(p\) is at the center of the environment map). With this knowledge, we can calculate or <em>pre-compute</em> a new cubemap that stores in each sample direction (or texel) \(w_o\) the diffuse integral's result by <def>convolution</def>.
     81 </p>
     82       
     83 <p>
     84 	Convolution is applying some computation to each entry in a data set considering all other entries in the data set; the data set being the scene's radiance or environment map. Thus for every sample direction in the cubemap, we take all other sample directions over the hemisphere \(\Omega\) into account.   
     85 </p>
     86 
     87 <p>
     88   To convolute an environment map we solve the integral for each output \(w_o\) sample direction by discretely sampling a large number of directions \(w_i\) over the hemisphere \(\Omega\) and averaging their radiance. The hemisphere we build the sample directions \(w_i\) from is oriented towards the output \(w_o\) sample direction we're convoluting. 
     89 </p>
     90 
     91 <img src="/img/pbr/ibl_hemisphere_sample.png" class="clean" alt="Convoluting a cubemap on a hemisphere for a PBR irradiance map."/>
     92 
     93 <p>
     94   This pre-computed cubemap, that for each sample direction \(w_o\) stores the integral result, can be thought of as the pre-computed sum of all indirect diffuse light of the scene hitting some surface aligned along direction \(w_o\). Such a cubemap is known as an <def>irradiance map</def> seeing as the convoluted cubemap effectively allows us to directly sample the scene's (pre-computed) irradiance from any direction \(w_o\). 
     95 </p>
     96 
     97 <note>
     98   The radiance equation also depends on a position \(p\), which we've assumed to be at the center of the irradiance map. This does mean all diffuse indirect light must come from a single environment map which may break the illusion of reality (especially indoors). Render engines solve this by placing <def>reflection probes</def> all over the scene where each reflection probes calculates its own irradiance map of its surroundings. This way, the irradiance (and radiance) at position \(p\) is the interpolated irradiance between its closest reflection probes. For now, we assume we always sample the environment map from its center.
     99 </note>
    100 
    101 <p>
    102   Below is an example of a cubemap environment map and its resulting irradiance map (courtesy of <a href="http://www.indiedb.com/features/using-image-based-lighting-ibl" target="_blank">wave engine</a>), averaging the scene's radiance for every direction \(w_o\).
    103 </p>
    104 
    105 <img src="/img/pbr/ibl_irradiance.png" class="clean" alt="The effect of convoluting a cubemap environment map."/>
    106   
    107 <p>
    108   By storing the convoluted result in each cubemap texel (in the direction of \(w_o\)), the irradiance map displays somewhat like an average color or lighting display of the environment. Sampling any direction from this environment map will give us the scene's irradiance in that particular direction. 
    109 </p>
    110 
    111 
    112 <h2>PBR and HDR</h2>
    113 <p>
    114   We've briefly touched upon it in the <a href="https://learnopengl.com/PBR/Lighting" target="_blank">previous</a> chapter: taking the high dynamic range of your scene's lighting into account in a PBR pipeline is incredibly important. As PBR bases most of its inputs on real physical properties and measurements it makes sense to closely match the incoming light values to their physical equivalents. Whether we make educated guesses on each light's radiant flux or use their <a href="https://en.wikipedia.org/wiki/Lumen_(unit)" target="_blank">direct physical equivalent</a>, the difference between a simple light bulb or the sun is significant either way. Without working in an <a href="https://learnopengl.com/Advanced-Lighting/HDR" target="_blank">HDR</a> render environment it's impossible to correctly specify each light's relative  intensity.
    115 </p>
    116 
    117 <p>
    118   So, PBR and HDR go hand in hand, but how does it all relate to image based lighting? We've seen in  the previous chapter that it's relatively easy to get PBR working in HDR. However, seeing as for image based lighting we base the environment's indirect light intensity on the color values of an environment cubemap we need some way to store the lighting's high dynamic range into an environment map.
    119 </p>
    120 
    121 <p>
    122   The environment maps we've been using so far as cubemaps (used as <a href="https://learnopengl.com/Advanced-OpenGL/Cubemaps" target="_blank">skyboxes</a> for instance) are in low dynamic range (LDR). We directly used their color values from the individual face images, ranged between <code>0.0</code> and <code>1.0</code>, and processed them as is. While this may work fine for visual output, when taking them as physical input parameters it's not going to work.
    123 </p>
    124 
    125 <h3>The radiance HDR file format</h3>
    126 <p>
    127   Enter the radiance file format. The radiance file format (with the <code>.hdr</code> extension) stores a full cubemap with all 6 faces as floating point data. This allows us to specify color values outside the <code>0.0</code> to <code>1.0</code> range to give lights their correct color intensities. The file format also uses a clever trick to store each floating point value, not as a 32 bit value per channel, but 8 bits per channel using the color's alpha channel as an exponent (this does come with a loss of precision). This works quite well, but requires the parsing program to re-convert each color to their floating point equivalent. 
    128 </p>
    129 
    130 <p>
    131   There are quite a few radiance HDR environment maps freely available from sources like <a href="http://www.hdrlabs.com/sibl/archive.html" target="_blank">sIBL archive</a> of which you can see an example below:
    132 </p>
    133 
    134 <img src="/img/pbr/ibl_hdr_radiance.png" alt="Example of an equirectangular map"/>
    135   
    136 <p>
    137     This may not be exactly what you were expecting, as the image appears distorted and doesn't show any of the 6 individual cubemap faces of environment maps we've seen before. This environment map is projected from a sphere onto a flat plane such that we can more easily store the environment into a single image known as an <def>equirectangular map</def>. This does come with a small caveat as most of the visual resolution is stored in the horizontal view direction, while less is preserved in the bottom and top directions. In most cases this is a decent compromise as with almost any renderer you'll find most of the interesting lighting and surroundings in the horizontal viewing directions.
    138 </p>
    139 
    140 <h3>HDR and stb_image.h</h3>
    141 <p>
    142   Loading radiance HDR images directly requires some knowledge of the <a href="http://radsite.lbl.gov/radiance/refer/Notes/picture_format.html" target="_blank">file format</a>  which isn't too difficult, but cumbersome nonetheless. Lucky for us, the popular one header library <a href="https://github.com/nothings/stb/blob/master/stb_image.h" target="_blank">stb_image.h</a> supports loading radiance HDR images directly as an array of floating point values which perfectly fits our needs. With <code>stb_image</code> added to your project, loading an HDR image is now as simple as follows:
    143 </p>
    144   
    145 <pre><code>
    146 #include "stb_image.h"
    147 [...]
    148 
    149 stbi_set_flip_vertically_on_load(true);
    150 int width, height, nrComponents;
    151 float *data = stbi_loadf("newport_loft.hdr", &width, &height, &nrComponents, 0);
    152 unsigned int hdrTexture;
    153 if (data)
    154 {
    155     <function id='50'>glGenTextures</function>(1, &hdrTexture);
    156     <function id='48'>glBindTexture</function>(GL_TEXTURE_2D, hdrTexture);
    157     <function id='52'>glTexImage2D</function>(GL_TEXTURE_2D, 0, GL_RGB16F, width, height, 0, GL_RGB, GL_FLOAT, data); 
    158 
    159     <function id='15'>glTexParameter</function>i(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    160     <function id='15'>glTexParameter</function>i(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    161     <function id='15'>glTexParameter</function>i(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    162     <function id='15'>glTexParameter</function>i(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    163 
    164     stbi_image_free(data);
    165 }
    166 else
    167 {
    168     std::cout &lt;&lt; "Failed to load HDR image." &lt;&lt; std::endl;
    169 }  
    170 </code></pre>
    171   
    172 <p>
    173   <code>stb_image.h</code> automatically maps the HDR values to a list of floating point values: 32 bits per channel and 3 channels per color by default. This is all we need to store the equirectangular HDR environment map into a 2D floating point texture.
    174 </p>
    175 
    176 <h3>From Equirectangular to Cubemap</h3>
    177 <p>
    178   It is possible to use the equirectangular map directly for environment lookups, but these operations can be relatively expensive in which case a direct cubemap sample is more performant. Therefore, in this chapter we'll first convert the equirectangular image to a cubemap for further processing. Note that in the process we also show how to sample an equirectangular map as if it was a 3D environment map in which case you're free to pick whichever solution you prefer.
    179 </p>
    180   
    181 <p>
    182   To convert an equirectangular image into a cubemap we need to render a (unit) cube and project the equirectangular map on all of the cube's faces from the inside and take 6 images of each of the cube's sides as a cubemap face. The vertex shader of this cube simply renders the cube as is and passes its local position to the fragment shader as a 3D sample vector:
    183 </p>
    184   
    185 <pre><code>
    186 #version 330 core
    187 layout (location = 0) in vec3 aPos;
    188 
    189 out vec3 localPos;
    190 
    191 uniform mat4 projection;
    192 uniform mat4 view;
    193 
    194 void main()
    195 {
    196     localPos = aPos;  
    197     gl_Position =  projection * view * vec4(localPos, 1.0);
    198 }
    199 </code></pre>
    200   
    201 <p>
    202   For the fragment shader, we color each part of the cube as if we neatly folded the equirectangular map onto each side of the cube. To accomplish this, we take the fragment's sample direction as interpolated from the cube's local position and then use this direction vector and some trigonometry magic (spherical to cartesian) to sample the equirectangular map as if it's a cubemap itself. We directly store the result onto the cube-face's fragment which should be all we need to do:
    203 </p>
    204   
    205 <pre><code>
    206 #version 330 core
    207 out vec4 FragColor;
    208 in vec3 localPos;
    209 
    210 uniform sampler2D equirectangularMap;
    211 
    212 const vec2 invAtan = vec2(0.1591, 0.3183);
    213 vec2 SampleSphericalMap(vec3 v)
    214 {
    215     vec2 uv = vec2(atan(v.z, v.x), asin(v.y));
    216     uv *= invAtan;
    217     uv += 0.5;
    218     return uv;
    219 }
    220 
    221 void main()
    222 {		
    223     vec2 uv = SampleSphericalMap(normalize(localPos)); // make sure to normalize localPos
    224     vec3 color = texture(equirectangularMap, uv).rgb;
    225     
    226     FragColor = vec4(color, 1.0);
    227 }
    228 
    229 </code></pre>
    230     
    231 <p>
    232   If you render a cube at the center of the scene given an HDR equirectangular map you'll get something that looks like this:
    233 </p>
    234   
    235   <img src="/img/pbr/ibl_equirectangular_projection.png" alt="OpenGL render of an equirectangular map converted to a cubemap."/>
    236   
    237 <p>
    238   This demonstrates that we effectively mapped an equirectangular image onto a cubic shape, but doesn't yet help us in converting the source HDR image to a cubemap texture. To accomplish this we have to render the same cube 6 times, looking at each individual face of the cube, while recording its visual result with a <a href="https://learnopengl.com/Advanced-OpenGL/Framebuffers" target="_blank">framebuffer</a> object:
    239 </p>
    240     
    241 <pre><code>
    242 unsigned int captureFBO, captureRBO;
    243 <function id='76'>glGenFramebuffers</function>(1, &captureFBO);
    244 <function id='82'>glGenRenderbuffers</function>(1, &captureRBO);
    245 
    246 <function id='77'>glBindFramebuffer</function>(GL_FRAMEBUFFER, captureFBO);
    247 <function id='83'>glBindRenderbuffer</function>(GL_RENDERBUFFER, captureRBO);
    248 <function id='88'>glRenderbufferStorage</function>(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 512, 512);
    249 <function id='89'>glFramebufferRenderbuffer</function>(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, captureRBO);  
    250 </code></pre>  
    251     
    252 <p>
    253   Of course, we then also generate the corresponding cubemap color textures, pre-allocating memory for each of its 6 faces:
    254 </p>
    255   
    256 <pre><code>
    257 unsigned int envCubemap;
    258 <function id='50'>glGenTextures</function>(1, &envCubemap);
    259 <function id='48'>glBindTexture</function>(GL_TEXTURE_CUBE_MAP, envCubemap);
    260 for (unsigned int i = 0; i &lt; 6; ++i)
    261 {
    262     // note that we store each face with 16 bit floating point values
    263     <function id='52'>glTexImage2D</function>(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB16F, 
    264                  512, 512, 0, GL_RGB, GL_FLOAT, nullptr);
    265 }
    266 <function id='15'>glTexParameter</function>i(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    267 <function id='15'>glTexParameter</function>i(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    268 <function id='15'>glTexParameter</function>i(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
    269 <function id='15'>glTexParameter</function>i(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    270 <function id='15'>glTexParameter</function>i(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    271 </code></pre>
    272     
    273 <p>
    274   Then what's left to do is capture the equirectangular 2D texture onto the cubemap faces.
    275 </p>
    276     
    277 <p>
    278   I won't go over the details as the code details topics previously discussed in the <a href="https://learnopengl.com/Advanced-OpenGL/Framebuffers" target="_blank">framebuffer</a> and <a href="https://learnopengl.com/Advanced-Lighting/Shadows/Point-Shadows" target="_blank">point shadows</a> chapters, but it effectively boils down to setting up 6 different view matrices (facing  each side of the cube), set up a projection matrix with a fov of <code>90</code> degrees to capture the entire face, and render a cube 6 times storing the results in a floating point framebuffer: 
    279 </p>
    280     
    281 <pre><code>
    282 glm::mat4 captureProjection = <function id='58'>glm::perspective</function>(<function id='63'>glm::radians</function>(90.0f), 1.0f, 0.1f, 10.0f);
    283 glm::mat4 captureViews[] = 
    284 {
    285    <function id='62'>glm::lookAt</function>(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 1.0f,  0.0f,  0.0f), glm::vec3(0.0f, -1.0f,  0.0f)),
    286    <function id='62'>glm::lookAt</function>(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(-1.0f,  0.0f,  0.0f), glm::vec3(0.0f, -1.0f,  0.0f)),
    287    <function id='62'>glm::lookAt</function>(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f,  1.0f,  0.0f), glm::vec3(0.0f,  0.0f,  1.0f)),
    288    <function id='62'>glm::lookAt</function>(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, -1.0f,  0.0f), glm::vec3(0.0f,  0.0f, -1.0f)),
    289    <function id='62'>glm::lookAt</function>(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f,  0.0f,  1.0f), glm::vec3(0.0f, -1.0f,  0.0f)),
    290    <function id='62'>glm::lookAt</function>(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f,  0.0f, -1.0f), glm::vec3(0.0f, -1.0f,  0.0f))
    291 };
    292 
    293 // convert HDR equirectangular environment map to cubemap equivalent
    294 equirectangularToCubemapShader.use();
    295 equirectangularToCubemapShader.setInt("equirectangularMap", 0);
    296 equirectangularToCubemapShader.setMat4("projection", captureProjection);
    297 <function id='49'>glActiveTexture</function>(GL_TEXTURE0);
    298 <function id='48'>glBindTexture</function>(GL_TEXTURE_2D, hdrTexture);
    299 
    300 <function id='22'>glViewport</function>(0, 0, 512, 512); // don't forget to configure the viewport to the capture dimensions.
    301 <function id='77'>glBindFramebuffer</function>(GL_FRAMEBUFFER, captureFBO);
    302 for (unsigned int i = 0; i &lt; 6; ++i)
    303 {
    304     equirectangularToCubemapShader.setMat4("view", captureViews[i]);
    305     <function id='81'>glFramebufferTexture2D</function>(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, 
    306                            GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, envCubemap, 0);
    307     <function id='10'>glClear</function>(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    308 
    309     renderCube(); // renders a 1x1 cube
    310 }
    311 <function id='77'>glBindFramebuffer</function>(GL_FRAMEBUFFER, 0);  
    312 </code></pre>
    313     
    314 <p>
    315   We take the color attachment of the framebuffer and switch its texture target around for every face of the cubemap, directly rendering the scene into one of the cubemap's faces. Once this routine has finished (which we only have to do once), the cubemap <var>envCubemap</var> should be the cubemapped environment version of our original HDR image.
    316 </p>
    317     
    318 <p>
    319   Let's test the cubemap by writing a very simple skybox shader to display the cubemap around us:
    320 </p>
    321     
    322 <pre><code>
    323 #version 330 core
    324 layout (location = 0) in vec3 aPos;
    325 
    326 uniform mat4 projection;
    327 uniform mat4 view;
    328 
    329 out vec3 localPos;
    330 
    331 void main()
    332 {
    333     localPos = aPos;
    334 
    335     mat4 rotView = mat4(mat3(view)); // remove translation from the view matrix
    336     vec4 clipPos = projection * rotView * vec4(localPos, 1.0);
    337 
    338     gl_Position = clipPos.xyww;
    339 }
    340 </code></pre>
    341     
    342 <p>
    343   Note the <code>xyww</code> trick here that ensures the depth value of the rendered cube fragments always end up at <code>1.0</code>, the maximum depth value, as described in the <a href="https://learnopengl.com/Advanced-OpenGL/Cubemaps" target="_blank">cubemap</a> chapter. Do note that we need to change the depth comparison function to <var>GL_LEQUAL</var>:
    344 </p>
    345     
    346 <pre><code>
    347 <function id='66'>glDepthFunc</function>(GL_LEQUAL);  
    348 </code></pre>
    349     
    350 <p>
    351   The fragment shader then directly samples the cubemap environment map using the cube's local fragment position:
    352 </p>
    353   
    354 <pre><code>
    355 #version 330 core
    356 out vec4 FragColor;
    357 
    358 in vec3 localPos;
    359   
    360 uniform samplerCube environmentMap;
    361   
    362 void main()
    363 {
    364     vec3 envColor = texture(environmentMap, localPos).rgb;
    365     
    366     envColor = envColor / (envColor + vec3(1.0));
    367     envColor = pow(envColor, vec3(1.0/2.2)); 
    368   
    369     FragColor = vec4(envColor, 1.0);
    370 }
    371 </code></pre>
    372     
    373 <p>
    374   We sample the environment map using its interpolated vertex cube positions that directly correspond to the correct direction vector to sample. Seeing as the camera's translation components are ignored, rendering this shader over a cube should give you the environment map as a non-moving background. Also, as we directly output the environment map's HDR values to the default LDR framebuffer, we want to properly tone map the color values. Furthermore, almost all HDR maps are in linear color space by default so we need to apply <a href="https://learnopengl.com/Advanced-Lighting/Gamma-Correction" target="_blank">gamma correction</a> before writing to the default framebuffer.
    375 </p>
    376     
    377 <p>
    378   Now rendering the sampled environment map over the previously rendered spheres should look something like this:
    379 </p>
    380     
    381     <img src="/img/pbr/ibl_hdr_environment_mapped.png" alt="Render the converted cubemap as a skybox."/>
    382       
    383 <p>
    384   Well... it took us quite a bit of setup to get here, but we successfully managed to read an HDR environment map, convert it from its equirectangular mapping to a cubemap, and render the HDR cubemap into the scene as a skybox. Furthermore, we set up a small system to render onto all 6 faces of a cubemap, which we'll need again when <def>convoluting</def> the environment map. You can find the source code of the entire conversion process <a href="/code_viewer_gh.php?code=src/6.pbr/2.1.1.ibl_irradiance_conversion/ibl_irradiance_conversion.cpp" target="_blank">here</a>.
    385 </p>
    386       
    387 <h2>Cubemap convolution</h2>
    388 <p>
    389   As described at the start of the chapter, our main goal is to solve the integral for all diffuse indirect lighting given the scene's irradiance in the form of a cubemap environment map. We know that we can get the radiance of the scene \(L(p, w_i)\) in a particular direction by sampling an HDR environment map in direction \(w_i\). To solve the integral, we have to sample the scene's radiance from all possible directions within the hemisphere \(\Omega\) for each fragment.
    390       </p>
    391       
    392 <p>
    393 	It is however computationally impossible to sample the environment's lighting from every possible direction in \(\Omega\), the number of possible directions is theoretically infinite. We can however, approximate the number of directions by taking a finite number of directions or samples, spaced uniformly or taken randomly from within the hemisphere, to get a fairly accurate approximation of the irradiance; effectively solving the integral \(\int\) discretely  
    394 </p>
    395       
    396 <p>
    397 	It is however still too expensive to do this for every fragment in real-time as the number of samples needs to be significantly large for decent results, so we want to <def>pre-compute</def> this. Since the orientation of the hemisphere decides where we capture the irradiance, we can pre-calculate the irradiance for every possible hemisphere orientation oriented around all outgoing directions \(w_o\):
    398 </p>
    399       
    400 \[
    401       L_o(p,\omega_o) = 
    402 		k_d\frac{c}{\pi} \int\limits_{\Omega} L_i(p,\omega_i) n \cdot \omega_i  d\omega_i
    403 \]
    404       
    405 <p>
    406   Given any direction vector \(w_i\) in the lighting pass, we can then sample the pre-computed irradiance map to retrieve the total diffuse irradiance from direction \(w_i\). To determine the amount of indirect diffuse (irradiant) light at a fragment surface, we retrieve the total irradiance from the hemisphere oriented around its surface normal. Obtaining the scene's irradiance is then as simple as:
    407 </p>
    408       
    409 <pre><code>
    410 vec3 irradiance = texture(irradianceMap, N).rgb;
    411 </code></pre>
    412       
    413 <p>
    414   Now, to generate the irradiance map, we need to convolute the environment's lighting as converted to a cubemap. Given that for each fragment the surface's hemisphere is oriented along the normal vector \(N\), convoluting a cubemap equals calculating the total averaged radiance of each direction \(w_i\) in the hemisphere \(\Omega\) oriented along \(N\). 
    415 </p>
    416       
    417 <img src="/img/pbr/ibl_hemisphere_sample_normal.png" class="clean" alt="Convoluting a cubemap on a hemisphere (oriented around the normal) for a PBR irradiance map."/>
    418       
    419 <p>
    420   Thankfully, all of the cumbersome setup of this chapter isn't all for nothing as we can now directly take the converted cubemap, convolute it in a fragment shader, and capture its result in a new cubemap using a framebuffer that renders to all 6 face directions. As we've already set this up for converting the equirectangular environment map to a cubemap, we can take the exact same approach but use a different fragment shader:
    421 </p>
    422       
    423 <pre><code>
    424 #version 330 core
    425 out vec4 FragColor;
    426 in vec3 localPos;
    427 
    428 uniform samplerCube environmentMap;
    429 
    430 const float PI = 3.14159265359;
    431 
    432 void main()
    433 {		
    434     // the sample direction equals the hemisphere's orientation 
    435     vec3 normal = normalize(localPos);
    436   
    437     vec3 irradiance = vec3(0.0);
    438   
    439     [...] // convolution code
    440   
    441     FragColor = vec4(irradiance, 1.0);
    442 }
    443 </code></pre>
    444    
    445 <p>
    446   With <var>environmentMap</var> being the HDR cubemap as converted from the equirectangular HDR environment map. 
    447 </p>
    448       
    449 <p>
    450 	There are many ways to convolute the environment map, but for this chapter we're going to generate a fixed amount of sample vectors for each cubemap texel along a hemisphere \(\Omega\) oriented around the sample direction and average the results. The fixed amount of sample vectors will be uniformly spread inside the hemisphere. Note that an integral is a continuous function and discretely sampling its function given a fixed amount of sample vectors will be an approximation. The more sample vectors we use, the better we approximate the integral.  
    451 </p>
    452       
    453 <p>
    454   The integral \(\int\) of the reflectance equation revolves around the solid angle \(dw\) which is rather difficult to work with. Instead of integrating over the solid angle \(dw\) we'll integrate over its equivalent spherical coordinates \(\theta\) and \(\phi\).
    455 </p>
    456 
    457       
    458       <img src="/img/pbr/ibl_spherical_integrate.png" class="clean" alt="Converting the solid angle over the equivalent polar azimuth and inclination angle for PBR"/>
    459         
    460 <p>
    461   We use the polar azimuth \(\phi\) angle to sample around the ring of the hemisphere between \(0\) and \(2\pi\), and use the inclination zenith \(\theta\) angle between \(0\) and \(\frac{1}{2}\pi\) to sample the increasing rings of the hemisphere. This will give us the updated reflectance integral: 
    462 </p>
    463 
    464 \[
    465       L_o(p,\phi_o, \theta_o) = 
    466         k_d\frac{c}{\pi} \int_{\phi = 0}^{2\pi} \int_{\theta = 0}^{\frac{1}{2}\pi} L_i(p,\phi_i, \theta_i) \cos(\theta) \sin(\theta)  d\phi d\theta
    467 \]
    468         
    469 <p>
    470   Solving the integral requires us to take a fixed number of discrete samples within the hemisphere \(\Omega\) and averaging their results. This translates the integral to the following discrete version as based on the <a href="https://en.wikipedia.org/wiki/Riemann_sum" target="_blank">Riemann sum</a> given \(n1\) and \(n2\) discrete samples on each spherical coordinate respectively:
    471 </p>
    472         
    473 \[
    474       L_o(p,\phi_o, \theta_o) = 
    475         k_d \frac{c\pi}{n1 n2} \sum_{\phi = 0}^{n1} \sum_{\theta = 0}^{n2} L_i(p,\phi_i, \theta_i) \cos(\theta) \sin(\theta)  d\phi d\theta
    476 \]   
    477 
    478         
    479 <p>
    480    As we sample both spherical values discretely, each sample will approximate or average an area on the hemisphere as the image before shows. Note that (due to the general properties of a spherical shape) the hemisphere's discrete sample area gets smaller the higher the zenith angle \(\theta\) as the sample regions converge towards the center top. To compensate for the smaller areas, we weigh its contribution by scaling the area by \(\sin \theta\).
    481 </p>
    482       
    483 <p>
    484   Discretely sampling the hemisphere given the integral's spherical coordinates translates to the following fragment code:
    485 </p>
    486       
    487 <pre><code>
    488 vec3 irradiance = vec3(0.0);  
    489 
    490 vec3 up    = vec3(0.0, 1.0, 0.0);
    491 vec3 right = normalize(cross(up, normal));
    492 up         = normalize(cross(normal, right));
    493 
    494 float sampleDelta = 0.025;
    495 float nrSamples = 0.0; 
    496 for(float phi = 0.0; phi &lt; 2.0 * PI; phi += sampleDelta)
    497 {
    498     for(float theta = 0.0; theta &lt; 0.5 * PI; theta += sampleDelta)
    499     {
    500         // spherical to cartesian (in tangent space)
    501         vec3 tangentSample = vec3(sin(theta) * cos(phi),  sin(theta) * sin(phi), cos(theta));
    502         // tangent space to world
    503         vec3 sampleVec = tangentSample.x * right + tangentSample.y * up + tangentSample.z * N; 
    504 
    505         irradiance += texture(environmentMap, sampleVec).rgb * cos(theta) * sin(theta);
    506         nrSamples++;
    507     }
    508 }
    509 irradiance = PI * irradiance * (1.0 / float(nrSamples));
    510 </code></pre>
    511       
    512 <p>
    513   We specify a fixed <var>sampleDelta</var> delta value to traverse the hemisphere; decreasing or increasing the sample delta will increase or decrease the accuracy respectively.
    514 </p>
    515         
    516 <p>
    517   From within both loops, we take both spherical coordinates to convert them to a 3D Cartesian sample vector, convert the sample from tangent to world space oriented around the normal, and use this sample vector to directly sample the HDR environment map.  We add each sample result to <var>irradiance</var> which at the end we divide by the total number of samples taken, giving us the average sampled irradiance. Note that we scale the sampled color value by <code>cos(theta)</code> due to the light being weaker at larger angles and by <code>sin(theta)</code> to account for the smaller sample areas in the higher hemisphere areas.
    518 </p>
    519               
    520 <p>
    521   Now what's left to do is to set up the OpenGL rendering code such that we can convolute the earlier captured <var>envCubemap</var>. First we create the irradiance cubemap (again, we only have to do this once before the render loop):
    522 </p>
    523       
    524 <pre><code>
    525 unsigned int irradianceMap;
    526 <function id='50'>glGenTextures</function>(1, &irradianceMap);
    527 <function id='48'>glBindTexture</function>(GL_TEXTURE_CUBE_MAP, irradianceMap);
    528 for (unsigned int i = 0; i &lt; 6; ++i)
    529 {
    530     <function id='52'>glTexImage2D</function>(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB16F, 32, 32, 0, 
    531                  GL_RGB, GL_FLOAT, nullptr);
    532 }
    533 <function id='15'>glTexParameter</function>i(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    534 <function id='15'>glTexParameter</function>i(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    535 <function id='15'>glTexParameter</function>i(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
    536 <function id='15'>glTexParameter</function>i(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    537 <function id='15'>glTexParameter</function>i(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    538 </code></pre>
    539               
    540 <p>
    541   As the irradiance map averages all surrounding radiance uniformly it doesn't have a lot of high frequency details, so we can store the map at a low resolution (32x32) and let OpenGL's linear filtering do most of the work. Next, we re-scale the capture framebuffer to the new resolution:
    542 </p>
    543       
    544 <pre class="cpp"><code>
    545 <function id='77'>glBindFramebuffer</function>(GL_FRAMEBUFFER, captureFBO);
    546 <function id='83'>glBindRenderbuffer</function>(GL_RENDERBUFFER, captureRBO);
    547 <function id='88'>glRenderbufferStorage</function>(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 32, 32);  
    548 </code></pre>
    549         
    550 <p>
    551    Using the convolution shader, we render the environment map in a similar way to how we captured the environment cubemap:
    552 </p>
    553         
    554 <pre><code>
    555 irradianceShader.use();
    556 irradianceShader.setInt("environmentMap", 0);
    557 irradianceShader.setMat4("projection", captureProjection);
    558 <function id='49'>glActiveTexture</function>(GL_TEXTURE0);
    559 <function id='48'>glBindTexture</function>(GL_TEXTURE_CUBE_MAP, envCubemap);
    560 
    561 <function id='22'>glViewport</function>(0, 0, 32, 32); // don't forget to configure the viewport to the capture dimensions.
    562 <function id='77'>glBindFramebuffer</function>(GL_FRAMEBUFFER, captureFBO);
    563 for (unsigned int i = 0; i &lt; 6; ++i)
    564 {
    565     irradianceShader.setMat4("view", captureViews[i]);
    566     <function id='81'>glFramebufferTexture2D</function>(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, 
    567                            GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, irradianceMap, 0);
    568     <function id='10'>glClear</function>(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    569 
    570     renderCube();
    571 }
    572 <function id='77'>glBindFramebuffer</function>(GL_FRAMEBUFFER, 0);  
    573 </code></pre>
    574   
    575 <p>
    576   Now after this routine we should have a pre-computed irradiance map that we can directly use for our diffuse image based lighting. To see if we successfully convoluted the environment map we'll substitute the environment map for the irradiance map as the skybox's environment sampler:
    577 </p>
    578       
    579       <img src="/img/pbr/ibl_irradiance_map_background.png" alt="Displaying a PBR irradiance map as the skybox background."/>
    580         
    581 <p>
    582   If it looks like a heavily blurred version of the environment map you've successfully convoluted the environment map.
    583 </p>
    584         
    585 <h2>PBR and indirect irradiance lighting</h2>
    586 <p>
    587   The irradiance map represents the diffuse part of the reflectance integral as accumulated from all surrounding indirect light. Seeing as the light doesn't come from direct light sources, but from the surrounding environment, we treat both the diffuse and specular indirect lighting as the ambient lighting, replacing our previously set constant term. 
    588 </p>
    589         
    590 <p>
    591   First, be sure to add the pre-calculated irradiance map as a cube sampler:
    592 </p>
    593         
    594 <pre><code>
    595 uniform samplerCube irradianceMap;
    596 </code></pre>
    597 
    598 <p>
    599   Given the irradiance map that holds all of the scene's indirect diffuse light, retrieving the irradiance influencing the fragment is as simple as a single texture sample given the surface normal:
    600 </p>
    601         
    602 <pre><code>
    603 // vec3 ambient = vec3(0.03);
    604 vec3 ambient = texture(irradianceMap, N).rgb;
    605 </code></pre>
    606         
    607 <p>
    608   However, as the indirect lighting contains both a diffuse and specular part (as we've seen from the split version of the reflectance equation) we need to weigh the diffuse part accordingly. Similar to what we did in the previous chapter, we use the Fresnel equation to determine the surface's indirect reflectance ratio from which we derive the refractive (or diffuse) ratio:
    609 </p>
    610         
    611 <pre><code>
    612 vec3 kS = fresnelSchlick(max(dot(N, V), 0.0), F0);
    613 vec3 kD = 1.0 - kS;
    614 vec3 irradiance = texture(irradianceMap, N).rgb;
    615 vec3 diffuse    = irradiance * albedo;
    616 vec3 ambient    = (kD * diffuse) * ao; 
    617 </code></pre>
    618         
    619 <p>
    620   As the ambient light comes from all directions within the hemisphere oriented around the normal <var>N</var>, there's no single halfway vector to determine the Fresnel response. To still simulate Fresnel, we calculate the Fresnel from the angle between the normal and view vector. However, earlier we used the micro-surface halfway vector, influenced by the roughness of the surface, as input to the Fresnel equation. As we currently don't take roughness into account, the surface's reflective ratio will always end up relatively high. Indirect light follows the same properties of direct light so we expect rougher surfaces to reflect less strongly on the surface edges. Because of this, the indirect Fresnel reflection strength looks off on rough non-metal surfaces (slightly exaggerated for demonstration purposes):
    621 </p>
    622       <img src="/img/pbr/lighting_fresnel_no_roughness.png" alt="The Fresnel equation for IBL without taking roughness into account."/>
    623           
    624 <p>
    625    We can alleviate the issue by injecting a roughness term in the Fresnel-Schlick equation as described by <a href="https://seblagarde.wordpress.com/2011/08/17/hello-world/" target="_blank">Sébastien Lagarde</a>:
    626 </p>
    627           
    628 <pre><code>
    629 vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness)
    630 {
    631     return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
    632 }   
    633 </code></pre>
    634           
    635 <p>
    636   By taking account of the surface's roughness when calculating the Fresnel response, the ambient code ends up as:
    637 </p>
    638         
    639 <pre><code>
    640 vec3 kS = fresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness); 
    641 vec3 kD = 1.0 - kS;
    642 vec3 irradiance = texture(irradianceMap, N).rgb;
    643 vec3 diffuse    = irradiance * albedo;
    644 vec3 ambient    = (kD * diffuse) * ao; 
    645 </code></pre>
    646         
    647 <p>
    648   As you can see, the actual image based lighting computation is quite simple and only requires a single cubemap texture lookup; most of the work is in pre-computing or convoluting the irradiance map. 
    649 </p>
    650           
    651 <p>
    652   If we take the initial scene from the PBR <a href="https://learnopengl.com/PBR/Lighting" target="_blank">lighting</a> chapter, where each sphere has a vertically increasing metallic and a horizontally increasing roughness value, and add the diffuse image based lighting it'll look a bit like this:
    653 </p>
    654           
    655           <img src="/img/pbr/ibl_irradiance_result.png" alt="Result of convoluting an irradiance map in OpenGL used by the PBR shader."/>
    656             
    657 <p>
    658   It still looks a bit weird as the more metallic spheres <strong>require</strong> some form of reflection to properly start looking like metallic surfaces (as metallic surfaces don't reflect diffuse light) which at the moment are only (barely) coming from the point light sources. Nevertheless, you can already tell the spheres do feel more <em>in place</em> within the environment (especially if you switch between environment maps) as the surface response reacts accordingly to the environment's ambient lighting.
    659 </p>
    660             
    661 <p>
    662   You can find the complete source code of the discussed topics <a href="/code_viewer_gh.php?code=src/6.pbr/2.1.2.ibl_irradiance/ibl_irradiance.cpp" target="_blank">here</a>. In the <a href="https://learnopengl.com/PBR/IBL/Specular-IBL"  target="_blank">next</a> chapter we'll add the indirect specular part of the reflectance integral at which point we're really going to see the power of PBR.
    663 </p>
    664   
    665 <h2>Further reading</h2>
    666 <ul>
    667   <li><a href="http://www.codinglabs.net/article_physically_based_rendering.aspx" target="_blank">Coding Labs: Physically based rendering</a>: an introduction to PBR and how and why to generate an irradiance map. </li>
    668   <li><a href="http://www.scratchapixel.com/lessons/mathematics-physics-for-computer-graphics/mathematics-of-shading" target="_blank">The Mathematics of Shading</a>: a brief introduction by ScratchAPixel on several of the mathematics described in this tutorial, specifically on polar coordinates and integrals.</li>
    669 </ul>       
    670 
    671     </div>
    672