LearnOpenGL

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

Particles.html (12960B)


      1     <h1 id="content-title">Particles</h1>
      2 <h1 id="content-url" style='display:none;'>In-Practice/2D-Game/Particles</h1>
      3 <p>
      4   A <def>particle</def>, as seen from OpenGL's perspective, is a tiny 2D quad that is always facing the camera (billboarding) and (usually) contains a texture with large parts of the sprite being transparent. A particle by itself is effectively just a sprite as we've been using extensively so far. However, when you put together hundreds or even thousands of these particles together you can create amazing effects.
      5 </p>
      6 
      7 <p>
      8   When working with particles, there is usually an object called a <def>particle emitter</def> or <def>particle generator</def> that, from its location, continuously <def>spawns</def> new particles  that decay over time. If such a particle emitter would for example spawn tiny particles with a smoke-like texture, color them less bright the larger the distance from the emitter, and give them a glowy appearance, you'd get a fire-like effect:
      9 </p>
     10 
     11 <img src="/img/in-practice/breakout/particles_example.jpg" alt="Example of particles as a fire"/>
     12   
     13 <p>
     14   A single particle often has a life variable that slowly decays once it's spawned. Once its life is less than a certain threshold (usually <code>0</code>), we <def>kill</def> the particle so it can be replaced with a new particle when the next particle spawns. A particle emitter controls all its spawned particles and changes their behavior based on their attributes. A particle generally has the following attributes:
     15 </p>
     16   
     17 <pre><code>
     18 struct Particle {
     19     glm::vec2 Position, Velocity;
     20     glm::vec4 Color;
     21     float     Life;
     22   
     23     Particle() 
     24       : Position(0.0f), Velocity(0.0f), Color(1.0f), Life(0.0f) { }
     25 };    
     26 </code></pre>
     27   
     28 <p>
     29   Looking at the fire example, the particle emitter probably spawns each particle with a position close to the emitter and with an upwards velocity. It seems to have 3 different regions, so it probably gives some particles a higher velocity than others. We can also see that the higher the <code>y</code> position of the particle, the less <em>yellow</em> or <em>bright</em> its color becomes. After the particles have reached a certain height, their life is depleted and the particles are killed; never reaching the stars.
     30 </p>
     31   
     32 <p>
     33   You can imagine that with systems like these we can create interesting effects like fire, smoke, fog, magic effects, gunfire residue etc. In Breakout, we're going to add a simple particle generator that follows the ball to make it all look just a bit more interesting. It'll look something like this:
     34 </p>
     35   
     36 <div class="video paused" onclick="ClickVideo(this)">
     37   <video width="600" height="450" loop>
     38     <source src="/video/in-practice/breakout/particles.mp4" type="video/mp4" />
     39     <img src="/img/in-practice/breakout/particles.png" class="clean"/>
     40   </video>
     41 </div>
     42   
     43 <p>
     44   Here, the particle generator spawns each particle at the ball's position, gives it a velocity equal to a fraction of the ball's velocity, and changes the color of the particle based on how long it lived. 
     45 </p>
     46   
     47 <p>
     48   For rendering the particles we'll be using a different set of shaders:
     49 </p>
     50   
     51 <pre><code>
     52 #version 330 core
     53 layout (location = 0) in vec4 vertex; // &lt;vec2 position, vec2 texCoords&gt;
     54 
     55 out vec2 TexCoords;
     56 out vec4 ParticleColor;
     57 
     58 uniform mat4 projection;
     59 uniform vec2 offset;
     60 uniform vec4 color;
     61 
     62 void main()
     63 {
     64     float scale = 10.0f;
     65     TexCoords = vertex.zw;
     66     ParticleColor = color;
     67     gl_Position = projection * vec4((vertex.xy * scale) + offset, 0.0, 1.0);
     68 }
     69 </code></pre>
     70 
     71 <p>
     72   And the fragment shader:
     73 </p>
     74   
     75 <pre><code>
     76 #version 330 core
     77 in vec2 TexCoords;
     78 in vec4 ParticleColor;
     79 out vec4 color;
     80 
     81 uniform sampler2D sprite;
     82 
     83 void main()
     84 {
     85     color = (texture(sprite, TexCoords) * ParticleColor);
     86 }  
     87 </code></pre>
     88 
     89 <p>
     90   We take the standard position and texture attributes per particle and also accept an <var>offset</var> and a <var>color</var> uniform for changing the outcome per particle. Note that in the vertex shader we scale the particle quad by <code>10.0f</code>; you can also set the scale as a uniform and control this individually per particle.
     91 </p>
     92   
     93 <p>
     94   First, we need a list of particles that we instantiate with default <fun>Particle</fun> structs:
     95 </p>
     96   
     97 <pre><code>
     98 unsigned int nr_particles = 500;
     99 std::vector&lt;Particle&gt; particles;
    100   
    101 for (unsigned int i = 0; i &lt; nr_particles; ++i)
    102     particles.push_back(Particle());
    103 </code></pre>
    104   
    105 <p>
    106   Then in each frame, we spawn several new particles with starting values. For each particle that is (still) alive we also update their values:
    107 </p>
    108   
    109 <pre><code>
    110 unsigned int nr_new_particles = 2;
    111 // add new particles
    112 for (unsigned int i = 0; i &lt; nr_new_particles; ++i)
    113 {
    114     int unusedParticle = FirstUnusedParticle();
    115     RespawnParticle(particles[unusedParticle], object, offset);
    116 }
    117 // update all particles
    118 for (unsigned int i = 0; i &lt; nr_particles; ++i)
    119 {
    120     Particle &p = particles[i];
    121     p.Life -= dt; // reduce life
    122     if (p.Life &gt; 0.0f)
    123     {	// particle is alive, thus update
    124         p.Position -= p.Velocity * dt;
    125         p.Color.a -= dt * 2.5f;
    126     }
    127 }  
    128 </code></pre>
    129   
    130 <p>
    131   The first loop may look a little daunting. As particles die over time we want to spawn <var>nr_new_particles</var> particles each frame, but since we don't want to infinitely keep spawning new particles (we'll quickly run out of memory this way) we only spawn up to a max of <var>nr_particles</var>. If were to push all new particles to the end of the list we'll quickly get a list filled with thousands of particles. This isn't really efficient considering only a small portion of that list has particles that are alive.
    132 </p>
    133   
    134 <p>
    135   What we want is to find the first particle that is dead (life &lt; <code>0.0f</code>) and update  that particle as a new respawned particle.
    136 </p>
    137 
    138 <p>
    139   The function <fun>FirstUnusedParticle</fun> tries to find the first particle that is dead and returns its index to the caller.    
    140   </p>
    141 
    142 <pre><code>
    143 unsigned int lastUsedParticle = 0;
    144 unsigned int FirstUnusedParticle()
    145 {
    146     // search from last used particle, this will usually return almost instantly
    147     for (unsigned int i = lastUsedParticle; i &lt; nr_particles; ++i) {
    148         if (particles[i].Life &lt;= 0.0f){
    149             lastUsedParticle = i;
    150             return i;
    151         }
    152     }
    153     // otherwise, do a linear search
    154     for (unsigned int i = 0; i &lt; lastUsedParticle; ++i) {
    155         if (particles[i].Life &lt;= 0.0f){
    156             lastUsedParticle = i;
    157             return i;
    158         }
    159     }
    160     // override first particle if all others are alive
    161     lastUsedParticle = 0;
    162     return 0;
    163 }  
    164 </code></pre>
    165   
    166 <p>
    167   The function stores the index of the last dead particle it found. Since the next dead particle will most likely be right after the last particle index, we first search from this stored index. If we found no dead particles this way, we simply do a slower linear search. If no particles are dead, it will return index <code>0</code> which results in the first particle being overwritten. Note that if it reaches this last case, it means your particles are alive for too long; you'd need to spawn less particles per frame and/or reserve a larger number of particles.
    168 </p>
    169   
    170 <p>
    171   Then, once the first dead particle in the list is found, we update its values by calling <fun>RespawnParticle</fun> that takes the particle, a <fun>GameObject</fun>, and an offset vector:
    172 </p>
    173   
    174 <pre><code>
    175 void RespawnParticle(Particle &particle, GameObject &object, glm::vec2 offset)
    176 {
    177     float random = ((rand() % 100) - 50) / 10.0f;
    178     float rColor = 0.5f + ((rand() % 100) / 100.0f);
    179     particle.Position = object.Position + random + offset;
    180     particle.Color = glm::vec4(rColor, rColor, rColor, 1.0f);
    181     particle.Life = 1.0f;
    182     particle.Velocity = object.Velocity * 0.1f;
    183 }  
    184 </code></pre>
    185   
    186 <p>
    187   This function simply resets the particle's life to <code>1.0f</code>, randomly gives it a brightness (via the color vector) starting from <code>0.5</code>, and assigns a (slightly random) position and velocity based on the game object's data.
    188 </p>
    189   
    190 <p>
    191   The second particle loop within the update function loops over all particles and for each particle reduces their life by the delta time variable; this way, each particle's life corresponds to exactly the second(s) it's allowed to live multiplied by some scalar. Then we check if the particle is alive and if so, update its position and color attributes. We also slowly reduce the alpha component of each particle so it looks like they're slowly disappearing over time. 
    192 </p>
    193   
    194 <p>
    195   Then what's left to do is render the particles:
    196 </p>
    197   
    198 <pre><code>
    199 <function id='70'>glBlendFunc</function>(GL_SRC_ALPHA, GL_ONE);
    200 particleShader.Use();
    201 for (Particle particle : particles)
    202 {
    203     if (particle.Life &gt; 0.0f)
    204     {
    205         particleShader.SetVector2f("offset", particle.Position);
    206         particleShader.SetVector4f("color", particle.Color);
    207         particleTexture.Bind();
    208         <function id='27'>glBindVertexArray</function>(particleVAO);
    209         <function id='1'>glDrawArrays</function>(GL_TRIANGLES, 0, 6);
    210         <function id='27'>glBindVertexArray</function>(0);
    211     } 
    212 } 
    213 <function id='70'>glBlendFunc</function>(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    214 </code></pre>
    215   
    216 <p>
    217   Here, for each particle, we set their offset and color uniform values, bind the texture, and render the 2D quad. What's interesting to note here are the two calls to <fun><function id='70'>glBlendFunc</function></fun>. When rendering the particles, instead of the default destination blend mode of <code>GL_ONE_MINUS_SRC_ALPHA</code>, we use the <code>GL_ONE</code> (additive) blend mode that gives the particles a very neat <def>glow effect</def> when stacked onto each other. This is also likely the blend mode used when rendering the fire at the top of the chapter, since the fire is more 'glowy' at the center where most of the particles are. 
    218 </p>
    219   
    220 <p>
    221   Because we (like most other parts of the Breakout chapters) like to keep things organized, we create another class called <fun>ParticleGenerator</fun> that hosts all the functionality we just described. You can find the source code below:
    222 </p>
    223   
    224 <ul>
    225   <li><a href="/code_viewer_gh.php?code=src/7.in_practice/3.2d_game/0.full_source/particle_generator.h" target="_blank">header</a>, <a href="/code_viewer_gh.php?code=src/7.in_practice/3.2d_game/0.full_source/particle_generator.cpp" target="_blank">code</a></li>
    226 </ul>
    227   
    228 <p>
    229   Within the game code, we create a particle generator and initialize it with <a href="/img/in-practice/breakout/textures/particle.png" target="_blank">this</a> texture.
    230 </p>
    231   
    232 <pre><code>
    233 ParticleGenerator   *Particles; 
    234 
    235 void Game::Init()
    236 {
    237     [...]
    238     ResourceManager::LoadShader("shaders/particle.vs", "shaders/particle.frag", nullptr, "particle");
    239     [...]
    240     ResourceManager::LoadTexture("textures/particle.png", true, "particle"); 
    241     [...]
    242     Particles = new ParticleGenerator(
    243         ResourceManager::GetShader("particle"), 
    244         ResourceManager::GetTexture("particle"), 
    245         500
    246     );
    247 }
    248 </code></pre>
    249   
    250 <p>
    251   Then we change the game class's <fun>Update</fun> function by adding an update statement for the particle generator:
    252 </p>
    253   
    254 <pre><code>
    255 void Game::Update(float dt)
    256 {
    257     [...]
    258     // update particles
    259     Particles-&gt;Update(dt, *Ball, 2, glm::vec2(Ball-&gt;Radius / 2.0f));
    260     [...]
    261 }
    262 </code></pre>
    263   
    264 <p>
    265   Each of the particles will use the game object properties from the ball object, spawn 2 particles each frame, and their positions will be offset towards the center of the ball. Last up is rendering the particles:
    266 </p>
    267   
    268 <pre><code>
    269 void Game::Render()
    270 {
    271     if (this-&gt;State == GAME_ACTIVE)
    272     {
    273         [...]
    274         // draw player
    275         Player-&gt;Draw(*Renderer);
    276         // draw particles	
    277         Particles-&gt;Draw();
    278         // draw ball
    279         Ball-&gt;Draw(*Renderer);
    280     }
    281 }  
    282 </code></pre>
    283   
    284 <p>
    285   Note that we render the particles before we render the ball. This way, the particles end up rendered in front of all other objects, but behind the ball. You can find the updated game class code <a href="/code_viewer_gh.php?code=src/7.in_practice/3.2d_game/0.full_source/progress/6.game.cpp" target="_blank">here</a>.
    286 </p>
    287     
    288 <p>
    289   If you'd now compile and run your application you should see a trail of particles following the ball, just like at the beginning of the chapter, giving the game a more modern look. The system can also easily be extended to host more advanced effects, so feel free to experiment with the particle generation and see if you can come up with your own creative effects.
    290 </p>       
    291 
    292     </div>
    293     
    294     <div id="hover">
    295         HI
    296     </div>
    297    <!-- 728x90/320x50 sticky footer -->
    298 <div id="waldo-tag-6196"></div>
    299 
    300    <div id="disqus_thread"></div>
    301 
    302     
    303 
    304 
    305 </div> <!-- container div -->
    306 
    307 
    308 </div> <!-- super container div -->
    309 </body>
    310 </html>