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; // <vec2 position, vec2 texCoords> 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 < 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 < 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 < 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->State == GAME_ACTIVE) 181 { 182 Effects->BeginRender(); 183 // draw background 184 // draw level 185 // draw player 186 // draw particles 187 // draw ball 188 Effects->EndRender(); 189 Effects->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->Levels[this->Level].Bricks) 213 { 214 if (!box.Destroyed) 215 { 216 Collision collision = CheckCollision(*Ball, box); 217 if (std::get<0>(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->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 > 0.0f) 244 { 245 ShakeTime -= dt; 246 if (ShakeTime <= 0.0f) 247 Effects->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>