LearnOpenGL

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

Rendering-Sprites.html (13675B)


      1     <h1 id="content-title">Rendering Sprites</h1>
      2 <h1 id="content-url" style='display:none;'>In-Practice/2D-Game/Rendering-Sprites</h1>
      3 <p>
      4   To bring some life to the currently black abyss of our game world, we will render sprites to fill the void. A <def>sprite</def> has many definitions, but it's effectively not much more than a 2D image used together with some data to position it in a larger world (e.g. position, rotation, and size). Basically, sprites are the render-able image/texture objects we use in a 2D game.
      5 </p>
      6 
      7 <p>
      8   We can, just like we did in previous chapters, create a 2D shape out of vertex data, pass all data to the GPU, and transform it all by hand. However, in a larger application like this we rather have some abstractions on rendering 2D shapes. If we were to manually define these shapes and transformations for each object, it'll quickly get messy. 
      9 </p>
     10 
     11 <p>
     12   In this chapter we'll define a rendering class that allows us to render a large amount of unique sprites with a minimal amount of code. This way, we're abstracting the gameplay code from the gritty OpenGL rendering code as is commonly done in larger projects. First, we have to set up a proper projection matrix though.
     13 </p>
     14 
     15 <h2>2D projection matrix</h2>
     16 <p>
     17   We know from the <a href="https://learnopengl.com/Getting-started/Coordinate-Systems" target="_blank">coordinate systems</a> chapter that a projection matrix converts all view-space coordinates to clip-space (and then to normalized device) coordinates. By generating the appropriate projection matrix we can  work with different coordinates that are easier to work with, compared to directly specifying all coordinates as normalized device coordinates.  
     18 </p>
     19 
     20 <p>
     21   We don't need any perspective applied to the coordinates, since the game is entirely in 2D, so an orthographic projection matrix would suit the rendering quite well. Because an orthographic projection matrix directly transforms all coordinates to normalized device coordinates, we can choose to specify the world coordinates as screen coordinates by defining the projection matrix as follows:
     22 </p>
     23 
     24 <pre><code>
     25 glm::mat4 projection = <function id='59'>glm::ortho</function>(0.0f, 800.0f, 600.0f, 0.0f, -1.0f, 1.0f);  
     26 </code></pre>
     27 
     28 <p>
     29   The first four arguments specify in order the left, right, bottom, and top part of the projection frustum. This projection matrix transforms all <code>x</code> coordinates between <code>0</code> and <code>800</code> to <code>-1</code> and <code>1</code>, and all <code>y</code> coordinates between <code>0</code> and <code>600</code> to <code>-1</code> and <code>1</code>. Here we specified that the top of the frustum has a <code>y</code> coordinate of <code>0</code>, while the bottom has a <code>y</code> coordinate of <code>600</code>. The result is that the top-left coordinate of the scene will be at (<code>0,0</code>) and the bottom-right part of the screen is at coordinate (<code>800,600</code>), just like screen coordinates; the world-space coordinates directly correspond to the resulting pixel coordinates.
     30 </p>
     31 
     32 <img src="/img/in-practice/breakout/projection.png" class="clean" alt="Orthographic projection in OpenGL"/>
     33 
     34 <p>
     35   This allows us to specify all vertex coordinates equal to the pixel coordinates they end up in on the screen, which is rather intuitive for 2D games.
     36 </p>
     37 
     38 <h2>Rendering sprites</h2>
     39 <p>
     40   Rendering an actual sprite shouldn't be too complicated. We create a textured quad that we can transform with a model matrix, after which we project it using the previously defined orthographic projection matrix.
     41 </p>
     42 
     43 <note>
     44   Since Breakout is a single-scene game, there is no need for a view/camera matrix. Using the projection matrix we can directly transform the world-space coordinates to normalized device coordinates.
     45 </note>
     46 
     47 <p>
     48   To transform a sprite, we use the following vertex shader:
     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 
     57 uniform mat4 model;
     58 uniform mat4 projection;
     59 
     60 void main()
     61 {
     62     TexCoords = vertex.zw;
     63     gl_Position = projection * model * vec4(vertex.xy, 0.0, 1.0);
     64 }
     65 </code></pre>
     66 
     67 <p>
     68   Note that we store both the position and texture-coordinate data in a single <fun>vec4</fun> variable. Because both the position and texture coordinates contain two floats, we can combine them in a single vertex attribute.
     69 </p>
     70 
     71 <p>
     72   The fragment shader is relatively straightforward as well. We take a texture and a color vector that both affect the final color of the fragment. By having a uniform color vector, we can easily change the color of sprites from the game-code:
     73 </p>
     74 
     75 <pre><code>
     76 #version 330 core
     77 in vec2 TexCoords;
     78 out vec4 color;
     79 
     80 uniform sampler2D image;
     81 uniform vec3 spriteColor;
     82 
     83 void main()
     84 {    
     85     color = vec4(spriteColor, 1.0) * texture(image, TexCoords);
     86 }  
     87 </code></pre>
     88 
     89 <p>
     90   To make the rendering of sprites more organized, we define a <fun>SpriteRenderer</fun> class that is able to render a sprite with just a single function. Its definition is as follows:
     91 </p>
     92 
     93 <pre><code>
     94 class SpriteRenderer
     95 {
     96     public:
     97         SpriteRenderer(Shader &shader);
     98         ~SpriteRenderer();
     99 
    100         void DrawSprite(Texture2D &texture, glm::vec2 position, 
    101             glm::vec2 size = glm::vec2(10.0f, 10.0f), float rotate = 0.0f, 
    102             glm::vec3 color = glm::vec3(1.0f));
    103     private:
    104         Shader       shader; 
    105         unsigned int quadVAO;
    106 
    107         void initRenderData();
    108 };
    109 </code></pre>
    110 
    111 <p>
    112   The <def>SpriteRenderer</def> class hosts a shader object, a single vertex array object, and a render and initialization function. Its constructor takes a shader object that it uses for all future rendering.
    113 </p>
    114 
    115 <h3>Initialization</h3>
    116 <p>
    117   First, let's delve into the <fun>initRenderData</fun> function that configures the <var>quadVAO</var>:
    118 </p>
    119 
    120 <pre><code>
    121 void SpriteRenderer::initRenderData()
    122 {
    123     // configure VAO/VBO
    124     unsigned int VBO;
    125     float vertices[] = { 
    126         // pos      // tex
    127         0.0f, 1.0f, 0.0f, 1.0f,
    128         1.0f, 0.0f, 1.0f, 0.0f,
    129         0.0f, 0.0f, 0.0f, 0.0f, 
    130     
    131         0.0f, 1.0f, 0.0f, 1.0f,
    132         1.0f, 1.0f, 1.0f, 1.0f,
    133         1.0f, 0.0f, 1.0f, 0.0f
    134     };
    135 
    136     <function id='33'>glGenVertexArrays</function>(1, &this-&gt;quadVAO);
    137     <function id='12'>glGenBuffers</function>(1, &VBO);
    138     
    139     <function id='32'>glBindBuffer</function>(GL_ARRAY_BUFFER, VBO);
    140     <function id='31'>glBufferData</function>(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    141 
    142     <function id='27'>glBindVertexArray</function>(this-&gt;quadVAO);
    143     <function id='29'><function id='60'>glEnable</function>VertexAttribArray</function>(0);
    144     <function id='30'>glVertexAttribPointer</function>(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
    145     <function id='32'>glBindBuffer</function>(GL_ARRAY_BUFFER, 0);  
    146     <function id='27'>glBindVertexArray</function>(0);
    147 }
    148 </code></pre>
    149 
    150 <p>
    151   Here we first define a set of vertices with (<code>0,0</code>) being the top-left corner of the quad. This means that when we apply translation or scaling transformations on the quad, they're transformed from the top-left position of the quad. This is commonly accepted in 2D graphics and/or GUI systems where elements' positions correspond to the top-left corner of the elements.
    152 </p>
    153 
    154 <p>
    155   Next we simply sent the vertices to the GPU and configure the vertex attributes, which in this case is a single vertex attribute. We only have to define a single VAO for the sprite renderer since all sprites share the same vertex data.
    156 </p>
    157 
    158 <h3>Rendering</h3>
    159 <p>
    160   Rendering sprites is not too difficult; we use the sprite renderer's shader, configure a model matrix, and set the relevant uniforms. What is important here is the order of transformations:
    161 </p>
    162 
    163 <pre><code>
    164 void SpriteRenderer::DrawSprite(Texture2D &texture, glm::vec2 position, 
    165   glm::vec2 size, float rotate, glm::vec3 color)
    166 {
    167     // prepare transformations
    168     this-&gt;shader.Use();
    169     glm::mat4 model = glm::mat4(1.0f);
    170     model = <function id='55'>glm::translate</function>(model, glm::vec3(position, 0.0f));  
    171 
    172     model = <function id='55'>glm::translate</function>(model, glm::vec3(0.5f * size.x, 0.5f * size.y, 0.0f)); 
    173     model = <function id='57'>glm::rotate</function>(model, <function id='63'>glm::radians</function>(rotate), glm::vec3(0.0f, 0.0f, 1.0f)); 
    174     model = <function id='55'>glm::translate</function>(model, glm::vec3(-0.5f * size.x, -0.5f * size.y, 0.0f));
    175 
    176     model = <function id='56'>glm::scale</function>(model, glm::vec3(size, 1.0f)); 
    177   
    178     this-&gt;shader.SetMatrix4("model", model);
    179     this-&gt;shader.SetVector3f("spriteColor", color);
    180   
    181     <function id='49'>glActiveTexture</function>(GL_TEXTURE0);
    182     texture.Bind();
    183 
    184     <function id='27'>glBindVertexArray</function>(this-&gt;quadVAO);
    185     <function id='1'>glDrawArrays</function>(GL_TRIANGLES, 0, 6);
    186     <function id='27'>glBindVertexArray</function>(0);
    187 }  
    188 </code></pre>
    189 
    190 <p>
    191   When trying to position objects somewhere in a scene with rotation and scaling transformations, it is advised to first scale, then rotate, and finally translate the object. Because multiplying matrices occurs from right to left, we transform the matrix in reverse order: translate, rotate, and then scale. 
    192 </p>
    193 
    194 <p>
    195   The rotation transformation may still seem a bit daunting. We know from the <a href="https://learnopengl.com/Getting-started/Transformations" target="_blank">transformations</a> chapter that rotations always revolve around the origin (<code>0,0</code>). Because we specified the quad's vertices with (<code>0,0</code>) as the top-left coordinate, all rotations will rotate around this point of (<code>0,0</code>). The <def>origin of rotation</def> is at the top-left of the quad, which produces undesirable results. What we want to do is move the origin of rotation to the center of the quad so the quad neatly rotates around this origin, instead of rotating around the top-left of the quad. We solve this by translating the quad by half its size first, so its center is at coordinate (<code>0,0</code>) before rotating.
    196 </p>
    197 
    198 <img src="/img/in-practice/breakout/rotation-origin.png" class="clean" alt="Properly rotating at the center of origin of the quad"/>
    199 
    200 <p>
    201   Since we first scale the quad, we have to take the size of the sprite into account when translating to the sprite's center, which is why we multiply with the sprite's <var>size</var> vector. Once the rotation transformation is applied, we reverse the previous translation.
    202 </p>
    203 
    204 <p>
    205   Combining all these transformations, we can position, scale, and rotate each sprite in any way we like. Below you can find the complete source code of the sprite renderer:
    206 </p>
    207 
    208 <ul>
    209   <li><strong>SpriteRenderer</strong>: <a href="/code_viewer_gh.php?code=src/7.in_practice/3.2d_game/0.full_source/sprite_renderer.h" target="_blank">header</a>, <a href="/code_viewer_gh.php?code=src/7.in_practice/3.2d_game/0.full_source/sprite_renderer.cpp" target="_blank">code</a> </li>
    210 </ul>
    211 
    212 <h2>Hello sprite</h2>
    213 <p>
    214   With the <fun>SpriteRenderer</fun> class we finally have the ability to render actual images to the screen! Let's initialize one within the game code and load our favorite <a href="/img/textures/awesomeface.png" target="_blank">texture</a> while we're at it:
    215 </p>
    216 
    217 <pre><code>
    218 SpriteRenderer  *Renderer;
    219   
    220 void Game::Init()
    221 {
    222     // load shaders
    223     ResourceManager::LoadShader("shaders/sprite.vs", "shaders/sprite.frag", nullptr, "sprite");
    224     // configure shaders
    225     glm::mat4 projection = <function id='59'>glm::ortho</function>(0.0f, static_cast&lt;float&gt;(this-&gt;Width), 
    226         static_cast&lt;float&gt;(this-&gt;Height), 0.0f, -1.0f, 1.0f);
    227     ResourceManager::GetShader("sprite").Use().SetInteger("image", 0);
    228     ResourceManager::GetShader("sprite").SetMatrix4("projection", projection);
    229     // set render-specific controls
    230     Renderer = new SpriteRenderer(ResourceManager::GetShader("sprite"));
    231     // load textures
    232     ResourceManager::LoadTexture("textures/awesomeface.png", true, "face");
    233 }
    234 </code></pre>
    235 
    236 <p>
    237   Then within the render function we can render our beloved mascot to see if everything works as it should:
    238 </p>
    239 
    240 <pre><code>
    241 void Game::Render()
    242 {
    243     Renderer-&gt;DrawSprite(ResourceManager::GetTexture("face"), 
    244         glm::vec2(200.0f, 200.0f), glm::vec2(300.0f, 400.0f), 45.0f, glm::vec3(0.0f, 1.0f, 0.0f));
    245 }  
    246 </code></pre>
    247 
    248 <p>
    249   Here we position the sprite somewhat close to the center of the screen with its height being slightly larger than its width. We also rotate it by 45 degrees and give it a green color. Note that the position we give the sprite equals the top-left vertex of the sprite's quad.
    250 </p>
    251 
    252 <p>
    253   If you did everything right you should get the following output:
    254 </p>
    255 
    256 <img src="/img/in-practice/breakout/rendering-sprites.png" class="clean" alt="Image of a rendered sprite using our custom-made OpenGL's SpriteRenderer class"/>
    257 
    258 <p>
    259   You can find the updated game class's source code <a href="/code_viewer_gh.php?code=src/7.in_practice/3.2d_game/0.full_source/progress/3.game.cpp" target="_blank">here</a>.
    260 </p>
    261 
    262 <p>
    263   Now that we got the render systems working, we can put it to good use in the <a href="https://learnopengl.com/In-Practice/2D-Game/Levels" target="_blank">next</a> chapter where  we'll work on building the game's levels.
    264 </p>       
    265 
    266     </div>
    267     
    268     <div id="hover">
    269         HI
    270     </div>
    271    <!-- 728x90/320x50 sticky footer -->
    272 <div id="waldo-tag-6196"></div>
    273 
    274    <div id="disqus_thread"></div>
    275 
    276     
    277 
    278 
    279 </div> <!-- container div -->
    280 
    281 
    282 </div> <!-- super container div -->
    283 </body>
    284 </html>