{"id":479,"date":"2021-12-04T21:09:39","date_gmt":"2021-12-04T21:09:39","guid":{"rendered":"https:\/\/writingagame.com\/?p=479"},"modified":"2023-05-15T20:44:01","modified_gmt":"2023-05-15T20:44:01","slug":"chapter-12-graphics-engine-basic-classes","status":"publish","type":"post","link":"https:\/\/writingagame.com\/index.php\/2021\/12\/04\/chapter-12-graphics-engine-basic-classes\/","title":{"rendered":"Chapter 12. Graphics Engine. Basic classes"},"content":{"rendered":"\n<p>At this point we successfully married desktop and mobile, Visual Studio and OpenGL ES. Now we are starting to implement our <strong>graphics engine<\/strong>. Sure, it can be done in many different ways. <\/p>\n\n\n\n<p><strong>Warning<\/strong>: All following materials represent <strong>my personal vision<\/strong> of this topic.<\/p>\n\n\n\n<p>We&#8217;ll start from basic classes. Our current <em>TheGame <\/em>code renders 1 simple subject, a textured rectangle. In the real game we will need an ARRAY of game subjects (like maybe tanks, terrain, trees, UI elements, etc.). Each of them will have it&#8217;s own behavior and rendering instructions. <em>TheGame <\/em>class shouldn&#8217;t care about all these details. It supposed to scan subjects array and invoke corresponding classes and functions which should reside <strong>outside <\/strong>of <em>TheGame <\/em>class.<\/p>\n\n\n\n<p>Let&#8217;s start dissecting <em>TheGame <\/em>class to build a set of basic classes that will go to a real Project. Let&#8217;s start on<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Windows<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>In <strong>Windows File Explorer<\/strong> make a copy of <em>a999hello <\/em>folder and rename it (copy) to <strong>a998engine<\/strong>.<\/li>\n<\/ol>\n\n\n\n<p>Start VS. Open <em>C:\\CPP\\<strong>a998engine<\/strong>\\p_windows\\p_windows.sln<\/em>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-css-opacity\"\/>\n\n\n\n<p><strong>Texture class<\/strong><br>It should have basic texture info, such as sizes and GL texture id, plus some related functions. Also we want to have here a list of all loaded textures to avoid duplications.<\/p>\n\n\n\n<ol class=\"wp-block-list\" start=\"2\">\n<li>Under <em>xEngine <\/em>add new header (.h) file <strong>Texture.h<\/strong><\/li>\n<\/ol>\n\n\n\n<p>Location &#8211; <em>C:\\CPP\\engine<\/em><\/p>\n\n\n\n<p>Code:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; title: ; notranslate\" title=\"\">\n#pragma once\n#include &lt;string&gt;\n#include &lt;vector&gt;\n\nclass Texture\n{\npublic:\n    \/\/texture&#039;s individual descriptor:\n    unsigned int GLid = -1; \/\/ GL texture id\n    int size&#91;2] = { 0,0 };  \/\/ image size\n    std::string source; \/\/file name\n    \/\/end of descriptor\n\n    \/\/static array (vector) of all loaded textures\n    static std::vector&lt;Texture*&gt; textures;\n\npublic:\n    static int loadTexture(std::string filePath);\n    static int findTexture(std::string filePath);\n    static int cleanUp();\n    static unsigned int getGLid(int texN) { return textures.at(texN)-&gt;GLid; };\n};\n\n<\/pre><\/div>\n\n\n<p><\/p>\n\n\n\n<hr class=\"wp-block-separator has-css-opacity\"\/>\n\n\n\n<ol class=\"wp-block-list\" start=\"3\">\n<li>Under <em>xEngine <\/em>add new C++ (.cpp) file <strong>Texture.cpp<\/strong><\/li>\n<\/ol>\n\n\n\n<p>Location &#8211; <em>C:\\CPP\\engine<\/em><\/p>\n\n\n\n<p>Code:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; title: ; notranslate\" title=\"\">\n#include &quot;Texture.h&quot;\n#define STB_IMAGE_IMPLEMENTATION  \/\/required by stb_image.h\n#include &quot;stb_image.h&quot;\n#include &quot;platform.h&quot;\n\n\/\/static array (vector) of all loaded textures\nstd::vector&lt;Texture*&gt; Texture::textures;\n\nint Texture::loadTexture(std::string filePath) {\n    int texN = findTexture(filePath);\n    if (texN &gt;= 0)\n        return texN;\n    \/\/if here - texture wasn&#039;t loaded\n    \/\/create Texture object\n    Texture* pTex = new Texture();\n    textures.push_back(pTex);\n    pTex-&gt;source.assign(filePath);\n    \/\/ load an image\n    int nrChannels;\n    unsigned char* imgData = stbi_load(filePath.c_str(),\n        &amp;pTex-&gt;size&#91;0], &amp;pTex-&gt;size&#91;1], &amp;nrChannels, 4); \/\/&quot;4&quot;-convert to 4 channels -RGBA\n    if (imgData == NULL) {\n        mylog(&quot;ERROR in Texture::loadTexture loading image %s\\n&quot;, filePath.c_str());\n    }\n    \/\/ generate texture\n    glGenTextures(1, &amp;pTex-&gt;GLid);\n    glBindTexture(GL_TEXTURE_2D, pTex-&gt;GLid);\n    \/\/ set the texture wrapping\/filtering options (on the currently bound texture object)\n    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);\n    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);\n    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);\n    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);\n    \/\/ attach\/load image data\n    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, pTex-&gt;size&#91;0], pTex-&gt;size&#91;1], 0, GL_RGBA, GL_UNSIGNED_BYTE, imgData);\n    glGenerateMipmap(GL_TEXTURE_2D);\n    \/\/ release image data\n    stbi_image_free(imgData);\n\n    return (textures.size() - 1);\n}\nint Texture::findTexture(std::string filePath) {\n    int texturesN = textures.size();\n    if (texturesN &lt; 1)\n        return -1;\n    for (int i = 0; i &lt; texturesN; i++) {\n        Texture* pTex = textures.at(i);\n        if (pTex-&gt;source.compare(filePath) == 0)\n            return i;\n    }\n    return -1;\n}\nint Texture::cleanUp() {\n    int texturesN = textures.size();\n    if (texturesN &lt; 1)\n        return -1;\n    \/\/detach all textures\n    glActiveTexture(GL_TEXTURE0); \/\/ activate the texture unit first before binding texture\n    glBindTexture(GL_TEXTURE_2D, 0);\n    glActiveTexture(GL_TEXTURE1);\n    glBindTexture(GL_TEXTURE_2D, 0);\n    glActiveTexture(GL_TEXTURE2);\n    glBindTexture(GL_TEXTURE_2D, 0);\n    glActiveTexture(GL_TEXTURE3);\n    glBindTexture(GL_TEXTURE_2D, 0);\n    \/\/release all textures\n    for (int i = 0; i &lt; texturesN; i++) {\n        Texture* pTex = textures.at(i);\n        glDeleteTextures(1, &amp;pTex-&gt;GLid);\n        delete pTex;\n    }\n    textures.clear();\n    return 1;\n}\n\n<\/pre><\/div>\n\n\n<p><\/p>\n\n\n\n<hr class=\"wp-block-separator has-css-opacity\"\/>\n\n\n\n<p><strong>Shader class<\/strong><\/p>\n\n\n\n<p>Right now it&#8217;s just a set of a few static functions, but it supposed to encapsulate all shader-related variables, such as <em>umvp_location, apos_location, atuv_location, shaderProgramId<\/em> of course (we&#8217;ll call them a bit differently), plus related functionality.<\/p>\n\n\n\n<p>4. Open <em>Shader.h<\/em> and replace code by following:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; title: ; notranslate\" title=\"\">\n#pragma once\n#include &quot;platform.h&quot;\n#include &lt;string&gt;\n#include &lt;vector&gt;\n\nclass Shader\n{\npublic:\n    \/\/Shader program&#039;s individual descriptor:\n    unsigned int GLid = -1; \/\/ GL shader id\n    \/\/common variables, &quot;l_&quot; for &quot;location&quot;\n    int l_uMVP; \/\/ transform matrix (Model-View-Projection)\n    int l_aPos; \/\/attribute position (3D coordinates)\n    int l_aTuv; \/\/attribute TUV (texture coordinates)\n    int l_uColor;\n    int l_uTex0; \/\/texture id\n    \/\/end of descriptor\n\n    \/\/static array (vector) of all loaded shaders\n    static std::vector&lt;Shader*&gt; shaders;\n    \/\/common shader programs (&quot;spN&quot; - shader program number)\n    static int spN_flat_ucolor;\n    static int spN_flat_tex;\n\npublic:\n    static int loadShaders();\n    static int loadShader(std::string filePathVertexS, std::string filePathFragmentS);\n    static int cleanUp();\n    static unsigned int getGLid(int shN) { return shaders.at(shN)-&gt;GLid; };\n    \n    static int linkShaderProgram(const char* filePathVertexS, const char* filePathFragmentS);\n\tstatic int compileShader(const char* filePath, GLenum shaderType);\n\tstatic int shaderErrorCheck(int shaderId, std::string ref);\n\tstatic int programErrorCheck(int programId, std::string ref);\n};\n\n<\/pre><\/div>\n\n\n<p><\/p>\n\n\n\n<hr class=\"wp-block-separator has-css-opacity\"\/>\n\n\n\n<p>5.  Open <em>Shader.cpp<\/em> and replace code by following: <\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; title: ; notranslate\" title=\"\">\n#include &quot;Shader.h&quot;\n#include &quot;platform.h&quot;\n#include &quot;utils.h&quot;\n\nextern std::string filesRoot;\n\n\/\/static array (vector) of all loaded shaders\nstd::vector&lt;Shader*&gt; Shader::shaders;\n\/\/common shader programs (&quot;spN&quot; - shader program number)\nint Shader::spN_flat_ucolor = -1;\nint Shader::spN_flat_tex = -1;\n\nint Shader::loadShaders() {\n    spN_flat_ucolor = loadShader(&quot;\/dt\/shaders\/flat_ucolor_v.txt&quot;, &quot;\/dt\/shaders\/flat_ucolor_f.txt&quot;);\n    spN_flat_tex = loadShader(&quot;\/dt\/shaders\/flat_tex_v.txt&quot;, &quot;\/dt\/shaders\/flat_tex_f.txt&quot;);\n    return 1;\n}\nint Shader::loadShader(std::string filePathVertexS, std::string filePathFragmentS) {\n    \/\/create shader object\n    Shader* pSh = new Shader();\n    shaders.push_back(pSh);\n    pSh-&gt;GLid = linkShaderProgram((filesRoot + filePathVertexS).c_str(), (filesRoot + filePathFragmentS).c_str());\n    \/\/common variables. If not presented, = -1;\n    pSh-&gt;l_uMVP = glGetUniformLocation(pSh-&gt;GLid, &quot;uMVP&quot;); \/\/ transform matrix (Model-View-Projection)\n    pSh-&gt;l_aPos = glGetAttribLocation(pSh-&gt;GLid, &quot;aPos&quot;); \/\/attribute position (3D coordinates)\n    pSh-&gt;l_aTuv = glGetAttribLocation(pSh-&gt;GLid, &quot;aTuv&quot;); \/\/attribute TUV (texture coordinates)\n    pSh-&gt;l_uColor = glGetUniformLocation(pSh-&gt;GLid, &quot;uColor&quot;);\n    pSh-&gt;l_uTex0 = glGetUniformLocation(pSh-&gt;GLid, &quot;uTex0&quot;); \/\/texture id\n\n    return (shaders.size() - 1);\n}\nint Shader::cleanUp() {\n    int shadersN = shaders.size();\n    if (shadersN &lt; 1)\n        return -1;\n    glUseProgram(0);\n    for (int i = 0; i &lt; shadersN; i++) {\n        Shader* pSh = shaders.at(i);\n        glDeleteProgram(pSh-&gt;GLid);\n        delete pSh;\n    }\n    shaders.clear();\n    return 1;\n}\n\nGLchar infoLog&#91;1024];\nint logLength;\nint Shader::shaderErrorCheck(int shaderId, std::string ref) {\n    \/\/use after glCompileShader()\n    if (checkGLerrors(ref) &gt; 0)\n        return -1;\n    glGetShaderInfoLog(shaderId, 1024, &amp;logLength, infoLog);\n    if (logLength == 0)\n        return 0;\n    mylog(&quot;%s shader infoLog:\\n%s\\n&quot;, ref.c_str(), infoLog);\n    return -1;\n}\nint Shader::programErrorCheck(int programId, std::string ref) {\n    \/\/use after glLinkProgram()\n    if (checkGLerrors(ref) &gt; 0)\n        return -1;\n    glGetProgramInfoLog(programId, 1024, &amp;logLength, infoLog);\n    if (logLength == 0)\n        return 0;\n    mylog(&quot;%s program infoLog:\\n%s\\n&quot;, ref.c_str(), infoLog);\n    return -1;\n}\n\nint Shader::compileShader(const char* filePath, GLenum shaderType) {\n    int shaderId = glCreateShader(shaderType);\n    FILE* pFile;\n    myFopen_s(&amp;pFile, filePath, &quot;rt&quot;);\n    if (pFile != NULL)\n    {\n        \/\/ obtain file size:\n        fseek(pFile, 0, SEEK_END);\n        int fSize = ftell(pFile);\n        rewind(pFile);\n        \/\/ size obtained, create buffer\n        char* shaderSource = new char&#91;fSize + 1];\n        fSize = fread(shaderSource, 1, fSize, pFile);\n        shaderSource&#91;fSize] = 0;\n        fclose(pFile);\n        \/\/ source code loaded, compile\n        glShaderSource(shaderId, 1, (const GLchar**)&amp;shaderSource, NULL);\n        \/\/myglErrorCheck(&quot;glShaderSource&quot;);\n        glCompileShader(shaderId);\n        if (shaderErrorCheck(shaderId, &quot;glCompileShader&quot;) &lt; 0)\n            return -1;\n        delete&#91;] shaderSource;\n    }\n    else {\n        mylog(&quot;ERROR loading %s\\n&quot;, filePath);\n        return -1;\n    }\n    return shaderId;\n}\nint Shader::linkShaderProgram(const char* filePathVertexS, const char* filePathFragmentS) {\n    int vertexShaderId = compileShader(filePathVertexS, GL_VERTEX_SHADER);\n    int fragmentShaderId = compileShader(filePathFragmentS, GL_FRAGMENT_SHADER);\n    int programId = glCreateProgram();\n    glAttachShader(programId, vertexShaderId);\n    glAttachShader(programId, fragmentShaderId);\n    glLinkProgram(programId);\n    if (programErrorCheck(programId, &quot;glLinkProgram&quot;) &lt; 0)\n        return -1;\n    \/\/don&#039;t need shaders any longer - detach and delete them\n    glDetachShader(programId, vertexShaderId);\n    glDetachShader(programId, fragmentShaderId);\n    glDeleteShader(vertexShaderId);\n    glDeleteShader(fragmentShaderId);\n    return programId;\n}\n\n<\/pre><\/div>\n\n\n<p><\/p>\n\n\n\n<hr class=\"wp-block-separator has-css-opacity\"\/>\n\n\n\n<p>6. Corresponding <em>TheGame.cpp<\/em> at this point. Open and replace code by following:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; highlight: [28,37,48,49]; title: ; notranslate\" title=\"\">\n#include &quot;TheGame.h&quot;\n#include &quot;platform.h&quot;\n#include &quot;linmath.h&quot;\n#include &quot;utils.h&quot;\n#include &quot;Shader.h&quot;\n#include &quot;Texture.h&quot;\n\nextern std::string filesRoot;\n\nstatic const struct\n{\n    float x, y, z, tu, tv;\n} vertices&#91;4] =\n{\n    { -0.5f,  0.5f, 0.f, 0.f, 0.f }, \/\/top-left\n    { -0.5f, -0.5f, 0.f, 0.f, 1.f }, \/\/bottom-left\n    {  0.5f,  0.5f, 0.f, 1.f, 0.f }, \/\/top-right\n    {  0.5f, -0.5f, 0.f, 1.f, 1.f }  \/\/bottom-right\n};\n\nShader* pShader;\nunsigned int vao_buffer, vertex_buffer;\nfloat angle_z = 0;\n\nint TheGame::getReady() {\n    bExitGame = false;\n\n    Shader::loadShaders();\n\n    glGenVertexArrays(1, &amp;vao_buffer);\n    glBindVertexArray(vao_buffer);\n\n    glGenBuffers(1, &amp;vertex_buffer);\n    glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer);\n    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);\n\n    pShader = Shader::shaders.at(Shader::spN_flat_tex);\n\n    glEnableVertexAttribArray(pShader-&gt;l_aPos);\n    glVertexAttribPointer(pShader-&gt;l_aPos, 3, GL_FLOAT, GL_FALSE,\n        sizeof(vertices&#91;0]), (void*)0);\n\n    glEnableVertexAttribArray(pShader-&gt;l_aTuv);\n    glVertexAttribPointer(pShader-&gt;l_aTuv, 2, GL_FLOAT, GL_FALSE,\n        sizeof(vertices&#91;0]), (void*)(sizeof(float) * 3));\n\n    \/\/load texture\n    int textureN = Texture::loadTexture(filesRoot + &quot;\/dt\/sample_img.png&quot;);\n    int textureId = Texture::getGLid(textureN);\n    \/\/pass textureId to shader program\n    glActiveTexture(GL_TEXTURE0); \/\/ activate the texture unit first before binding texture\n    glBindTexture(GL_TEXTURE_2D, textureId);\n    \/\/ Tell the texture uniform sampler to use this texture in the shader by binding to texture unit 0.    \n    glUniform1i(pShader-&gt;l_uTex0, 0);\n\n    return 1;\n}\nint TheGame::drawFrame() {\n    myPollEvents();\n\n    mat4x4 m, p, mvp;\n\n    \/\/glClearColor(0.0, 0.0, 0.5, 1.0);\n    glClear(GL_COLOR_BUFFER_BIT);\n    angle_z += 0.01f;\n    mat4x4_identity(m);\n    mat4x4_rotate_Z(m, m, angle_z);\n    mat4x4_scale_aniso(m, m, 2.0, 1.0, 1.0);\n    mat4x4_ortho(p, -screenRatio, screenRatio, -1.f, 1.f, 1.f, -1.f);\n    mat4x4_mul(mvp, p, m);\n\n    glUseProgram(pShader-&gt;GLid);\n    glUniformMatrix4fv(pShader-&gt;l_uMVP, 1, GL_FALSE, (const GLfloat*)mvp);\n\n    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);\n\n    mySwapBuffers();\n    return 1;\n}\nint TheGame::cleanUp() {\n    Texture::cleanUp();\n    Shader::cleanUp();\n    return 1;\n}\nint TheGame::onScreenResize(int width, int height) {\n    if (screenSize&#91;0] == width &amp;&amp; screenSize&#91;1] == height)\n        return 0;\n    screenSize&#91;0] = width;\n    screenSize&#91;1] = height;\n    screenRatio = (float)width \/ (float)height;\n    glViewport(0, 0, width, height);\n    mylog(&quot; screen size %d x %d\\n&quot;, width, height);\n    return 1;\n}\nint TheGame::run() {\n    getReady();\n    while (!bExitGame) {\n        drawFrame();\n    }\n    cleanUp();\n    return 1;\n}\n\n<\/pre><\/div>\n\n\n<p><\/p>\n\n\n\n<p>Build and run. So far so good.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-css-opacity\"\/>\n\n\n\n<p>Now &#8211;<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Android<\/h2>\n\n\n\n<p><br>7. Re-start VS. Open <em>C:\\CPP\\<strong>a998engine<\/strong>\\p_android\\p_android.sln<\/em>.<\/p>\n\n\n\n<p>Add new classes to the Project:<\/p>\n\n\n\n<p>Under <em>xEngine <\/em>add Existing Item<\/p>\n\n\n\n<p>Go to <em>C:\\CPP\\engine<\/em><\/p>\n\n\n\n<p>Pick<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Texture.cpp<\/li>\n\n\n\n<li>Texture.h<\/li>\n<\/ul>\n\n\n\n<p><strong>Add<\/strong>.<\/p>\n\n\n\n<p>Plug in Android, build and run.<\/p>\n\n\n\n<p>Cool.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-css-opacity\"\/>\n","protected":false},"excerpt":{"rendered":"<p class=\"mb-2\">At this point we successfully married desktop and mobile, Visual Studio and OpenGL ES. Now we are starting to implement our graphics engine. Sure, it can be done in many different ways. Warning: All following materials represent my personal vision of this topic. We&#8217;ll start from basic classes. Our current TheGame code renders 1 simple [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[4],"tags":[],"class_list":["post-479","post","type-post","status-publish","format-standard","hentry","category-cross-platform-3d"],"_links":{"self":[{"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/posts\/479","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/comments?post=479"}],"version-history":[{"count":18,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/posts\/479\/revisions"}],"predecessor-version":[{"id":1954,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/posts\/479\/revisions\/1954"}],"wp:attachment":[{"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/media?parent=479"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/categories?post=479"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/tags?post=479"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}