LearnOpenGL

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

Postprocessing.html (11340B)


      1     <h1 id="content-title">Postprocessing</h1>
      2 <h1 id="content-url" style='display:none;'>In-Practice/2D-Game/Postprocessing</h1>
      3 <p>
      4   Wouldn't it be fun if we could completely spice up the visuals of the Breakout game with just a few postprocessing effects? We could create a blurry shake effect, inverse all the colors of the scene, do crazy vertex movement, and/or make use of other interesting effects with relative ease thanks to OpenGL's framebuffers.
      5 </p>
      6 
      7 <note>
      8   This chapters makes extensive use of concepts from the <a href="https://learnopengl.com/Advanced-OpenGL/Framebuffers" target="_blank">framebuffers</a> and <a href="https://learnopengl.com!Advanced-OpenGL/Anti-Aliasing" target="_blank">anti-aliasing</a> chapters. 
      9 </note>
     10 
     11 <p>
     12   In the framebuffers chapter we demonstrated how we could use postprocessing to achieve interesting effects using just a single texture. In Breakout we're going to do something similar: we're going to create a framebuffer object with a multisampled renderbuffer object attached as its color attachment. All the game's render code should render to this multisampled framebuffer that then blits its content to a different framebuffer with a texture attachment as its color buffer. This texture contains the rendered anti-aliased image of the game that we'll render to a full-screen 2D quad with zero or more postprocessing effects applied. 
     13 </p>
     14 
     15 <p>
     16   So to summarize, the rendering steps are:
     17 </p>
     18 
     19 <ol>
     20   <li>Bind to multisampled framebuffer.</li>
     21   <li>Render game as normal.</li>
     22   <li>Blit multisampled framebuffer to normal framebuffer with texture attachment.</li>
     23   <li>Unbind framebuffer (use default framebuffer).</li>
     24   <li>Use color buffer texture from normal framebuffer in postprocessing shader.</li>
     25   <li>Render quad of screen-size as output of postprocessing shader.</li>  
     26 </ol>
     27 
     28 <p>
     29   The postprocessing shader allows for three type of effects: shake, confuse, and chaos.
     30 </p>
     31 
     32 <ul>
     33   <li><strong>shake</strong>: slightly shakes the scene with a small blur.</li>
     34   <li><strong>confuse</strong>: inverses the colors of the scene, but also the <code>x</code> and <code>y</code> axis.</li>
     35   <li><strong>chaos</strong>: makes use of an edge detection kernel to create interesting visuals and also moves the textured image in a circular fashion for an interesting <em>chaotic</em> effect.</li>
     36 </ul>
     37 
     38 
     39 <p>
     40   Below is a glimpse of what these effects are going to look like:
     41 </p>
     42 
     43 <img src="/img/in-practice/breakout/postprocessing_effects.png" alt="Postprocessing effects in OpenGL Breakout game"/>
     44 
     45 <p>
     46   Operating on a 2D quad, the vertex shader looks as follows:
     47 </p>
     48 
     49 <pre><code>
     50 #version 330 core
     51 layout (location = 0) in vec4 vertex; // &lt;vec2 position, vec2 texCoords&gt;
     52 
     53 out vec2 TexCoords;
     54 
     55 uniform bool  chaos;
     56 uniform bool  confuse;
     57 uniform bool  shake;
     58 uniform float time;
     59 
     60 void main()
     61 {
     62     gl_Position = vec4(vertex.xy, 0.0f, 1.0f); 
     63     vec2 texture = vertex.zw;
     64     if (chaos)
     65     {
     66         float strength = 0.3;
     67         vec2 pos = vec2(texture.x + sin(time) * strength, texture.y + cos(time) * strength);        
     68         TexCoords = pos;
     69     }
     70     else if (confuse)
     71     {
     72         TexCoords = vec2(1.0 - texture.x, 1.0 - texture.y);
     73     }
     74     else
     75     {
     76         TexCoords = texture;
     77     }
     78     if (shake)
     79     {
     80         float strength = 0.01;
     81         gl_Position.x += cos(time * 10) * strength;        
     82         gl_Position.y += cos(time * 15) * strength;        
     83     }
     84 }  
     85 </code></pre>
     86   
     87 <p>
     88   Based on whatever uniform is set to <code>true</code>, the vertex shader takes different paths. If either <var>chaos</var> or <var>confuse</var> is set to <code>true</code>, the vertex shader will manipulate the texture coordinates to move the scene around (either translate texture coordinates in a circle-like fashion, or inverse them). Because we set the texture wrapping methods to <code>GL_REPEAT</code>, the chaos effect will cause the scene to repeat itself at various parts of the quad. Additionally if <var>shake</var> is set to <code>true</code>, it will move the vertex positions around by a small amount, as if the screen shakes. Note that <var>chaos</var> and <var>confuse</var> shouldn't be <code>true</code> at the same time while <var>shake</var> is able to work with any of the other effects on.
     89 </p>
     90 
     91 <p>
     92   In addition to offsetting the vertex positions or texture coordinates, we'd also like to create some visual change as soon as any of the effects are active. We can accomplish this within the fragment shader:
     93 </p>
     94 
     95 <pre><code>
     96 #version 330 core
     97 in  vec2  TexCoords;
     98 out vec4  color;
     99   
    100 uniform sampler2D scene;
    101 uniform vec2      offsets[9];
    102 uniform int       edge_kernel[9];
    103 uniform float     blur_kernel[9];
    104 
    105 uniform bool chaos;
    106 uniform bool confuse;
    107 uniform bool shake;
    108 
    109 void main()
    110 {
    111     color = vec4(0.0f);
    112     vec3 sample[9];
    113     // sample from texture offsets if using convolution matrix
    114     if(chaos || shake)
    115         for(int i = 0; i &lt; 9; i++)
    116             sample[i] = vec3(texture(scene, TexCoords.st + offsets[i]));
    117 
    118     // process effects
    119     if (chaos)
    120     {           
    121         for(int i = 0; i &lt; 9; i++)
    122             color += vec4(sample[i] * edge_kernel[i], 0.0f);
    123         color.a = 1.0f;
    124     }
    125     else if (confuse)
    126     {
    127         color = vec4(1.0 - texture(scene, TexCoords).rgb, 1.0);
    128     }
    129     else if (shake)
    130     {
    131         for(int i = 0; i &lt; 9; i++)
    132             color += vec4(sample[i] * blur_kernel[i], 0.0f);
    133         color.a = 1.0f;
    134     }
    135     else
    136     {
    137         color =  texture(scene, TexCoords);
    138     }
    139 }
    140 
    141 </code></pre>
    142 
    143 <p>
    144   This long shader almost directly builds upon the fragment shader from the framebuffers chapter and processes several postprocessing effects based on the effect type activated. This time though, the offset matrix and convolution kernels are defined as a uniform that we set from the OpenGL code. The advantage is that we only have to set this once, instead of recalculating these matrices each fragment shader run. For example, the <var>offsets</var> matrix is configured as follows:
    145 </p>
    146   
    147 <pre><code>
    148 float offset = 1.0f / 300.0f;
    149 float offsets[9][2] = {
    150     { -offset,  offset  },  // top-left
    151     {  0.0f,    offset  },  // top-center
    152     {  offset,  offset  },  // top-right
    153     { -offset,  0.0f    },  // center-left
    154     {  0.0f,    0.0f    },  // center-center
    155     {  offset,  0.0f    },  // center - right
    156     { -offset, -offset  },  // bottom-left
    157     {  0.0f,   -offset  },  // bottom-center
    158     {  offset, -offset  }   // bottom-right    
    159 };
    160 <function id='44'>glUniform</function>2fv(<function id='45'>glGetUniformLocation</function>(shader.ID, "offsets"), 9, (float*)offsets);  
    161 </code></pre>
    162 
    163 <p>
    164   Since all of the concepts of managing (multisampled) framebuffers were already extensively discussed in earlier chapters, I won't delve into the details this time. Below you'll find the code of a <fun>PostProcessor</fun> class that manages initialization, writing/reading the framebuffers, and rendering a screen quad. You should be able to understand the code if you understood the framebuffers and anti-aliasing chapter:
    165 </p>
    166 
    167 <ul>
    168   <li><strong>PostProcessor</strong>: <a href="/code_viewer_gh.php?code=src/7.in_practice/3.2d_game/0.full_source/post_processor.h" target="_blank">header</a>, <a href="/code_viewer_gh.php?code=src/7.in_practice/3.2d_game/0.full_source/post_processor.cpp" target="_blank">code</a>.</li>
    169 </ul>
    170 
    171 <p>
    172   What is interesting to note here are the <fun>BeginRender</fun> and <fun>EndRender</fun> functions. Since we have to render the entire game scene into the framebuffer we can conventiently call <fun>BeginRender()</fun> and <fun>EndRender()</fun> before and after the scene's rendering code respectively. The class will then handle the behind-the-scenes framebuffer operations. For example, using the <fun>PostProcessor</fun> class will look like this within the game's <fun>Render</fun> function:
    173 </p>
    174 
    175 <pre><code>
    176 PostProcessor   *Effects;
    177   
    178 void Game::Render()
    179 {
    180     if (this-&gt;State == GAME_ACTIVE)
    181     {
    182         Effects-&gt;BeginRender();
    183             // draw background
    184             // draw level
    185             // draw player
    186             // draw particles	
    187             // draw ball
    188         Effects-&gt;EndRender();
    189         Effects-&gt;Render(<function id='47'>glfwGetTime</function>());
    190     }
    191 }
    192 </code></pre>
    193 
    194 <p>
    195   Wherever we want, we can now conveniently set the required effect property of the postprocessing class to <code>true</code> and its effect will be immediately active.
    196 </p>
    197 
    198 <h3>Shake it</h3>
    199 <p>
    200   As a (practical) demonstration of these effects we'll emulate the visual impact of the ball when it hits a solid concrete block. By enabling the shake effect for a short period of time wherever a solid collision occurs, it'll look like the collision had a stronger impact.
    201 </p>
    202   
    203 <p>
    204    We want to enable the screen shake effect only over a small period of time. We can get this to work by creating a variable called <var>ShakeTime</var> that manages the duration the shake effect is supposed to be active. Wherever a solid collision occurs, we reset this variable to a specific duration:
    205 </p>
    206   
    207 <pre><code>
    208 float ShakeTime = 0.0f;  
    209 
    210 void Game::DoCollisions()
    211 {
    212     for (GameObject &box : this-&gt;Levels[this-&gt;Level].Bricks)
    213     {
    214         if (!box.Destroyed)
    215         {
    216             Collision collision = CheckCollision(*Ball, box);
    217             if (std::get&lt;0&gt;(collision)) // if collision is true
    218             {
    219                 // destroy block if not solid
    220                 if (!box.IsSolid)
    221                     box.Destroyed = true;
    222                 else
    223                 {   // if block is solid, enable shake effect
    224                     ShakeTime = 0.05f;
    225                     Effects-&gt;Shake = true;
    226                 }
    227                 [...]
    228             }
    229         }    
    230     }
    231     [...]
    232 }  
    233 </code></pre>
    234   
    235 <p>
    236   Then within the game's <fun>Update</fun> function, we decrease the <var>ShakeTime</var> variable until it's <code>0.0</code> after which we disable the shake effect:
    237 </p>
    238   
    239 <pre><code>
    240 void Game::Update(float dt)
    241 {
    242     [...]
    243     if (ShakeTime &gt; 0.0f)
    244     {
    245         ShakeTime -= dt;
    246         if (ShakeTime &lt;= 0.0f)
    247             Effects-&gt;Shake = false;
    248     }
    249 }  
    250 </code></pre>
    251   
    252 <p>
    253   Then each time we hit a solid block, the screen briefly starts to shake and blur, giving the player some visual feedback the ball collided with a solid object. 
    254 </p>
    255   
    256 <div class="video paused" onclick="ClickVideo(this)">
    257   <video width="600" height="450" loop>
    258     <source src="/video/in-practice/breakout/postprocessing_shake.mp4" type="video/mp4" />
    259     <img src="/img/in-practice/breakout/postprocessing_shake.png" class="clean"/>
    260   </video>
    261 </div>
    262   
    263 <p>
    264   You can find the updated source code of the game class <a href="/code_viewer_gh.php?code=src/7.in_practice/3.2d_game/0.full_source/progress/7.game.cpp" target="_blank">here</a>.
    265 </p>
    266   
    267 <p>
    268   In the <a href="https://learnopengl.com/In-Practice/2D-Game/Powerups" target="_blank">next</a> chapter about powerups we'll bring the other two postprocessing effects to good use.
    269 </p>       
    270 
    271     </div>
    272     
    273     <div id="hover">
    274         HI
    275     </div>
    276    <!-- 728x90/320x50 sticky footer -->
    277 <div id="waldo-tag-6196"></div>
    278 
    279    <div id="disqus_thread"></div>
    280 
    281     
    282 
    283 
    284 </div> <!-- container div -->
    285 
    286 
    287 </div> <!-- super container div -->
    288 </body>
    289 </html>