{"id":937,"date":"2021-12-13T23:06:26","date_gmt":"2021-12-13T23:06:26","guid":{"rendered":"https:\/\/writingagame.com\/?p=937"},"modified":"2022-01-09T07:23:17","modified_gmt":"2022-01-09T07:23:17","slug":"chapter-31-normal-maps","status":"publish","type":"post","link":"https:\/\/writingagame.com\/index.php\/2021\/12\/13\/chapter-31-normal-maps\/","title":{"rendered":"Chapter 31. Normal maps"},"content":{"rendered":"\n<p>Now I want to make gilded blazon and &#8220;Marlboro&#8221; sign on the front <em>embossed<\/em>. For this we&#8217;ll use <strong>Normal Maps<\/strong>. That&#8217;s a bluish images on the right:<\/p>\n\n\n\n<p><img decoding=\"async\" src=\"https:\/\/writingagame.com\/img\/b01\/c28\/marlboro03small.png\"><\/p>\n\n\n\n<p><\/p>\n\n\n\n<p>To burn them I used <a href=\"https:\/\/cpetry.github.io\/NormalMap-Online\/\" target=\"_blank\" rel=\"noreferrer noopener\">NormalMap-Online<\/a> website.<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>Default settings are for <em>DirectX<\/em>. <\/li><li>For <em>OpenGL <\/em>&#8211; checkmark <strong>Invert R<\/strong><\/li><li>Click on the <strong>left <\/strong>picture to upload your own.<\/li><\/ul>\n\n\n\n<p><\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>You already have this image in your <strong>\/dt<\/strong> folder, so no need to download it.<\/li><\/ul>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p>Normal map <strong>shaders&#8217; <\/strong>logic is a bit different, it uses so called <strong>Tangent Space<\/strong> instead of <em>Eye Space<\/em>, so it will be 2 <strong>new <\/strong>shaders.<\/p>\n\n\n\n<p><strong>Vertex shader<\/strong>:<\/p>\n\n\n\n<p>1. Copy following code in a <strong>Text Editor<\/strong> and save it to\/as<\/p>\n\n\n\n<p><em>C:\\CPP\\engine\\dt\\shaders\\<strong>nm_v.txt<\/strong><\/em><\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: xml; title: ; notranslate\" title=\"\">\n\/\/#version 320 es\nprecision lowp float;\nuniform mat4 uMVP; \/\/ transform matrix (Model-View-Projection)\nuniform mat3 uMV3x3; \/\/ Model-View matrix (for calculating normals into eye space)\n\nin vec3 aPos; \/\/ position attribute (x,y,z)\nin vec3 aNormal; \/\/ normal attribute (x,y,z)\n\n\/\/normal map\nin vec3 aTangent;\nin vec3 aBinormal;\nin vec2 aTuv2; \/\/attribute TUV2 (texture coordinates)\nout vec2 vTuv2; \/\/varying TUV2 (pass to fragment shader)\nuniform vec3 uVectorToLight;\nuniform vec3 uHalfVector;\nout vec3 tbnVectorToLight;\nout vec3 tbnHalfVector;\n#if defined(MIRROR)\n\tout vec2 vScreenPosition01;\n\tout mat3 inversedTBN;\n#endif\n#if defined(USE_TUV0)\n\tin vec2 aTuv; \/\/attribute TUV (texture coordinates)\n\tout vec2 vTuv; \/\/varying TUV (pass to fragment shader)\n#endif\n\nvoid main(void) { \n\tgl_Position = uMVP * vec4(aPos, 1.0);\n#if defined(USE_TUV0)\n\tvTuv = aTuv;\n#endif\n\n\tvTuv2 = aTuv2;\n\n\t\/\/ Transform the normal&#039;s orientation into eye space.    \n\tvec3 N = uMV3x3 * aNormal;\n\tvec3 T = uMV3x3 * aTangent;\n\tvec3 B = uMV3x3 * aBinormal;\n\t\/\/build TBN matrix\n\tmat3 TBN = mat3(\n\t\t\tT&#91;0],B&#91;0],N&#91;0],\n\t\t\tT&#91;1],B&#91;1],N&#91;1],\n\t\t\tT&#91;2],B&#91;2],N&#91;2]\n\t\t\t);\n\ttbnVectorToLight = TBN * uVectorToLight;\n\ttbnHalfVector = TBN * uHalfVector;\n#if defined(MIRROR)\n\tvScreenPosition01&#91;0] =  (gl_Position&#91;0]\/gl_Position&#91;3])*0.1;\n\tvScreenPosition01&#91;1] = -(gl_Position&#91;1]\/gl_Position&#91;3])*0.1;\n\tinversedTBN = inverse(TBN);\n#endif\n}\n\n<\/pre><\/div>\n\n\n<p><\/p>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p><strong>Fragment shader<\/strong>:<\/p>\n\n\n\n<p>2. Copy following code in a <strong>Text Editor<\/strong> and save it to\/as<\/p>\n\n\n\n<p><em>C:\\CPP\\engine\\dt\\shaders\\nm_<strong>f<\/strong>.txt<\/em><\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: xml; title: ; notranslate\" title=\"\">\n\/\/#version 320 es\nprecision lowp float;\nout vec4 FragColor; \/\/output pixel color\nuniform float uAlphaFactor; \/\/for semi-transparency\nuniform int uAlphaBlending; \/\/for semi-transparency\n\nin vec2 vTuv2;\nuniform sampler2D uTex2nm;\nin vec3 tbnVectorToLight;\nin vec3 tbnHalfVector;\n\n#if defined(USE_TEX0)\n\tuniform sampler2D uTex0;  \/\/texture id\n\tuniform sampler2D uTex3;  \/\/translate texture id\n\tuniform int uTex0translateChannelN;\n#else\n\tuniform vec4 uColor;\n#endif\n#if defined(USE_TUV0)\n\tin vec2 vTuv; \/\/varying TUV (passed from vertex shader)\n#endif\n#if defined(OVERMASK)\n\tuniform sampler2D uTex1mask;  \/\/texture id\n\tuniform int uTex1alphaChannelN;\n\tuniform int uTex1alphaNegative;\n#endif\n#if defined(MIRROR)\n\tin vec2 vScreenPosition01;\n\tin mat3 inversedTBN;\n#endif\nuniform float uAmbient;\nuniform float uSpecularIntencity;\nuniform float uSpecularMinDot;\nuniform float uSpecularPowerOf;\n\nvoid main(void) {\n\tvec4 tbnNormal4 = texture(uTex2nm, vTuv2);\n\tfloat alpha = tbnNormal4.a;\n\n\tif(alpha &lt; 0.5){\n\t\tif(uAlphaBlending &gt; 0){\n\t\t\tif(alpha == 0.0){\n\t\t\t\tdiscard;\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\telse{ \/\/no AlphaBlending\n\t\t\tdiscard;\n\t\t\treturn;\n\t\t}\n\t}\n\t\/\/black?\n\tif(tbnNormal4.b &lt; 0.4){\n\t\tFragColor = vec4(0.0,0.0,0.0,alpha);\n\t\treturn;\n\t}\n\t\n\tvec4 outColor;\n#if defined(OVERMASK)\n\toutColor = texture(uTex1mask, vTuv);\n\tfloat alpha2 = outColor&#91;uTex1alphaChannelN];\n\tif(uTex1alphaNegative &gt; 0)\n\t\talpha2 = 1.0 - alpha2;\n\tif(alpha2 &lt; 1.0){\n\t\talpha *= alpha2;\n\t\tif(alpha &lt; 0.5){\n\t\t\tif(uAlphaBlending &gt; 0){\n\t\t\t\tif(alpha == 0.0){\n\t\t\t\t\tdiscard;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\telse{ \/\/no AlphaBlending\n\t\t\t\tdiscard;\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n#endif\n\tvec3 vNormalNormal = normalize(vec3(tbnNormal4) * 2.0 - 1.0);\n\n#if defined(USE_TEX0)\n\t#if defined(MIRROR)\n\t\tvec3 inversedNormal = normalize(inversedTBN * vNormalNormal);\n\t\tvec2 vTuvMirror;\n\t\tvTuvMirror&#91;0] =  (vScreenPosition01&#91;0]+inversedNormal&#91;0]*0.4)+0.5;\n\t\tvTuvMirror&#91;1] = -(vScreenPosition01&#91;1]+inversedNormal&#91;1]*0.4)+0.5;\n\t\toutColor = texture(uTex0, vTuvMirror);\n\t#else\n\t\toutColor = texture(uTex0, vTuv);\n\t#endif\n\tif(uTex0translateChannelN &gt;= 0){ \/\/translate channel\n\t\tvec2 tuv3;\n\t\ttuv3&#91;0] = outColor&#91;uTex0translateChannelN];\n\t\ttuv3&#91;1] = 0.0;\n\t\toutColor = texture(uTex3, tuv3);\n\t}\n\tFragColor = outColor;\n#else\n\tFragColor = uColor;\n#endif\n\tif(FragColor.a != 1.0){\n\t\talpha *= FragColor.a;\n\t\tif(alpha &lt; 0.5){\n\t\t\tif(uAlphaBlending &gt; 0){\n\t\t\t\tif(alpha == 0.0){\n\t\t\t\t\tdiscard;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\telse{ \/\/no AlphaBlending\n\t\t\t\tdiscard;\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\n\tif(uAmbient&lt;1.0){\n\t\t \/\/ Calculate the dot product of the light vector and vertex normal. If the normal and light vector are\n\t\t \/\/ pointing in the same direction then it will get max illumination.\n\t\t float directionalLightIntencity = dot(vNormalNormal, normalize(tbnVectorToLight));\n\t\t\n\t\t \/\/ count ambient component\n\t\t directionalLightIntencity += uAmbient;\n\t\t if(directionalLightIntencity &lt; uAmbient)\n\t\t\tdirectionalLightIntencity = uAmbient;\n\n\t\t \/\/ Multiply the color by the lightIntencity illumination level to get final output color.\n\t\t FragColor *= directionalLightIntencity;\n\t}\n\tif(uSpecularIntencity&gt;0.0){\n\t\t\/\/specular light\n\t\t\/\/ INTENSITY OF THE SPECULAR LIGHT\n\t\t\/\/ DOT PRODUCT OF NORMAL VECTOR AND THE HALF VECTOR TO THE POWER OF THE SPECULAR HARDNESS\n\t\tfloat dotProduct = dot(vNormalNormal, normalize(tbnHalfVector));\n\n\t\tif(dotProduct&gt;uSpecularMinDot){\n\t\t\tfloat specularIntencity = pow(dotProduct, uSpecularPowerOf) * uSpecularIntencity;\t\t\n\t\t\tif(specularIntencity &gt; uSpecularIntencity)\n\t\t\t\tspecularIntencity = uSpecularIntencity;\n\t\t\tFragColor += specularIntencity;\n\t\t}\n\t}\n\tif(uAlphaFactor != 1.0)\n\t\talpha *= uAlphaFactor;\t\n\tFragColor.a = alpha;\n}\n\n<\/pre><\/div>\n\n\n<p><\/p>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p>Revised <strong>root01.txt<\/strong>:<\/p>\n\n\n\n<p>3. Copy following code in a <strong>Text Editor<\/strong> and save it (overwrite) to\/as<\/p>\n\n\n\n<p><em>C:\\CPP\\<strong>a997modeler<\/strong>\\dt\\models\\misc\\marlboro01\\<strong>root01.txt<\/strong><\/em><\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: xml; highlight: [23,24,26]; title: ; notranslate\" title=\"\">\n&lt;texture_as=&quot;tx0&quot; src=&quot;marlboro03small.png&quot; ckey=&quot;#00ff00&quot;\/&gt;\n&lt;mt_type=&quot;phong&quot; uTex0_use=&quot;tx0&quot; \/&gt;\n&lt;vs=&quot;box_tank&quot; whl=&quot;53,83,21&quot; ext=1 sectR=1 \/&gt;\n&lt;a=&quot;front v,back v&quot; xywh=&quot;2,1,323,495&quot;\/&gt;\n&lt;a=&quot;right all&quot; xywh=&quot;327,1,128,495&quot;\/&gt;\n&lt;a=&quot;left all&quot; xywh=&quot;457,1,128,495&quot;\/&gt;\n&lt;a=&quot;top&quot; xywh=&quot;588,1,323,133&quot;\/&gt;\n&lt;a=&quot;bottom&quot; xywh=&quot;587,136,324,134&quot;\/&gt;\n\/\/golden prints\n&lt;vs=&quot;box&quot; whl=&quot;55.1,85.1,23.1&quot; \/&gt;\n&lt;texture_as=&quot;whitenoise&quot; src=&quot;\/dt\/common\/img\/whitenoise\/wn64_blur3.bmp&quot;\/&gt;\n&lt;texture_as=&quot;gold&quot; src=&quot;\/dt\/common\/img\/materials\/gold02roman.bmp&quot; \/&gt;\n&lt;mt_type=&quot;mirror&quot; uAlphaBlending uTex1mask_use=&quot;tx0&quot; uTex1alphaChannelN=1 uTex0_use=&quot;whitenoise&quot; uTex0translateChannelN=0 uTex3_use=&quot;gold&quot; \/&gt;\n\/\/side golden prints\n&lt;a=&quot;right&quot; xywh=&quot;342,12,101,10&quot; whl=&quot;x,1.8,18.1&quot; pxyz=&quot;x,39.8, -0.3&quot; \/&gt; \/\/Please do not litter\n&lt;a=&quot;right&quot; xywh=&quot;339,144,105,89&quot; whl=&quot;x,15.35,18.9&quot; pxyz=&quot;x,10.3,-0.12&quot; \/&gt; \/\/For special offers...\n&lt;a=&quot;left&quot; xywh=&quot;475,15,95,48&quot; whl=&quot;x,8.4,17&quot; pxyz=&quot;x,36, 0.3&quot; \/&gt; \/\/Underage sale...\n\/\/front prints\n&lt;group&gt;\n\t\/\/bottom golden print &quot;20 class a...&quot;\n\t&lt;a=&quot;front&quot; xywh=&quot;20,498,289,13&quot; whl=&quot;47.5,2,x&quot; pxyz=&quot;1,-36,x&quot; \/&gt;\n\t\/\/blazon\/emblem\n\t&lt;mt_type=&quot;mirror&quot; uAlphaBlending uTex2nm_use=&quot;tx0&quot; uTex0_use=&quot;whitenoise&quot; uTex0translateChannelN=0 uTex3_use=&quot;gold&quot; \/&gt;\n\t&lt;a=&quot;front&quot; xywh2nm=&quot;589,415,128,94&quot; whl=&quot;20.7,16,x&quot; pxyz=&quot;0.3,6.1,x&quot; \/&gt; \/\/emblem\n\t\/\/&quot;Marlboro\n\t&lt;mt_type=&quot;phong&quot; uAlphaBlending uTex2nm_use=&quot;tx0&quot; uColor=&quot;#1E211E&quot; \/&gt;\n\t&lt;a=&quot;front&quot; xywh2nm=&quot;590,275,301,136&quot; whl=&quot;49.2,23.3,x&quot; pxyz=&quot;0.21,-18,x&quot; \/&gt; \/\/marlboro\n&lt;\/group&gt; \n&lt;clone ay=180 \/&gt;\n\n<\/pre><\/div>\n\n\n<p><\/p>\n\n\n\n<p>Please note:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>New material for blazon (line 23): Instead of <em>uTex1mask <\/em>we are setting a <strong>Normal Map<\/strong> <em>uTex2nm_use=&#8221;tx0&#8243;<\/em> <\/li><li>In &#8220;a&#8221; tag for the blazon (line 24) we use not <em>xywh<\/em>, but <em>xywh2nm<\/em> (xywh for normal map)<\/li><li>Material for &#8220;Marlboro&#8221; sign (line 26): Phong with uColor and with Normal Map<\/li><\/ul>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p>Now &#8211; SW side.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Windows<\/h2>\n\n\n\n<p>4. Start VS, open&nbsp;<em>C:\\CPP\\<em>a997modeler<\/em>\\p_windows\\p_windows.sln<\/em>.<\/p>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p>Now we&#8217;ll have more shaders.<\/p>\n\n\n\n<p>5. Open <em>Shader.cpp<\/em> and replace code by:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; highlight: [20,21,22,23,24,25]; title: ; notranslate\" title=\"\">\n#include &quot;Shader.h&quot;\n#include &quot;platform.h&quot;\n#include &quot;utils.h&quot;\n#include &quot;FileLoader.h&quot;\n\nextern std::string filesRoot;\n\n\/\/static array (vector) of all loaded shaders\nstd::vector&lt;Shader*&gt; Shader::shaders;\n\nint Shader::loadShaders() {\n    FileLoader* pFLvertex = new FileLoader(&quot;\/dt\/shaders\/phong_v.txt&quot;);\n    FileLoader* pFLfragment = new FileLoader(&quot;\/dt\/shaders\/phong_f.txt&quot;);\n    loadShadersGroup(&quot;flat&quot;, &quot;FLAT; COLOR | TEXTURE; NONE | OVERMASK&quot;, pFLvertex-&gt;pData, pFLfragment-&gt;pData);\n    loadShadersGroup(&quot;phong&quot;, &quot;PHONG; COLOR | TEXTURE; NONE | OVERMASK&quot;, pFLvertex-&gt;pData, pFLfragment-&gt;pData);\n    loadShadersGroup(&quot;mirror&quot;, &quot;PHONG;MIRROR; NONE | OVERMASK&quot;, pFLvertex-&gt;pData, pFLfragment-&gt;pData);\n    delete pFLvertex;\n    delete pFLfragment;\n    \/\/Normal Maps\n    pFLvertex = new FileLoader(&quot;\/dt\/shaders\/nm_v.txt&quot;);\n    pFLfragment = new FileLoader(&quot;\/dt\/shaders\/nm_f.txt&quot;);\n    loadShadersGroup(&quot;phong&quot;, &quot;COLOR | TEXTURE; NONE | OVERMASK&quot;, pFLvertex-&gt;pData, pFLfragment-&gt;pData);\n    loadShadersGroup(&quot;mirror&quot;, &quot;MIRROR; NONE | OVERMASK&quot;, pFLvertex-&gt;pData, pFLfragment-&gt;pData);\n    delete pFLvertex;\n    delete pFLfragment;\n    return 1;\n}\nint Shader::buildShaderObjectFromFiles(std::string filePathVertexS, std::string filePathFragmentS) {\n    \/\/create shader object\n    Shader* pSh = new Shader();\n    shaders.push_back(pSh);\n    pSh-&gt;GLid = linkShaderProgramFromFiles((filesRoot + filePathVertexS).c_str(), (filesRoot + filePathFragmentS).c_str());\n    \/\/common variables. If not presented, = -1;\n    fillLocations(pSh);\n\n    return (shaders.size() - 1);\n}\nint Shader::fillLocations(Shader* pSh) {\n    \/\/common variables. If not presented, = -1;\n    \/\/attributes\n    pSh-&gt;l_aPos = glGetAttribLocation(pSh-&gt;GLid, &quot;aPos&quot;); \/\/attribute position (3D coordinates)\n    pSh-&gt;l_aNormal = glGetAttribLocation(pSh-&gt;GLid, &quot;aNormal&quot;); \/\/attribute normal (3D vector)\n    pSh-&gt;l_aTangent = glGetAttribLocation(pSh-&gt;GLid, &quot;aTangent&quot;); \/\/for normal map\n    pSh-&gt;l_aBinormal = glGetAttribLocation(pSh-&gt;GLid, &quot;aBinormal&quot;); \/\/for normal map\n    pSh-&gt;l_aTuv = glGetAttribLocation(pSh-&gt;GLid, &quot;aTuv&quot;); \/\/attribute TUV (texture coordinates)\n    pSh-&gt;l_aTuv2 = glGetAttribLocation(pSh-&gt;GLid, &quot;aTuv2&quot;); \/\/attribute TUV (texture coordinates)\n    \/\/uniforms\n    pSh-&gt;l_uMVP = glGetUniformLocation(pSh-&gt;GLid, &quot;uMVP&quot;); \/\/ transform matrix (Model-View-Projection)\n    pSh-&gt;l_uMV3x3 = glGetUniformLocation(pSh-&gt;GLid, &quot;uMV3x3&quot;); \/\/ Model-View matrix for normals\n    pSh-&gt;l_uVectorToLight = glGetUniformLocation(pSh-&gt;GLid, &quot;uVectorToLight&quot;); \/\/ \n    pSh-&gt;l_uHalfVector = glGetUniformLocation(pSh-&gt;GLid, &quot;uHalfVector&quot;); \/\/ required for specular light\n    \/\/material&#039;s properties\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    pSh-&gt;l_uTex1mask = glGetUniformLocation(pSh-&gt;GLid, &quot;uTex1mask&quot;); \/\/texture id\n    pSh-&gt;l_uTex2nm = glGetUniformLocation(pSh-&gt;GLid, &quot;uTex2nm&quot;); \/\/texture id\n    pSh-&gt;l_uTex3 = glGetUniformLocation(pSh-&gt;GLid, &quot;uTex3&quot;); \/\/texture id\n    pSh-&gt;l_uTex1alphaChannelN = glGetUniformLocation(pSh-&gt;GLid, &quot;uTex1alphaChannelN&quot;);\n    pSh-&gt;l_uTex1alphaNegative = glGetUniformLocation(pSh-&gt;GLid, &quot;uTex1alphaNegative&quot;);\n    pSh-&gt;l_uTex0translateChannelN = glGetUniformLocation(pSh-&gt;GLid, &quot;uTex0translateChannelN&quot;);\n    pSh-&gt;l_uAlphaFactor = glGetUniformLocation(pSh-&gt;GLid, &quot;uAlphaFactor&quot;); \/\/ for semi-transparency\n    pSh-&gt;l_uAlphaBlending = glGetUniformLocation(pSh-&gt;GLid, &quot;uAlphaBlending&quot;); \/\/ for semi-transparency\n    pSh-&gt;l_uAmbient = glGetUniformLocation(pSh-&gt;GLid, &quot;uAmbient&quot;); \/\/ ambient light\n    pSh-&gt;l_uSpecularIntencity = glGetUniformLocation(pSh-&gt;GLid, &quot;uSpecularIntencity&quot;); \/\/ \n    pSh-&gt;l_uSpecularMinDot = glGetUniformLocation(pSh-&gt;GLid, &quot;uSpecularMinDot&quot;); \/\/ \n    pSh-&gt;l_uSpecularPowerOf = glGetUniformLocation(pSh-&gt;GLid, &quot;uSpecularPowerOf&quot;); \/\/ \n    return 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::compileShaderFromFile(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::linkShaderProgramFromFiles(const char* filePathVertexS, const char* filePathFragmentS) {\n    int vertexShaderId = compileShaderFromFile(filePathVertexS, GL_VERTEX_SHADER);\n    int fragmentShaderId = compileShaderFromFile(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\nint Shader::buildShaderObjectWithDefines(std::string shaderType, std::string definesString, char* sourceVertex, char* sourceFragment) {\n    \/\/create shader object\n    Shader* pSh = new Shader();\n    shaders.push_back(pSh);\n    myStrcpy_s(pSh-&gt;shaderType, 20, shaderType.c_str());\n\n    pSh-&gt;GLid = linkShaderProgramWithDefines(definesString, sourceVertex, sourceFragment);\n    \/\/common variables. If not presented, = -1;\n    fillLocations(pSh);\n\n    return (shaders.size() - 1);\n}\nint Shader::linkShaderProgramWithDefines(std::string definesString00, char* sourceVertex, char* sourceFragment) {\n    \/\/build extended definesString\n    bool bUSE_NORMALS = false;\n    bool bUSE_TEX0 = false;\n    bool bUSE_TUV0 = false;\n    if (definesString00.find(&quot; PHONG\\n&quot;) != std::string::npos)\n        bUSE_NORMALS = true;\n    if (definesString00.find(&quot; TEXTURE\\n&quot;) != std::string::npos) {\n        bUSE_TEX0 = true;\n        bUSE_TUV0 = true;\n    }\n    if (definesString00.find(&quot; MIRROR\\n&quot;) != std::string::npos) {\n        bUSE_NORMALS = true;\n        bUSE_TEX0 = true;\n    }\n    if (definesString00.find(&quot; OVERMASK\\n&quot;) != std::string::npos) {\n        bUSE_TUV0 = true;\n    }\n    std::string definesString;\n    definesString.assign(&quot;#version 320 es\\n&quot;);\n    definesString.append(definesString00);\n    if (bUSE_NORMALS)\n        definesString.append(&quot;#define USE_NORMALS\\n&quot;);\n    if (bUSE_TEX0)\n        definesString.append(&quot;#define USE_TEX0\\n&quot;);\n    if (bUSE_TUV0)\n        definesString.append(&quot;#define USE_TUV0\\n&quot;);\n\n    int vertexShaderId = compileShaderWithDefines(definesString, sourceVertex, GL_VERTEX_SHADER);\n    int fragmentShaderId = compileShaderWithDefines(definesString, sourceFragment, GL_FRAGMENT_SHADER);\n\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    \/\/mylog(&quot;linking program\\n%s\\n&quot;, definesString.c_str());\n    return programId;\n}\nint Shader::compileShaderWithDefines(std::string definesString, char* shaderSource, GLenum shaderType) {\n    int shaderId = glCreateShader(shaderType);\n    if (definesString.empty())\n        glShaderSource(shaderId, 1, (const GLchar**)&amp;shaderSource, NULL);\n    else { \/\/2 strings\n        const char* sourceStrings&#91;2];\n        sourceStrings&#91;0] = definesString.c_str();\n        sourceStrings&#91;1] = shaderSource;\n        \/\/ source code loaded, compile\n        glShaderSource(shaderId, 2, (const GLchar**)sourceStrings, NULL);\n    }\n    \/\/myglErrorCheck(&quot;glShaderSource&quot;);\n    glCompileShader(shaderId);\n    if (shaderErrorCheck(shaderId, &quot;glCompileShader&quot;) &lt; 0) {\n        mylog(&quot;ERROR in compileShader,\\n%s\\n%s\\n&quot;, definesString.c_str(), shaderSource);\n        return -1;\n    }\n    return shaderId;\n}\n\nint Shader::loadShadersGroup(std::string shaderType, std::string optionsString, char* sourceVertex, char* sourceFragment) {\n    struct Terms {\n        std::vector&lt;std::string&gt; terms;\n        int totalN = 0;\n        int currentN = 0;\n    };\n    std::vector&lt;Terms*&gt; terms;\n    std::vector&lt;std::string&gt; termGroups = splitString(optionsString, &quot;;&quot;);\n    int groupsN = termGroups.size();\n    for (int groupN = 0; groupN &lt; groupsN; groupN++) {\n        Terms* pTerms = new Terms();\n        terms.push_back(pTerms);\n        pTerms-&gt;terms = splitString(termGroups.at(groupN), &quot;|&quot;);\n        pTerms-&gt;totalN = pTerms-&gt;terms.size();\n    }\n    while (1) {\n        std::string definesString = &quot;&quot;;\n        for (int groupN = 0; groupN &lt; groupsN; groupN++) {\n            Terms* pTerms = terms.at(groupN);\n            std::string term = pTerms-&gt;terms.at(pTerms-&gt;currentN);\n            if (term.compare(&quot;NONE&quot;) != 0) {\n                definesString.append(&quot;#define &quot;);\n                definesString.append(term);\n                definesString.append(&quot;\\n&quot;);\n            }\n        }\n        int shaderObjN = buildShaderObjectWithDefines(shaderType, definesString, sourceVertex, sourceFragment);\n        \/\/go to next terms combo\n        bool noMoreOptions = false;\n        for (int groupN = groupsN - 1; groupN &gt;= 0; groupN--) {\n            Terms* pTerms = terms.at(groupN);\n            if (pTerms-&gt;currentN &lt; pTerms-&gt;totalN - 1) {\n                pTerms-&gt;currentN++;\n                break;\n            }\n            else { \/\/ the level exhausted\n                pTerms-&gt;currentN = 0;\n                \/\/proceed to upper level\n                if (groupN == 0) {\n                    noMoreOptions = true;\n                    break;\n                }\n            }\n        }\n        if (noMoreOptions)\n            break;\n    }\n    return 1;\n}\n\n<\/pre><\/div>\n\n\n<p><\/p>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p>In VBOs for shaders with normal maps involved, we&#8217;ll need to calculate <strong>Tangent Space<\/strong> for each vertex.<\/p>\n\n\n\n<p>6.  Open <em>ModelBuilder1base.h<\/em> and replace code by: <\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; highlight: [48]; title: ; notranslate\" title=\"\">\n#pragma once\n#include &lt;string&gt;\n#include &lt;vector&gt;\n#include &quot;Vertex01.h&quot;\n#include &quot;Triangle01.h&quot;\n#include &quot;VirtualShape.h&quot;\n#include &quot;Group01.h&quot;\n#include &quot;Material.h&quot;\n#include &quot;GameSubj.h&quot;\n#include &lt;map&gt;\n\nclass ModelBuilder1base\n{\npublic:\n\tstd::vector&lt;Vertex01*&gt; vertices;\n\tstd::vector&lt;Triangle01*&gt; triangles;\n\tstd::vector&lt;int&gt; subjNumbersList;\n\tint usingSubjN = -1;\n\n\tstd::vector&lt;Group01*&gt; groupsStack;\n\tGroup01* pCurrentGroup = NULL;\n\tGroup01* pLastClosedGroup = NULL;\n\t\n\tstd::vector&lt;VirtualShape*&gt; vShapesStack;\n\tVirtualShape* pCurrentVShape = NULL;\n\n\tstd::vector&lt;Material*&gt; materialsList;\n\tint usingMaterialN = -1;\n\tstd::vector&lt;int&gt; materialsStack;\n\n\tstd::map&lt;std::string, int&gt; texturesHashMap;\npublic:\n\tvirtual ~ModelBuilder1base();\n\tint useSubjN(int subjN) { return useSubjN(this, subjN); };\n\tstatic int useSubjN(ModelBuilder1base* pMB, int subjN);\n\tint useMaterial(Material* pMT) { return useMaterial(this, pMT); };\n\tstatic int useMaterial(ModelBuilder1base* pMB, Material* pMT);\n\tstatic void lockGroup(ModelBuilder1base* pMB);\n\tstatic void releaseGroup(ModelBuilder1base* pMB);\n\tstatic int addVertex(ModelBuilder1base* pMB, float kx, float ky, float kz, float nx, float ny, float nz);\n\tstatic int add2triangles(ModelBuilder1base* pMB, int nNW, int nNE, int nSW, int nSE, int n);\n\tstatic int addTriangle(ModelBuilder1base* pMB, int n0, int n1, int n2);\n\tint buildDrawJobs(std::vector&lt;GameSubj*&gt; gameSubjs) { return buildDrawJobs(this, gameSubjs); };\n\tstatic int buildDrawJobs(ModelBuilder1base* pMB, std::vector&lt;GameSubj*&gt; gameSubjs);\n\tstatic int rearrangeArraysForDrawJob(ModelBuilder1base* pMB, std::vector&lt;Vertex01*&gt; allVertices, std::vector&lt;Vertex01*&gt; useVertices, std::vector&lt;Triangle01*&gt; useTriangles);\n\tstatic int buildSingleDrawJob(Material* pMT, std::vector&lt;Vertex01*&gt; useVertices, std::vector&lt;Triangle01*&gt; useTriangles);\n\tstatic int moveGroupDg(ModelBuilder1base* pMB, float aX, float aY, float aZ, float kX, float kY, float kZ);\n\tstatic int calculateTangentSpace(std::vector&lt;Vertex01*&gt; useVertices, std::vector&lt;Triangle01*&gt; useTriangles);\n};\n\n<\/pre><\/div>\n\n\n<p><\/p>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p>7.  Open <em>ModelBuilder1base.cpp<\/em> and replace code by:  <\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; highlight: [210,211,334,409,410,414,415]; title: ; notranslate\" title=\"\">\n#include &quot;ModelBuilder1base.h&quot;\n#include &quot;platform.h&quot;\n#include &quot;utils.h&quot;\n#include &quot;DrawJob.h&quot;\n#include &quot;Shader.h&quot;\n\nextern float degrees2radians;\n\nModelBuilder1base::~ModelBuilder1base() {\n\t\/\/clear all vectors\n\tint itemsN = vertices.size();\n\tfor (int i = 0; i &lt; itemsN; i++)\n\t\tdelete vertices.at(i);\n\tvertices.clear();\n\n\titemsN = triangles.size();\n\tfor (int i = 0; i &lt; itemsN; i++)\n\t\tdelete triangles.at(i);\n\ttriangles.clear();\n\n\titemsN = vShapesStack.size();\n\tfor (int i = 0; i &lt; itemsN; i++)\n\t\tdelete vShapesStack.at(i);\n\tvShapesStack.clear();\n\n\titemsN = groupsStack.size();\n\tfor (int i = 0; i &lt; itemsN; i++)\n\t\tdelete groupsStack.at(i);\n\tgroupsStack.clear();\n\n\titemsN = materialsList.size();\n\tfor (int i = 0; i &lt; itemsN; i++)\n\t\tdelete materialsList.at(i);\n\tmaterialsList.clear();\n\n\tsubjNumbersList.clear();\n}\nint ModelBuilder1base::useSubjN(ModelBuilder1base* pMB, int subjN) {\n\tpMB-&gt;usingSubjN = subjN;\n\tint itemsN = pMB-&gt;subjNumbersList.size();\n\tbool newN = true;\n\tif (itemsN &gt; 0)\n\t\tfor (int i = 0; i &lt; itemsN; i++)\n\t\t\tif (pMB-&gt;subjNumbersList.at(i) == subjN) {\n\t\t\t\tnewN = false;\n\t\t\t\tbreak;\n\t\t\t}\n\tif (newN)\n\t\tpMB-&gt;subjNumbersList.push_back(subjN);\n\treturn subjN;\n}\nint ModelBuilder1base::useMaterial(ModelBuilder1base* pMB, Material* pMT) {\n\tint itemsN = pMB-&gt;materialsList.size();\n\tif (itemsN &gt; 0)\n\t\tfor (int i = 0; i &lt; itemsN; i++)\n\t\t\tif (memcmp(pMB-&gt;materialsList.at(i), pMT, sizeof(Material)) == 0) {\n\t\t\t\tpMB-&gt;usingMaterialN = i;\n\t\t\t\treturn i;\n\t\t\t}\n\t\/\/if here - add new material to the list\n\tpMB-&gt;usingMaterialN = itemsN;\n\t\/\/create a copy of new Material and add to the list\n\tMaterial* pMTnew = new Material(*pMT);\n\tpMB-&gt;materialsList.push_back(pMTnew);\n\treturn itemsN;\n}\nint ModelBuilder1base::add2triangles(ModelBuilder1base* pMB, int nNW, int nNE, int nSW, int nSE, int n) {\n\t\/\/indexes: NorthWest, NorthEast, SouthWest,SouthEast\n\tif (n % 2 == 0) { \/\/even number\n\t\taddTriangle(pMB, nNW, nSW, nNE);\n\t\taddTriangle(pMB, nNE, nSW, nSE);\n\t}\n\telse { \/\/odd number\n\t\taddTriangle(pMB, nNW, nSE, nNE);\n\t\taddTriangle(pMB, nNW, nSW, nSE);\n\t}\n\treturn pMB-&gt;triangles.size() - 1;\n}\nint ModelBuilder1base::addTriangle(ModelBuilder1base* pMB, int i0, int i1, int i2) {\n\tTriangle01* pTR = new Triangle01();\n\tpMB-&gt;triangles.push_back(pTR);\n\tpTR-&gt;idx&#91;0] = i0;\n\tpTR-&gt;idx&#91;1] = i1;\n\tpTR-&gt;idx&#91;2] = i2;\n\tpTR-&gt;subjN = pMB-&gt;usingSubjN;\n\tpTR-&gt;materialN = pMB-&gt;usingMaterialN;\n\treturn pMB-&gt;triangles.size() - 1;\n}\nint ModelBuilder1base::addVertex(ModelBuilder1base* pMB, float kx, float ky, float kz, float nx, float ny, float nz) {\n\tVertex01* pVX = new Vertex01();\n\tpMB-&gt;vertices.push_back(pVX);\n\tpVX-&gt;aPos&#91;0] = kx;\n\tpVX-&gt;aPos&#91;1] = ky;\n\tpVX-&gt;aPos&#91;2] = kz;\n\t\/\/normal\n\tpVX-&gt;aNormal&#91;0] = nx;\n\tpVX-&gt;aNormal&#91;1] = ny;\n\tpVX-&gt;aNormal&#91;2] = nz;\n\tpVX-&gt;subjN = pMB-&gt;usingSubjN;\n\tpVX-&gt;materialN = pMB-&gt;usingMaterialN;\n\treturn pMB-&gt;vertices.size() - 1;\n}\nint ModelBuilder1base::buildDrawJobs(ModelBuilder1base* pMB, std::vector&lt;GameSubj*&gt; gameSubjs) {\n\tint totalSubjsN = pMB-&gt;subjNumbersList.size();\n\tif (totalSubjsN &lt; 1) {\n\t\tpMB-&gt;subjNumbersList.push_back(-1);\n\t\ttotalSubjsN = 1;\n\t}\n\tint totalMaterialsN = pMB-&gt;materialsList.size();\n\tif (totalSubjsN &lt; 2 &amp;&amp; totalMaterialsN &lt; 2) {\n\t\t\/\/simple single DrawJob\n\t\tMaterial* pMT = pMB-&gt;materialsList.at(0);\n\t\tGameSubj* pGS = NULL;\n\t\tint gsN = pMB-&gt;subjNumbersList.at(0);\n\t\tif (gsN &gt;= 0)\n\t\t\tpGS = gameSubjs.at(gsN);\n\t\tif (pGS != NULL)\n\t\t\tpGS-&gt;djStartN = DrawJob::drawJobs.size();\n\t\tbuildSingleDrawJob(pMT, pMB-&gt;vertices, pMB-&gt;triangles);\n\t\tif (pGS != NULL)\n\t\t\tpGS-&gt;djTotalN = DrawJob::drawJobs.size() - pGS-&gt;djStartN;\n\t\treturn 1;\n\t}\n\tint totalVertsN = pMB-&gt;vertices.size();\n\tint totalTrianglesN = pMB-&gt;triangles.size();\n\t\/\/clear flags\n\tfor (int vN = 0; vN &lt; totalVertsN; vN++) {\n\t\tVertex01* pVX = pMB-&gt;vertices.at(vN);\n\t\tpVX-&gt;flag = 0;\n\t}\n\tfor (int tN = 0; tN &lt; totalTrianglesN; tN++) {\n\t\tTriangle01* pTR = pMB-&gt;triangles.at(tN);\n\t\tpTR-&gt;flag = 0;\n\t}\n\tint addedDJs = 0;\n\tfor (int sN = 0; sN &lt; totalSubjsN; sN++) {\n\t\tGameSubj* pGS = NULL;\n\t\tint gsN = pMB-&gt;subjNumbersList.at(sN);\n\t\tif (gsN &gt;= 0)\n\t\t\tpGS = gameSubjs.at(gsN);\n\t\tif (pGS != NULL)\n\t\t\tpGS-&gt;djStartN = DrawJob::drawJobs.size();\n\t\tfor (int mtN = 0; mtN &lt; totalMaterialsN; mtN++) {\n\t\t\tMaterial* pMT = pMB-&gt;materialsList.at(mtN);\n\t\t\tstd::vector&lt;Vertex01*&gt; useVertices;\n\t\t\tstd::vector&lt;Triangle01*&gt; useTriangles;\n\t\t\tfor (int vN = 0; vN &lt; totalVertsN; vN++) {\n\t\t\t\tVertex01* pVX = pMB-&gt;vertices.at(vN);\n\t\t\t\tif (pVX-&gt;flag != 0)\n\t\t\t\t\tcontinue;\n\t\t\t\tif (pVX-&gt;subjN != gsN)\n\t\t\t\t\tcontinue;\n\t\t\t\tif (pVX-&gt;materialN != mtN)\n\t\t\t\t\tcontinue;\n\t\t\t\t\/\/if here - make a copy\n\t\t\t\tVertex01* pVX2 = new Vertex01(*pVX);\n\t\t\t\tuseVertices.push_back(pVX2);\n\t\t\t\tpVX2-&gt;altN = vN;\n\t\t\t\tpVX-&gt;flag = 1;\n\t\t\t\tif (pVX-&gt;endOfSequence &gt; 0) {\n\t\t\t\t\trearrangeArraysForDrawJob(pMB, pMB-&gt;vertices, useVertices, useTriangles);\n\t\t\t\t\tbuildSingleDrawJob(pMT, useVertices, useTriangles);\n\t\t\t\t\taddedDJs++;\n\t\t\t\t\t\/\/clear and proceed to next sequence\n\t\t\t\t\tint useVerticesN = useVertices.size();\n\t\t\t\t\tfor (int i = 0; i &lt; useVerticesN; i++)\n\t\t\t\t\t\tdelete useVertices.at(i);\n\t\t\t\t\tuseVertices.clear();\n\t\t\t\t}\n\t\t\t}\n\t\t\tint useVerticesN = useVertices.size();\n\t\t\tif (useVerticesN &lt; 1)\n\t\t\t\tcontinue; \/\/to next material\n\t\t\t\/\/pick triangles\n\t\t\tfor (int tN = 0; tN &lt; totalTrianglesN; tN++) {\n\t\t\t\tTriangle01* pTR = pMB-&gt;triangles.at(tN);\n\t\t\t\tif (pTR-&gt;flag != 0)\n\t\t\t\t\tcontinue;\n\t\t\t\tif (pTR-&gt;subjN != gsN)\n\t\t\t\t\tcontinue;\n\t\t\t\tif (pTR-&gt;materialN != mtN)\n\t\t\t\t\tcontinue;\n\t\t\t\t\/\/if here - make a copy\n\t\t\t\tTriangle01* pTR2 = new Triangle01(*pTR);\n\t\t\t\tuseTriangles.push_back(pTR2);\n\t\t\t\tpTR-&gt;flag = 1;\n\t\t\t}\n\t\t\trearrangeArraysForDrawJob(pMB, pMB-&gt;vertices, useVertices, useTriangles);\n\t\t\tbuildSingleDrawJob(pMT, useVertices, useTriangles);\n\t\t\taddedDJs++;\n\t\t\t\/\/clear all for next material\n\t\t\tfor (int i = 0; i &lt; useVerticesN; i++)\n\t\t\t\tdelete useVertices.at(i);\n\t\t\tuseVertices.clear();\n\t\t\tint useTrianglesN = useTriangles.size();\n\t\t\tfor (int i = 0; i &lt; useTrianglesN; i++)\n\t\t\t\tdelete useTriangles.at(i);\n\t\t\tuseTriangles.clear();\n\t\t}\n\t\tif (pGS != NULL)\n\t\t\tpGS-&gt;djTotalN = DrawJob::drawJobs.size() - pGS-&gt;djStartN;\n\t}\n\treturn addedDJs;\n}\n\nint ModelBuilder1base::buildSingleDrawJob(Material* pMT, std::vector&lt;Vertex01*&gt; useVertices, std::vector&lt;Triangle01*&gt; useTriangles) {\n\tint totalVertsN = useVertices.size();\n\tif (totalVertsN &lt; 1)\n\t\treturn 0;\n\tif (pMT-&gt;uTex2nm &gt;= 0)\n\t\tcalculateTangentSpace(useVertices, useTriangles);\n\tpMT-&gt;pickShaderNumber();\n\tDrawJob* pDJ = new DrawJob();\n\t\/\/copy material to DJ\n\tmemcpy(&amp;pDJ-&gt;mt, pMT, sizeof(Material));\n\t\/\/calculate VBO element size (stride) and variables offsets in VBO\n\tint VBOid = DrawJob::newBufferId();\n\tint stride = 0;\n\tpDJ-&gt;setDesirableOffsets(&amp;stride, pDJ-&gt;mt.shaderN, VBOid);\n\t\/\/create an array for VBO\n\tint bufferSize = totalVertsN * stride;\n\tfloat* vertsBuffer = new float&#91;bufferSize];\n\t\/\/fill vertsBuffer\n\tShader* pSh = Shader::shaders.at(pDJ-&gt;mt.shaderN);\n\tint floatSize = sizeof(float);\n\tfor (int vN = 0; vN &lt; totalVertsN; vN++) {\n\t\tVertex01* pVX = useVertices.at(vN);\n\t\tint idx = vN * stride \/ floatSize;\n\t\t\/\/pick data from vertex and move to the buffer\n\t\tmemcpy(&amp;vertsBuffer&#91;idx + pDJ-&gt;aPos.offset \/ floatSize], pVX-&gt;aPos, 3 * floatSize);\n\t\tif (pSh-&gt;l_aNormal &gt;= 0) \/\/normal\n\t\t\tmemcpy(&amp;vertsBuffer&#91;idx + pDJ-&gt;aNormal.offset \/ floatSize], pVX-&gt;aNormal, 3 * floatSize);\n\t\tif (pSh-&gt;l_aTuv &gt;= 0) \/\/attribute TUV (texture coordinates)\n\t\t\tmemcpy(&amp;vertsBuffer&#91;idx + pDJ-&gt;aTuv.offset \/ floatSize], pVX-&gt;aTuv, 2 * floatSize);\n\t\tif (pSh-&gt;l_aTuv2 &gt;= 0) \/\/attribute TUV2 (normal maps)\n\t\t\tmemcpy(&amp;vertsBuffer&#91;idx + pDJ-&gt;aTuv2.offset \/ floatSize], pVX-&gt;aTuv2, 2 * floatSize);\n\t\tif (pSh-&gt;l_aTangent &gt;= 0)\n\t\t\tmemcpy(&amp;vertsBuffer&#91;idx + pDJ-&gt;aTangent.offset \/ floatSize], pVX-&gt;aTangent, 3 * floatSize);\n\t\tif (pSh-&gt;l_aBinormal &gt;= 0)\n\t\t\tmemcpy(&amp;vertsBuffer&#91;idx + pDJ-&gt;aBinormal.offset \/ floatSize], pVX-&gt;aBinormal, 3 * floatSize);\n\t}\n\t\/\/buffer is ready, create VBO\n\tglBindBuffer(GL_ARRAY_BUFFER, VBOid);\n\tglBufferData(GL_ARRAY_BUFFER, bufferSize * floatSize, vertsBuffer, GL_STATIC_DRAW);\n\tdelete&#91;] vertsBuffer;\n\tpDJ-&gt;pointsN = totalVertsN;\n\n\tint totalTrianglesN = useTriangles.size();\n\tif (totalTrianglesN &gt; 0) {\n\t\t\/\/create EBO\n\t\tint totalIndexesN = totalTrianglesN * 3;\n\t\t\/\/create buffer\n\t\tGLushort* indexBuffer = new GLushort&#91;totalIndexesN];\n\t\tfor (int tN = 0; tN &lt; totalTrianglesN; tN++) {\n\t\t\tTriangle01* pTR = useTriangles&#91;tN];\n\t\t\tint idx = tN * 3;\n\t\t\tindexBuffer&#91;idx] = (GLushort)pTR-&gt;idx&#91;0];\n\t\t\tindexBuffer&#91;idx + 1] = (GLushort)pTR-&gt;idx&#91;1];\n\t\t\tindexBuffer&#91;idx + 2] = (GLushort)pTR-&gt;idx&#91;2];\n\t\t}\n\t\t\/\/buffer is ready, create IBO\n\t\tpDJ-&gt;glEBOid = DrawJob::newBufferId();\n\t\tglBindBuffer(GL_ELEMENT_ARRAY_BUFFER, pDJ-&gt;glEBOid);\n\t\tglBufferData(GL_ELEMENT_ARRAY_BUFFER, totalIndexesN * sizeof(GLushort), indexBuffer, GL_STATIC_DRAW);\n\t\tdelete&#91;] indexBuffer;\n\t\tpDJ-&gt;pointsN = totalIndexesN;\n\t}\n\t\/\/create and fill vertex attributes array (VAO)\n\tpDJ-&gt;buildVAO();\n\treturn 1;\n}\n\nint ModelBuilder1base::rearrangeArraysForDrawJob(ModelBuilder1base* pMB, std::vector&lt;Vertex01*&gt; allVertices, std::vector&lt;Vertex01*&gt; useVertices, std::vector&lt;Triangle01*&gt; useTriangles) {\n\tint totalTrianglesN = useTriangles.size();\n\tif (totalTrianglesN &lt; 1)\n\t\treturn 0;\n\tint totalVerticesN = useVertices.size();\n\t\/\/save new vertices order in original vertices array\n\t\/\/since triangles indices refer to original vertices order\n\tfor (int i = 0; i &lt; totalVerticesN; i++) {\n\t\tVertex01* pVX1 = useVertices.at(i);\n\t\tVertex01* pVX0 = allVertices.at(pVX1-&gt;altN);\n\t\tpVX0-&gt;altN = i;\n\t}\n\t\/\/replace triangle original indices by new numbers saved in original vertices altN\n\tfor (int tN = 0; tN &lt; totalTrianglesN; tN++) {\n\t\tTriangle01* pTR = useTriangles.at(tN);\n\t\tfor (int i = 0; i &lt; 3; i++) {\n\t\t\tVertex01* pVX0 = allVertices.at(pTR-&gt;idx&#91;i]);\n\t\t\tpTR-&gt;idx&#91;i] = pVX0-&gt;altN;\n\t\t}\n\t}\n\treturn 1;\n}\n\nint ModelBuilder1base::moveGroupDg(ModelBuilder1base* pMB, float aX, float aY, float aZ, float kX, float kY, float kZ) {\n\t\/\/moves and rotates vertex group\n\t\/\/rotation angles are set in degrees\n\tmat4x4 transformMatrix = { 1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1 };\n\tmat4x4_translate(transformMatrix, kX, kY, kZ);\n\t\/\/rotation order: Z-X-Y\n\tif (aY != 0) mat4x4_rotate_Y(transformMatrix, transformMatrix, degrees2radians * aY);\n\tif (aX != 0) mat4x4_rotate_X(transformMatrix, transformMatrix, degrees2radians * aX);\n\tif (aZ != 0) mat4x4_rotate_Z(transformMatrix, transformMatrix, degrees2radians * aZ);\n\n\tint vertsN = pMB-&gt;vertices.size();\n\tfor (int i = pMB-&gt;pCurrentGroup-&gt;fromVertexN; i &lt; vertsN; i++) {\n\t\tVertex01* pVX = pMB-&gt;vertices.at(i);\n\t\tmat4x4_mul_vec4plus(pVX-&gt;aPos, transformMatrix, pVX-&gt;aPos, 1);\n\t\tmat4x4_mul_vec4plus(pVX-&gt;aNormal, transformMatrix, pVX-&gt;aNormal, 0);\n\t}\n\treturn 1;\n}\n\nvoid ModelBuilder1base::lockGroup(ModelBuilder1base* pMB) {\n\tif (pMB-&gt;pCurrentGroup != NULL)\n\t\tpMB-&gt;groupsStack.push_back(pMB-&gt;pCurrentGroup);\n\tpMB-&gt;pCurrentGroup = new Group01();\n\tpMB-&gt;pCurrentGroup-&gt;fromVertexN = pMB-&gt;vertices.size();\n\tpMB-&gt;pCurrentGroup-&gt;fromTriangleN = pMB-&gt;triangles.size();\n}\nvoid ModelBuilder1base::releaseGroup(ModelBuilder1base* pMB) {\n\tif (pMB-&gt;pLastClosedGroup != NULL)\n\t\tdelete pMB-&gt;pLastClosedGroup;\n\tpMB-&gt;pLastClosedGroup = pMB-&gt;pCurrentGroup;\n\n\tif (pMB-&gt;groupsStack.size() &gt; 0) {\n\t\tpMB-&gt;pCurrentGroup = pMB-&gt;groupsStack.back();\n\t\tpMB-&gt;groupsStack.pop_back();\n\t}\n\telse\n\t\tpMB-&gt;pCurrentGroup = NULL;\n}\nint ModelBuilder1base::calculateTangentSpace(std::vector&lt;Vertex01*&gt; useVertices, std::vector&lt;Triangle01*&gt; useTriangles) {\n\tint totalVertsN = useVertices.size();\n\tif (totalVertsN &lt; 1)\n\t\treturn 0;\n\tint totalTrianglesN = useTriangles.size();\n\t\/\/assuming that GL_TRIANGLES\n\t\/\/clear flags\n\tfor (int vN = 0; vN &lt; totalVertsN; vN++) {\n\t\tVertex01* pV = useVertices.at(vN);\n\t\tpV-&gt;flag = 0;\n\t}\n\tfor (int vN = 0; vN &lt; totalVertsN; vN++) {\n\t\tVertex01* pVX = useVertices.at(vN);\n\t\tif (pVX-&gt;flag != 0)\n\t\t\tcontinue;\n\t\tTriangle01* pT = NULL;\n\t\tfor (int tN = 0; tN &lt; totalTrianglesN; tN++) {\n\t\t\tpT = useTriangles.at(tN);\n\t\t\tbool haveTriangle = false;\n\t\t\tfor (int i = 0; i &lt; 3; i++)\n\t\t\t\tif (pT-&gt;idx&#91;i] == vN) {\n\t\t\t\t\thaveTriangle = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\tif (haveTriangle)\n\t\t\t\tbreak;\n\t\t}\n\t\tVertex01* pV&#91;3];\n\t\tfor (int i = 0; i &lt; 3; i++)\n\t\t\tpV&#91;i] = useVertices.at(pT-&gt;idx&#91;i]);\n\n\t\tfloat dPos1&#91;3];\n\t\tfloat dPos2&#91;3];\n\t\tfloat dUV1&#91;2];\n\t\tfloat dUV2&#91;2];\n\t\tfor (int i = 0; i &lt; 3; i++) {\n\t\t\tdPos1&#91;i] = pV&#91;1]-&gt;aPos&#91;i] - pV&#91;0]-&gt;aPos&#91;i];\n\t\t\tdPos2&#91;i] = pV&#91;2]-&gt;aPos&#91;i] - pV&#91;0]-&gt;aPos&#91;i];\n\t\t}\n\t\tfor (int i = 0; i &lt; 2; i++) {\n\t\t\tdUV1&#91;i] = pV&#91;1]-&gt;aTuv2&#91;i] - pV&#91;0]-&gt;aTuv2&#91;i];\n\t\t\tdUV2&#91;i] = pV&#91;2]-&gt;aTuv2&#91;i] - pV&#91;0]-&gt;aTuv2&#91;i];\n\t\t}\n\n\t\tfloat tangent&#91;3];\n\t\tfloat binormal&#91;3];\n\t\tfloat divider = dUV1&#91;0] * dUV2&#91;1] - dUV1&#91;1] * dUV2&#91;0];\n\t\tif (divider == 0) {\n\t\t\tv3set(tangent, 1, 0, 0);\n\t\t\tv3set(binormal, 0, -1, 0);\n\t\t}\n\t\telse {\n\t\t\tfloat r = 1.0f \/ divider;\n\t\t\tfor (int i = 0; i &lt; 3; i++) {\n\t\t\t\ttangent&#91;i] = (dPos1&#91;i] * dUV2&#91;1] - dPos2&#91;i] * dUV1&#91;1]) * r;\n\t\t\t\tbinormal&#91;i] = -(dPos2&#91;i] * dUV1&#91;0] - dPos1&#91;i] * dUV2&#91;0]) * r;\n\t\t\t}\n\t\t\tvec3_norm(tangent, tangent);\n\t\t\tvec3_norm(binormal, binormal);\n\t\t}\n\t\t\/\/add to all 3 vertices\n\t\tfor (int n = 0; n &lt; 3; n++) {\n\t\t\tif (pV&#91;n]-&gt;flag &gt; 0)\n\t\t\t\tcontinue;\n\t\t\tv3copy(pV&#91;n]-&gt;aTangent, tangent);\n\t\t\tv3copy(pV&#91;n]-&gt;aBinormal, binormal);\n\t\t\tpV&#91;n]-&gt;flag = 1;\n\t\t}\n\t}\n\t\/\/normalize tangent and binormal around normal\n\tfor (int vN = 0; vN &lt; totalVertsN; vN++) {\n\t\tVertex01* pV = useVertices.at(vN);\n\t\tfloat v3out&#91;3];\n\t\t\/\/tangent\n\t\tvec3_mul_cross(v3out, pV-&gt;aNormal, pV-&gt;aBinormal);\n\t\tif (v3dotProduct(pV-&gt;aTangent, v3out) &lt; 0)\n\t\t\tv3inverse(v3out);\n\t\tv3copy(pV-&gt;aTangent, v3out);\n\t\t\/\/binormal\n\t\tvec3_mul_cross(v3out, pV-&gt;aNormal, pV-&gt;aTangent);\n\t\tif (v3dotProduct(pV-&gt;aBinormal, v3out) &lt; 0)\n\t\t\tv3inverse(v3out);\n\t\tv3copy(pV-&gt;aBinormal, v3out);\n\t}\n\treturn 1;\n}\n\n<\/pre><\/div>\n\n\n<p><\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>In <em>buildSingleDrawJob(..)<\/em>, if normal map is involved (<em>pMT-&gt;uTex2nm &gt;= 0<\/em>), then we&#8217;ll call <em>calculateTangentSpace(..)<\/em><\/li><\/ul>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p> <\/p>\n\n\n\n<p><em>calculateTangentSpace(..)<\/em> uses new functions <em>v3inverse(..)<\/em> and <em>v3dotProduct(..)<\/em><\/p>\n\n\n\n<p>8. Open <em>utils.h<\/em> and replace code by: <\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; highlight: [16,17,18,19]; title: ; notranslate\" title=\"\">\n#pragma once\n#include &lt;string&gt;\n#include &lt;vector&gt;\n#include &quot;linmath.h&quot;\n\nint checkGLerrors(std::string ref);\nvoid mat4x4_mul_vec4plus(vec4 vOut, mat4x4 M, vec4 vIn, int v3);\n\nvoid v3set(float* vOut, float x, float y, float z);\nvoid v3copy(float* vOut, float* vIn);\nfloat v3pitchRd(float* vIn);\nfloat v3yawRd(float* vIn);\nfloat v3pitchDg(float* vIn);\nfloat v3yawDg(float* vIn);\n\nfloat v3dotProduct(float* a0, float* b0);\nfloat v3dotProductNorm(float* a, float* b);\nvoid v3inverse(float inV&#91;]);\nvoid v3inverse(float outV&#91;], float inV&#91;]);\nfloat v3length(float* v);\nfloat v3lengthXZ(float v&#91;]);\nfloat v3lengthXY(float v&#91;]);\nbool v3equals(float v&#91;], float x);\nbool v3match(float v0&#91;], float v1&#91;]);\nvoid v3fromTo(float* v, float* v0, float* v1);\nfloat v3lengthFromTo(float* v0, float* v1);\nvoid v3dirFromTo(float* v, float* v0, float* v1);\n\nlong long int getSystemMillis();\nlong long int getSystemNanos();\n\nint getRandom(int fromN, int toN);\nfloat getRandom(float fromN, float toN);\nstd::vector&lt;std::string&gt; splitString(std::string inString, std::string delimiter);\nstd::string trimString(std::string inString);\nbool fileExists(const char* filePath);\nstd::string getFullPath(std::string filePath);\nstd::string getInAppPath(std::string filePath);\nint makeDirs(std::string filePath);\n\n<\/pre><\/div>\n\n\n<p><\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>A few other vector functions also added<\/li><\/ul>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p>9. Open <em>utils.cpp<\/em> and replace code by:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; title: ; notranslate\" title=\"\">\n#include &quot;utils.h&quot;\n#include &quot;platform.h&quot;\n#include &lt;chrono&gt;\n#include &lt;stdlib.h&gt;     \/* srand, rand *\/\n#include &lt;sys\/stat.h&gt; \/\/if fileExists\n#include &lt;time.h&gt; \/\/for srand()\n\nextern std::string filesRoot;\nextern float radians2degrees;\n\nint checkGLerrors(std::string ref) {\n    \/\/can be used after any GL call\n    int res = glGetError();\n    if (res == 0)\n        return 0;\n    std::string errCode;\n    switch (res) {\n        \/\/case GL_NO_ERROR: errCode = &quot;GL_NO_ERROR&quot;; break;\n        case GL_INVALID_ENUM: errCode = &quot;GL_INVALID_ENUM&quot;; break;\n        case GL_INVALID_VALUE: errCode = &quot;GL_INVALID_VALUE&quot;; break;\n        case GL_INVALID_OPERATION: errCode = &quot;GL_INVALID_OPERATION&quot;; break;\n        case GL_INVALID_FRAMEBUFFER_OPERATION: errCode = &quot;GL_INVALID_FRAMEBUFFER_OPERATION&quot;; break;\n        case GL_OUT_OF_MEMORY: errCode = &quot;GL_OUT_OF_MEMORY&quot;; break;\n        default: errCode = &quot;??&quot;; break;\n    }\n    mylog(&quot;GL ERROR %d-%s in %s\\n&quot;, res, errCode.c_str(), ref.c_str());\n    return -1;\n}\nvoid mat4x4_mul_vec4plus(vec4 vOut, mat4x4 M, vec4 vIn, int v3) {\n    vec4 v2;\n    if (vOut == vIn) {\n        memcpy(&amp;v2, vIn, sizeof(vec4));\n        vIn = v2;\n    }\n    vIn&#91;3] = (float)v3;\n    mat4x4_mul_vec4(vOut, M, vIn);\n}\nvoid v3set(float* vOut, float x, float y, float z) {\n    vOut&#91;0] = x;\n    vOut&#91;1] = y;\n    vOut&#91;2] = z;\n}\nvoid v3copy(float* vOut, float* vIn) {\n    for (int i = 0; i &lt; 3; i++)\n        vOut&#91;i] = vIn&#91;i];\n}\nfloat v3yawRd(float* vIn) {\n    return atan2f(vIn&#91;0], vIn&#91;2]);\n}\nfloat v3pitchRd(float* vIn) {\n    return -atan2f(vIn&#91;1], sqrtf(vIn&#91;0] * vIn&#91;0] + vIn&#91;2] * vIn&#91;2]));\n}\nfloat v3pitchDg(float* vIn) { \n    return v3pitchRd(vIn) * radians2degrees; \n}\nfloat v3yawDg(float* vIn) { \n    return v3yawRd(vIn) * radians2degrees; \n}\n\nlong long int getSystemMillis() {\n    auto currentTime = std::chrono::system_clock::now().time_since_epoch();\n    return std::chrono::duration_cast&lt;std::chrono::milliseconds&gt;(currentTime).count();\n}\nlong long int getSystemNanos() {\n    auto currentTime = std::chrono::system_clock::now().time_since_epoch();\n    return std::chrono::duration_cast&lt;std::chrono::nanoseconds&gt;(currentTime).count();\n}\nint randomCallN = 0;\nint getRandom() {\n    if (randomCallN % 1000 == 0)\n        srand((unsigned int)getSystemNanos()); \/\/re-initialize random seed:\n    randomCallN++;\n    return rand();\n}\nint getRandom(int fromN, int toN) {\n    int randomN = getRandom();\n    int range = toN - fromN + 1;\n    return (fromN + randomN % range);\n}\nfloat getRandom(float fromN, float toN) {\n    int randomN = getRandom();\n    float range = toN - fromN;\n    return (fromN + (float)randomN \/ RAND_MAX * range);\n}\nstd::vector&lt;std::string&gt; splitString(std::string inString, std::string delimiter) {\n    std::vector&lt;std::string&gt; outStrings;\n    int delimiterSize = delimiter.size();\n    outStrings.clear();\n    while (inString.size() &gt; 0) {\n        int delimiterPosition = inString.find(delimiter);\n        if (delimiterPosition == 0) {\n            inString = inString.substr(delimiterSize, inString.size() - delimiterSize);\n            continue;\n        }\n        if (delimiterPosition == std::string::npos) {\n            \/\/last element\n            outStrings.push_back(trimString(inString));\n            break;\n        }\n        std::string outString = inString.substr(0, delimiterPosition);\n        outStrings.push_back(trimString(outString));\n        int startAt = delimiterPosition + delimiterSize;\n        inString = inString.substr(startAt, inString.size() - startAt);\n    }\n    return outStrings;\n}\nstd::string trimString(std::string inString) {\n    \/\/Remove leading and trailing spaces\n    int startsAt = inString.find_first_not_of(&quot; &quot;);\n    if (startsAt == std::string::npos)\n        return &quot;&quot;;\n    int endsAt = inString.find_last_not_of(&quot; &quot;) + 1;\n    return inString.substr(startsAt, endsAt - startsAt);\n}\nbool fileExists(const char* filePath) {\n    struct stat info;\n    if (stat(filePath, &amp;info) == 0)\n        return true;\n    else\n        return false;\n}\nstd::string getFullPath(std::string filePath) {\n    if (filePath.find(filesRoot) == 0)\n        return filePath;\n    else\n        return (filesRoot + filePath);\n}\nstd::string getInAppPath(std::string filePath) {\n    std::string inAppPath(filePath);\n    if (inAppPath.find(filesRoot) == 0) {\n        int startsAt = filesRoot.size();\n        inAppPath = inAppPath.substr(startsAt, inAppPath.size() - startsAt);\n    }\n    if (inAppPath.find(&quot;.&quot;) != std::string::npos) {\n        \/\/cut off file name\n        int endsAt = inAppPath.find_last_of(&quot;\/&quot;);\n        inAppPath = inAppPath.substr(0, endsAt + 1);\n    }\n    return inAppPath;\n}\nint makeDirs(std::string filePath) {\n    filePath = getFullPath(filePath);\n    std::string inAppPath = getInAppPath(filePath);\n    std::vector&lt;std::string&gt; path = splitString(inAppPath, &quot;\/&quot;);\n    int pathSize = path.size();\n    filePath.assign(filesRoot);\n    for (int i = 0; i &lt; pathSize; i++) {\n        filePath.append(&quot;\/&quot; + path.at(i));\n        if (fileExists(filePath.c_str())) {\n            continue;\n        }\n        \/\/create dir\n        myMkDir(filePath.c_str());\n        mylog(&quot;Folder %d: %s created.\\n&quot;, i, filePath.c_str());\n    }\n    return 1;\n}\nvoid v3inverse(float inV&#91;]) {\n    return v3inverse(inV, inV);\n}\nvoid v3inverse(float outV&#91;], float inV&#91;]) {\n    for (int i = 0; i &lt; 3; i++)\n        outV&#91;i] = -inV&#91;i];\n    return;\n}\n\nfloat v3dotProduct(float* a0, float* b0) {\n    float a&#91;3];\n    float b&#91;3];\n    vec3_norm(a, a0);\n    vec3_norm(b, b0);\n    return v3dotProductNorm(a, b);\n}\nfloat v3dotProductNorm(float* a, float* b) {\n    \/\/assuming that vectors are normalized\n    float dp = 0.0f;\n    for (int i = 0; i &lt; 3; i++)\n        dp += a&#91;i] * b&#91;i];\n    return dp;\n}\nfloat v3length(float* v) {\n    float r = v&#91;0] * v&#91;0] + v&#91;1] * v&#91;1] + v&#91;2] * v&#91;2];\n    if (r == 0)\n        return 0;\n    return sqrtf(r);\n}\nfloat v3lengthXZ(float v&#91;]) {\n    float r = v&#91;0] * v&#91;0] + v&#91;2] * v&#91;2];\n    if (r == 0)\n        return 0;\n    return sqrtf(r);\n}\nfloat v3lengthXY(float v&#91;]) {\n    float r = v&#91;0] * v&#91;0] + v&#91;1] * v&#91;1];\n    if (r == 0)\n        return 0;\n    return sqrtf(r);\n}\nbool v3equals(float v&#91;],float x) {\n    for (int i = 0; i &lt; 3; i++)\n        if (v&#91;i] != x)\n            return false;\n    return true;\n}\nbool v3match(float v0&#91;], float v1&#91;]) {\n    for (int i = 0; i &lt; 3; i++)\n        if (v0&#91;i] != v1&#91;i])\n            return false;\n    return true;\n}\nvoid v3fromTo(float* v, float* v0, float* v1) {\n    for (int i = 0; i &lt; 3; i++)\n        v&#91;i] = v1&#91;i] - v0&#91;i];\n}\nfloat v3lengthFromTo(float* v0, float* v1) {\n    float v&#91;3];\n    v3fromTo(v, v0, v1);\n    float r = v&#91;0] * v&#91;0] + v&#91;1] * v&#91;1] + v&#91;2] * v&#91;2];\n    if (r == 0)\n        return 0;\n    return sqrtf(r);\n}\nvoid v3dirFromTo(float* v, float* v0, float* v1) {\n    v3fromTo(v, v0, v1);\n    vec3_norm(v,v);\n}\n\n<\/pre><\/div>\n\n\n<p><\/p>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p>10. Build and run.<\/p>\n\n\n\n<p><strong>Before<\/strong>:<\/p>\n\n\n\n<p><img decoding=\"async\" src=\"https:\/\/writingagame.com\/img\/b01\/c30\/01.jpg\"><\/p>\n\n\n\n<p><\/p>\n\n\n\n<p><strong>After<\/strong>:<\/p>\n\n\n\n<p><img decoding=\"async\" src=\"https:\/\/writingagame.com\/img\/b01\/c31\/01.jpg\"><\/p>\n\n\n\n<p><\/p>\n\n\n\n<iframe loading=\"lazy\" width=\"100%\" height=\"400\" src=\"https:\/\/www.youtube.com\/embed\/sC8FODgJSds?controls=0&amp;autoplay=1&amp;loop=1&amp;playlist=sC8FODgJSds\" title=\"Normal maps\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen=\"\"><\/iframe>\n\n\n\n<p><\/p>\n\n\n\n<p>Now &#8211; embossed.<\/p>\n\n\n\n<p><\/p>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p>On Android also verified &#8211; direct fit !<\/p>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p class=\"mb-2\">Now I want to make gilded blazon and &#8220;Marlboro&#8221; sign on the front embossed. For this we&#8217;ll use Normal Maps. That&#8217;s a bluish images on the right: To burn them I used NormalMap-Online website. Default settings are for DirectX. For OpenGL &#8211; checkmark Invert R Click on the left picture to upload your own. You [&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-937","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\/937","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=937"}],"version-history":[{"count":27,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/posts\/937\/revisions"}],"predecessor-version":[{"id":1204,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/posts\/937\/revisions\/1204"}],"wp:attachment":[{"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/media?parent=937"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/categories?post=937"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/tags?post=937"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}