LearnOpenGL

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

Basic-Lighting.html (26113B)


      1     <h1 id="content-title">Basic Lighting</h1>
      2 <h1 id="content-url" style='display:none;'>Lighting/Basic-Lighting</h1>
      3 <p>
      4   Lighting in the real world is extremely complicated and depends on way too many factors, something we can't afford to calculate on the limited processing power we have. Lighting in OpenGL is therefore based on approximations of reality using simplified models that are much easier to process and look relatively similar. These lighting models are based on the physics of light as we understand it. One of those models is called the <def>Phong lighting model</def>. The major building blocks of the Phong lighting model consist of 3 components: ambient, diffuse and specular lighting. Below you can see what these lighting components look like on their own and combined:
      5 </p>
      6 
      7 <img src="/img/lighting/basic_lighting_phong.png"/>
      8 
      9 <p>  
     10   <ul>
     11     <li><def>Ambient lighting</def>: even when it is dark there is usually still some light somewhere in the world (the moon, a distant light) so objects are almost never completely dark. To simulate this we use an ambient lighting constant that always gives the object some color.</li>
     12     <li><def>Diffuse lighting</def>: simulates the directional impact a light object has on an object. This is the most visually significant component of the lighting model. The more a part of an object faces the light source, the brighter it becomes.</li>
     13     <li><def>Specular lighting</def>: simulates the bright spot of a light that appears on shiny objects. Specular highlights are more inclined to the color of the light than the color of the object.</li>
     14   </ul>
     15 </p>
     16 
     17 <p>
     18   To create visually interesting scenes we want to at least simulate these 3 lighting components. We'll start with the simplest one: <em>ambient lighting</em>.
     19 </p>
     20 
     21 <h1>Ambient lighting</h1>
     22 <p>
     23   Light usually does not come from a single light source, but from many light sources scattered all around us, even when they're not immediately visible. One of the properties of light is that it can scatter and bounce in many directions, reaching spots that aren't directly visible; light can thus <em>reflect</em> on other surfaces and have an indirect impact on the lighting of an object. Algorithms that take this into consideration are called <def>global illumination</def> algorithms, but these are complicated and expensive to calculate.
     24 </p>
     25 
     26 <p>
     27   Since we're not big fans of complicated and expensive algorithms we'll start by using a very simplistic model of global illumination, namely <def>ambient lighting</def>. As you've seen in the previous section we use a small constant (light) color that we add to the final resulting color of the object's fragments, thus making it look like there is always some scattered light even when there's not a direct light source.
     28 </p>
     29 
     30 <p>
     31   Adding ambient lighting to the scene is really easy. We take the light's color, multiply it with a small constant ambient factor, multiply this with the object's color, and use that as the fragment's color in the cube object's shader:
     32 </p>
     33 
     34 <pre><code>
     35 void main()
     36 {
     37     float ambientStrength = 0.1;
     38     vec3 ambient = ambientStrength * lightColor;
     39 
     40     vec3 result = ambient * objectColor;
     41     FragColor = vec4(result, 1.0);
     42 }  
     43 </code></pre>
     44 
     45 <p>
     46   If you'd now run the program, you'll notice that the first stage of lighting is now successfully applied to the object. The object is quite dark, but not completely since ambient lighting is applied (note that the light cube is unaffected because we use a different shader). It should look something like this:
     47 </p>
     48 
     49 <img src="/img/lighting/ambient_lighting.png" class="clean"/>
     50 
     51 <h1>Diffuse lighting</h1>
     52 <p>
     53 	Ambient lighting by itself doesn't produce the most interesting results, but diffuse lighting however will start to give a significant visual impact on the object. Diffuse lighting gives the object more brightness the closer its fragments are aligned to the light rays from a light source. To give you a better understanding of diffuse lighting take a look at the following image:
     54 </p>
     55 
     56 <img src="/img/lighting/diffuse_light.png" class="clean"/>
     57 
     58 <p>
     59   To the left we find a light source with a light ray targeted at a single fragment of our object. We  need to measure at what angle the light ray touches the fragment. If the light ray is perpendicular to the object's surface the light has the greatest impact. To measure the angle between the light ray and the fragment we use something called a <def>normal vector</def>, that is a vector  perpendicular to the fragment's surface (here depicted as a yellow arrow); we'll get to that later. The angle between the two vectors can then easily be calculated with the dot product.
     60 </p>
     61 
     62 <p>
     63   You may remember from the <a href="https://learnopengl.com/Getting-started/Transformations" target="_blank">transformations</a> chapter that, the lower the angle  between two unit vectors, the more the dot product is inclined towards a value of 1. When the angle between both vectors is 90 degrees, the dot product becomes 0. The same applies to \(\theta\): the larger \(\theta\) becomes, the less of an impact the light should have on the fragment's color.
     64 </p>
     65 
     66 <note>
     67   Note that to get (only) the cosine of the angle between both vectors we will work with <em>unit vectors</em> (vectors of length <code>1</code>) so we need to make sure all the vectors are normalized, otherwise the dot product returns more than just the cosine (see <a href="https://learnopengl.com/Getting-started/Transformations" target="_blank">Transformations</a>).
     68 </note>
     69 
     70 <p>
     71   The resulting dot product thus returns a scalar that we can use to calculate the light's impact on the fragment's color, resulting in differently lit fragments based on their orientation towards the light.
     72 </p>
     73 
     74 <p>
     75   So, what do we need to calculate diffuse lighting:
     76   <ul>
     77     <li>Normal vector: a vector that is perpendicular to the vertex' surface.</li>
     78     <li>The directed light ray: a direction vector that is the difference vector between the light's position and the fragment's position. To calculate this light ray we need the light's position vector and the fragment's position vector.</li>
     79   </ul>
     80 </p>
     81 
     82 <h2>Normal vectors</h2>
     83 <p>
     84   A normal vector is a (unit) vector that is perpendicular to the surface of a vertex. Since a vertex by itself has no surface (it's just a single point in space) we retrieve a normal vector by using its surrounding vertices to figure out the surface of the vertex. We can use a little trick to calculate the normal vectors for all the cube's vertices by using the cross product, but since a 3D cube is not a complicated shape we can simply manually add them to the vertex data. The updated vertex data array can be found <a href="/code_viewer.php?code=lighting/basic_lighting_vertex_data" target="_blank">here</a>. Try to visualize that the normals are indeed vectors perpendicular to each plane's surface (a cube consists of 6 planes).
     85 </p>
     86 
     87 <p>
     88   Since we added extra data to the vertex array we should update the cube's vertex shader:
     89 </p>
     90 
     91 <pre><code>
     92 #version 330 core
     93 layout (location = 0) in vec3 aPos;
     94 layout (location = 1) in vec3 aNormal;
     95 ...
     96 </code></pre>
     97 
     98 <p>
     99   Now that we added a normal vector to each of the vertices and updated the vertex shader we should update the vertex attribute pointers as well. Note that the light source's cube uses the same vertex array for its vertex data, but the lamp shader has no use of the newly added normal vectors. We don't have to update the lamp's shaders or attribute configurations, but we have to at least modify the vertex attribute pointers to reflect the new vertex array's size:
    100 </p>
    101 
    102 <pre><code>
    103 <function id='30'>glVertexAttribPointer</function>(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
    104 <function id='29'><function id='60'>glEnable</function>VertexAttribArray</function>(0);
    105 </code></pre>
    106 
    107 <p>
    108   We only want to use the first <code>3</code> floats of each vertex and ignore the last <code>3</code> floats so we only need to update the <em>stride</em> parameter to <code>6</code> times the size of a <code>float</code> and we're done.
    109 </p>
    110 
    111 <note>
    112    It may look inefficient using vertex data that is not completely used by the lamp shader, but the vertex data is already stored in the GPU's memory from the container object so we don't have to store new data into the GPU's memory. This actually makes it more efficient compared to allocating a new VBO specifically for the lamp.
    113 </note>
    114 
    115 <p>
    116  All the lighting calculations are done in the fragment shader so we need to forward the normal vectors from the vertex shader to the fragment shader. Let's do that:
    117 </p>
    118 
    119 <pre><code>
    120 out vec3 Normal;
    121 
    122 void main()
    123 {
    124     gl_Position = projection * view * model * vec4(aPos, 1.0);
    125     Normal = aNormal;
    126 } 
    127 </code></pre>
    128 
    129 <p>
    130   What's left to do is declare the corresponding input variable in the fragment shader:
    131 </p>
    132 
    133 <pre><code>
    134 in vec3 Normal;  
    135 </code></pre>
    136 
    137 <h2>Calculating the diffuse color</h2>
    138 <p>
    139   We now have the normal vector for each vertex, but we still need the light's position vector and the fragment's position vector. Since the light's position is a single static variable we can declare it as a uniform in the fragment shader:
    140 </p>
    141 
    142 <pre><code>
    143 uniform vec3 lightPos;  
    144 </code></pre>
    145 
    146 <p>
    147   And then update the uniform in the render loop (or outside since it doesn't change per frame). We use the <var>lightPos</var> vector declared in the previous chapter as the location of the diffuse light source:
    148 </p>
    149 
    150 <pre><code>
    151 lightingShader.setVec3("lightPos", lightPos);  
    152 </code></pre>
    153 
    154 <p>
    155   Then the last thing we need is the actual fragment's position. We're going to do all the lighting calculations in world space so we want a vertex position that is in world space first. We can accomplish this by multiplying the vertex position attribute with the model matrix only (not the view and projection matrix) to transform it to world space coordinates. This can easily be accomplished in the vertex shader so let's declare an output variable and calculate its world space coordinates:
    156 </p>
    157 
    158 <pre><code>
    159 out vec3 FragPos;  
    160 out vec3 Normal;
    161   
    162 void main()
    163 {
    164     gl_Position = projection * view * model * vec4(aPos, 1.0);
    165     FragPos = vec3(model * vec4(aPos, 1.0));
    166     Normal = aNormal;
    167 }
    168 </code></pre>
    169 
    170 <p> 
    171   And lastly add the corresponding input variable to the fragment shader:
    172 </p>
    173 
    174 <pre><code>
    175 in vec3 FragPos;  
    176 </code></pre>
    177 
    178 <p>
    179   This <code>in</code> variable will be interpolated from the 3 world position vectors of the triangle to form the <var>FragPos</var> vector that is the per-fragment world position.   Now that all the required variables are set we can start the lighting calculations.
    180 </p>
    181 
    182 
    183 <p>
    184   The first thing we need to calculate is the direction vector between the light source and the fragment's position. From the previous section we know that the light's direction vector is the difference vector between the light's position vector and the fragment's position vector. As you may remember from the <a href="https://learnopengl.com/Getting-started/Transformations" target="_blank">transformations</a> chapter we can easily calculate this difference by subtracting both vectors from each other. We also want to make sure all the relevant vectors end up as unit vectors so we normalize both the normal and the resulting direction vector:
    185 </p>
    186 
    187 <pre><code>
    188 vec3 norm = normalize(Normal);
    189 vec3 lightDir = normalize(lightPos - FragPos);  
    190 </code></pre>
    191 
    192 <note>
    193   When calculating lighting we usually do not care about the magnitude of a vector or their position; we only care about their direction. Because we only care about their direction almost all the calculations are done with unit vectors since it simplifies most calculations (like the dot product). So when doing lighting calculations, make sure you always normalize the relevant vectors to ensure they're actual unit vectors. Forgetting to normalize a vector is a popular mistake.
    194 </note>
    195 
    196 <p>
    197   Next we need to calculate the diffuse impact of the light on the current fragment by taking the dot product between the <var>norm</var> and <var>lightDir</var> vectors. The resulting value is then multiplied with the light's color to get the diffuse component, resulting in a darker diffuse component the greater the angle between both vectors:
    198 </p>
    199 
    200 <pre><code>
    201 float diff = max(dot(norm, lightDir), 0.0);
    202 vec3 diffuse = diff * lightColor;
    203 </code></pre>
    204 
    205 <p>
    206   If the angle between both vectors is greater than <code>90</code> degrees then the result of the dot product will actually become negative and we end up with a negative diffuse component.
    207   For that reason we use the <fun>max</fun> function that returns the highest of both its parameters to make sure the diffuse component (and thus the colors) never become negative. Lighting for negative colors is not really defined so it's best to stay away from that, unless you're one of those eccentric artists. 
    208 </p>
    209 
    210 <p>
    211   Now that we have both an ambient and a diffuse component we add both colors to each other and then multiply the result with the color of the object to get the resulting fragment's output color:
    212 </p>
    213 
    214 <pre><code>
    215 vec3 result = (ambient + diffuse) * objectColor;
    216 FragColor = vec4(result, 1.0);
    217 </code></pre>
    218 
    219 <p>
    220   If your application (and shaders) compiled successfully you should see something like this:
    221 </p>
    222 
    223 <img src="/img/lighting/basic_lighting_diffuse.png" class="clean"/>
    224 
    225 <p>
    226   You can see that with diffuse lighting the cube starts to look like an actual cube again. Try visualizing the normal vectors in your head and move the camera around the cube to see that the larger the angle between the normal vector and the light's direction vector, the darker the fragment becomes.
    227 </p>
    228 
    229 <p>
    230   Feel free to compare your source code with the complete source code <a href="/code_viewer_gh.php?code=src/2.lighting/2.1.basic_lighting_diffuse/basic_lighting_diffuse.cpp" target="_blank">here</a> if you're stuck.
    231 </p>
    232 
    233 <h2>One last thing</h2>
    234 <p>
    235   in the previous section we passed the normal vector directly from the vertex shader to the fragment shader. However, the calculations in the fragment shader are all done in world space, so shouldn't we transform the normal vectors to world space coordinates as well? Basically yes, but it's not as simple as simply multiplying it with a model matrix. 
    236 </p>
    237 
    238 <p>
    239   First of all, normal vectors are only direction vectors and do not represent a specific position in space. Second, normal vectors do not have a homogeneous coordinate (the <code>w</code> component of a vertex position). This means that translations should not have any effect on the normal vectors. So if we want to multiply the normal vectors with a model matrix we want to remove the translation part of the matrix by taking the upper-left <code>3x3</code> matrix of the model matrix (note that we could also set the <code>w</code> component of a normal vector to <code>0</code> and multiply with the 4x4 matrix). 
    240 </p>
    241 
    242 <p>
    243  Second, if the model matrix would perform a non-uniform scale, the vertices would be changed in such a way that the normal vector is not perpendicular to the surface anymore. The following image shows the effect such a model matrix (with non-uniform scaling) has on a normal vector:
    244 </p>
    245 
    246 <img src="/img/lighting/basic_lighting_normal_transformation.png" class="clean"/>
    247 
    248 <p>
    249   Whenever we apply a non-uniform scale (note: a uniform scale only changes the normal's magnitude, not its direction, which is easily fixed by normalizing it) the normal vectors are not perpendicular to the corresponding surface anymore which distorts the lighting.
    250 </p>
    251 
    252 <p>
    253   The trick of fixing this behavior is to use a different model matrix specifically tailored for normal vectors. This matrix is called the <def>normal matrix</def> and uses a few linear algebraic operations to remove the effect of wrongly scaling the normal vectors. If you want to know how this matrix is calculated I suggest the following <a href="http://www.lighthouse3d.com/tutorials/glsl-tutorial/the-normal-matrix/" target="_blank">article</a>. 
    254 </p>
    255 
    256 <p>
    257   The normal matrix is defined as 'the transpose of the inverse of the upper-left 3x3 part of the model matrix'. Phew, that's a mouthful and if you don't really understand what that means, don't worry; we haven't discussed inverse and transpose matrices yet. Note that most resources define the normal matrix as derived from the model-view matrix, but since we're working in world space (and not in view space) we will derive it from the model matrix.
    258 </p>
    259 
    260 <p>
    261   In the vertex shader we can generate the normal matrix by using the <fun>inverse</fun> and <fun>transpose</fun> functions in the vertex shader that work on any matrix type. Note that we cast the matrix to a 3x3 matrix to ensure it loses its translation properties and that it can multiply with the <code>vec3</code> normal vector:
    262 </p>
    263 
    264 <pre><code>
    265 Normal = mat3(transpose(inverse(model))) * aNormal;  
    266 </code></pre>
    267 
    268 <warning>
    269   Inversing matrices is a costly operation for shaders, so wherever possible try to avoid doing inverse operations since they have to be done on each vertex of your scene. For learning purposes this is fine, but for an efficient application you'll likely want to calculate the normal matrix on the CPU and send it to the shaders via a uniform before drawing (just like the model matrix).
    270 </warning>
    271 
    272 <p>
    273   In the diffuse lighting section the lighting was fine because we didn't do any scaling on the object, so there was not really a need to use a normal matrix and we could've just multiplied the normals with the model matrix. If you are doing a non-uniform scale however, it is essential that you multiply your normal vectors with the normal matrix.
    274 </p>
    275 
    276 
    277 <h1>Specular Lighting</h1>
    278 <p>
    279   If you're not exhausted already by all the lighting talk we can start finishing the Phong lighting model by adding specular highlights.
    280 </p>
    281 
    282 <p>
    283   Similar to diffuse lighting, specular lighting is based on the light's direction vector and the object's normal vectors, but this time it is also based on the view direction e.g. from what direction the player is looking at the fragment. Specular lighting is based on the reflective properties of surfaces. If we think of the object's surface as a mirror, the specular lighting is the strongest wherever we would see the light reflected on the surface. You can see this effect in the following image:  
    284 </p>
    285 
    286 <img src="/img/lighting/basic_lighting_specular_theory.png" class="clean"/>
    287 
    288 <p>
    289   We calculate a reflection vector by reflecting the light direction around the normal vector. Then we calculate the angular distance between this reflection vector and the view direction. The closer the angle between them, the greater the impact of the specular light. The resulting effect is that we see a bit of a highlight when we're looking at the light's direction reflected via the surface. 
    290 </p>
    291 
    292 <p>
    293   The view vector is the one extra variable we need for specular lighting which we can calculate using the viewer's world space position and the fragment's position. Then we calculate the specular's intensity, multiply this with the light color and add this to the ambient and diffuse components.
    294 </p>
    295 
    296 <note>
    297   We chose to do the lighting calculations in world space, but most people tend to prefer doing lighting in view space. An advantage of view space is that the viewer's position is always at <code>(0,0,0)</code> so you already got the position of the viewer for free. However, I find calculating lighting in world space more intuitive for learning purposes. If you still want to calculate lighting in view space you want to transform all the relevant vectors with the view matrix as well (don't forget to change the normal matrix too).
    298 </note>
    299 
    300 <p>
    301   To get the world space coordinates of the viewer we simply take the position vector of the camera object (which is the viewer of course). So let's add another uniform to the fragment shader and pass the camera position vector to the shader:
    302 </p>
    303 
    304 <pre><code>
    305 uniform vec3 viewPos;
    306 </code></pre>
    307 
    308 <pre><code>
    309 lightingShader.setVec3("viewPos", camera.Position); 
    310 </code></pre>
    311 
    312 <p>
    313   Now that we have all the required variables we can calculate the specular intensity. First we define a specular intensity value to give the specular highlight a medium-bright color so that it doesn't have too much of an impact:
    314 </p>
    315 
    316 <pre><code>
    317 float specularStrength = 0.5;
    318 </code></pre>
    319 
    320 <p>
    321   If we would set this to <code>1.0f</code> we'd get a really bright specular component which is a bit too much for a coral cube. In the <a href="https://learnopengl.com/Lighting/Materials" target="_blank">next</a> chapter we'll talk about properly setting all these lighting intensities and how they affect the objects. Next we calculate the view direction vector and the corresponding reflect vector along the normal axis:
    322 </p>
    323 
    324 <pre><code>
    325 vec3 viewDir = normalize(viewPos - FragPos);
    326 vec3 reflectDir = reflect(-lightDir, norm);  
    327 </code></pre>
    328 
    329 <p>
    330   Note that we negate the <code>lightDir</code> vector. The <code>reflect</code> function expects the first vector to point <strong>from</strong> the light source towards the fragment's position, but the <code>lightDir</code> vector is currently pointing the other way around: from the fragment  <strong>towards</strong> the light source (this depends on the order of subtraction earlier on when we calculated the <code>lightDir</code> vector). To make sure we get the correct <code>reflect</code> vector we reverse its direction by negating the <code>lightDir</code> vector first. The second argument expects a normal vector so we supply the normalized <code>norm</code> vector.
    331 </p>
    332 
    333 <p>
    334   Then what's left to do is to actually calculate the specular component. This is accomplished with the following formula:
    335 
    336 <pre><code>
    337 float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
    338 vec3 specular = specularStrength * spec * lightColor;  
    339 </code></pre>
    340 
    341 <p>
    342   We first calculate the dot product between the view direction and the reflect direction (and make sure it's not negative) and then raise it to the power of <code>32</code>. This <code>32</code> value is the <def>shininess</def> value of the highlight. The higher the shininess value of an object, the more it properly reflects the light instead of scattering it all around and thus the smaller the highlight becomes. Below you can see an image that shows the visual impact of different shininess values:
    343 </p>
    344 
    345 <img src="/img/lighting/basic_lighting_specular_shininess.png"/>
    346 
    347 <p>
    348   We don't want the specular component to be too distracting so we keep the exponent at <code>32</code>. The only thing left to do is to add it to the ambient and diffuse components and multiply the combined result with the object's color:
    349 </p>
    350 
    351 <pre><code>
    352 vec3 result = (ambient + diffuse + specular) * objectColor;
    353 FragColor = vec4(result, 1.0);
    354 </code></pre>
    355 
    356 <p>
    357 	We now calculated all the lighting components of the Phong lighting model. Based on your point of view you should see something like this:
    358 </p>
    359 
    360 <img src="/img/lighting/basic_lighting_specular.png" class="clean"/>
    361 
    362 <p>
    363   You can find the complete source code of the application <a href="/code_viewer_gh.php?code=src/2.lighting/2.2.basic_lighting_specular/basic_lighting_specular.cpp" target="_blank">here</a>. 
    364 </p>
    365 
    366 <note>
    367   <p>
    368   In the earlier days of lighting shaders, developers used to implement the Phong lighting model in the vertex shader. The advantage of doing lighting in the vertex shader is that it is a lot more efficient since there are generally a lot less vertices compared to fragments, so the (expensive) lighting calculations are done less frequently. However, the resulting color value in the vertex shader is the resulting lighting color of that vertex only and the color values of the surrounding fragments are then the result of interpolated lighting colors. The result was that the lighting was not very realistic unless large amounts of vertices were used:
    369   </p>
    370   
    371   <img src="/img/lighting/basic_lighting_gouruad.png"/>
    372   
    373   <p>
    374   When the Phong lighting model is implemented in the vertex shader it is called <def>Gouraud shading</def> instead of <def>Phong shading</def>. Note that due to the interpolation the lighting  looks somewhat off. The Phong shading gives much smoother lighting results.
    375   </p>
    376 </note>
    377 
    378 <p>
    379   By now you should be starting to see just how powerful shaders are. With little information shaders are able to calculate how lighting affects the fragment's colors for all our objects. In the <a href="https://learnopengl.com/Lighting/Materials" target="_blank">next</a> chapters we'll be delving much deeper into what we can do with the lighting model.
    380 </p>
    381 
    382 <h2>Exercises</h2>
    383 <ul>
    384   <li>Right now the light source is a boring static light source that doesn't move. Try to move the light source around the scene over time using either <fun>sin</fun> or <fun>cos</fun>. Watching the lighting change over time gives you a good understanding of Phong's lighting model: <a href="/code_viewer_gh.php?code=src/2.lighting/2.3.basic_lighting_exercise1/basic_lighting_exercise1.cpp" target="_blank">solution</a>. </li>
    385   <li>Play around with different ambient, diffuse and specular strengths and see how they impact the result. Also experiment with the shininess factor. Try to comprehend why certain values have a certain visual output.</li>
    386   <li>Do Phong shading in view space instead of world space: <a href="/code_viewer_gh.php?code=src/2.lighting/2.4.basic_lighting_exercise2/basic_lighting_exercise2.cpp" target="_blank">solution</a>. </li>
    387   <li>Implement Gouraud shading instead of Phong shading. If you did things right the lighting should <a href="/img/lighting/basic_lighting_exercise3.png" target="_blank">look a bit  off</a> (especially the specular highlights) with the cube object. Try to reason why it looks so weird: <a href="/code_viewer_gh.php?code=src/2.lighting/2.5.basic_lighting_exercise3/basic_lighting_exercise3.cpp" target="_blank">solution</a>.</li>
    388 </ul>
    389        
    390 
    391     </div>
    392