{"id":546,"date":"2021-12-06T04:32:33","date_gmt":"2021-12-06T04:32:33","guid":{"rendered":"https:\/\/writingagame.com\/?p=546"},"modified":"2023-05-15T22:00:05","modified_gmt":"2023-05-15T22:00:05","slug":"chapter-16-modeler","status":"publish","type":"post","link":"https:\/\/writingagame.com\/index.php\/2021\/12\/06\/chapter-16-modeler\/","title":{"rendered":"Chapter 16. Modeler"},"content":{"rendered":"\n<p>Now it&#8217;s time to think where to get actual 3D models for our Project. Our options are:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Web. There is a wide choice of pre-built models in different formats from professional and amateur 3D artists. Many of models are free.<\/li>\n\n\n\n<li>Learn Blender, Maya, 3Ds Max or some other 3D editor. A HUGE task by itself. Don&#8217;t feel ready for it yet.<\/li>\n\n\n\n<li>Find an artist. May not be cheap plus implies communication problems: artists think and work in different terms and concepts than programmers&#8230; The challenge with all 3 mentioned options &#8211; is how to read\/load such models to the app and how to control them. It can be a quite untrivial task. We&#8217;ll definitely address this issue later. But now I&#8217;d prefer a faster result.<\/li>\n\n\n\n<li>We can generate models programmatically. Doubtful that it will compete with professional 3D art work, but anyway, having such option definitely won&#8217;t hurt.<\/li>\n<\/ul>\n\n\n\n<p>So, let&#8217;s start with the last option &#8211; generate models <em>programmatically<\/em>. It will be a set of tools and classes, a considerable part of our engine. Let&#8217;s create a placeholder for it.<\/p>\n\n\n\n<p>1. In <strong>Windows File Explorer<\/strong> under <em>C:\\CPP\\engine<\/em> folder create a new sub-folder <strong>modeler<\/strong>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-css-opacity\"\/>\n\n\n\n<p>2. Go to <em>C:\\CPP<\/em> folder. Make a copy of <em>a998engine <\/em>and name it <strong>a997modeler<\/strong>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-css-opacity\"\/>\n\n\n\n<p>3. Start VS. Open <em>C:\\CPP\\<strong>a997modeler<\/strong>\\p_windows\\p_windows.sln<\/em>.<\/p>\n\n\n\n<p>Under <em>xEngine <\/em>(right-click on  <em>xEngine <\/em>) add New <strong>Filter<\/strong>, call it <strong>modeler<\/strong>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-css-opacity\"\/>\n\n\n\n<p>4. Add a reference to new folder: <\/p>\n\n\n\n<p>Open  <em>p_windows<\/em>  project Properties. All Configurations \/ Active(Win32), <\/p>\n\n\n\n<p><em>C\/C++ -&gt; General -&gt; Additional Include Directories -&gt; Edit<\/em>, add a new line, <\/p>\n\n\n\n<p>browse to <em>C:\\CPP\\engine\\modeler<\/em>, <\/p>\n\n\n\n<p>Select Folder, Ok, Apply, Ok.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-css-opacity\"\/>\n\n\n\n<p>The idea of the &#8220;modeler&#8221; is to build an array of vertices and indices and then convert them to VBOs, EBOs, VAOs and DrawJobs.<\/p>\n\n\n\n<p>5. Add the first class, <strong>Vertex01<\/strong>. As usual, file-by-file:<\/p>\n\n\n\n<p>Under <em>modeler <\/em>add new header file <strong>Vertex01.h<\/strong><\/p>\n\n\n\n<p>Location &#8211; <em>C:\\CPP\\engine\\modeler<\/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\nclass Vertex01\n{\npublic:\n\tint subjN = -1; \/\/game subject number\n\tint materialN = -1; \/\/material number\n\tint flag = 0;\n\tint endOfSequence = 0; \/\/for sequentional (unindexed) primitives (like GL_LINE_STRIP for example)\n\tint altN = -1; \/\/vertex&#039; position in alternative array\n\t\/\/atributes\n\tfloat aPos&#91;4] = { 0,0,0,0 }; \/\/position x,y,z + 4-th float for matrix operations\n\tfloat aNormal&#91;4] = { 0,0,0,0 }; \/\/normal (surface reflection vector) x,y,z + 4-th float for matrix operations\n\tfloat aTuv&#91;2] = { 0,0 }; \/\/2D texture coordinates\n\tfloat aTuv2&#91;2] = { 0,0 }; \/\/for normal maps\n\t\/\/tangent space (for normal maps)\n\tfloat aTangent&#91;3] = { 0,0,0 };\n\tfloat aBinormal&#91;3] = { 0,0,0 };\n};\n\n<\/pre><\/div>\n\n\n<p><\/p>\n\n\n\n<p>Don&#8217;t worry about extra variables, will explain them later, as we use them. Don&#8217;t need <em>cpp <\/em>file yet.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-css-opacity\"\/>\n\n\n\n<p>Since we are planning to use indexes (EBO) to draw triangles arrays, we&#8217;ll need a placeholder for them.<\/p>\n\n\n\n<p>6. Under <em>modeler <\/em>add new header file <strong>Triangle01.h<\/strong><\/p>\n\n\n\n<p>Location &#8211; <em>C:\\CPP\\engine\\modeler<\/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\nclass Triangle01\n{\npublic:\n\tint subjN = -1; \/\/game subject number\n\tint materialN = -1; \/\/material number\n\tint flag = 0;\n\tint idx&#91;3] = { 0,0,0 }; \/\/3 vertex indices\n};\n\n<\/pre><\/div>\n\n\n<p><\/p>\n\n\n\n<p>Same here, don&#8217;t worry about extra variables, will explain them later, as we use them.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-css-opacity\"\/>\n\n\n\n<p>Also we&#8217;ll need vertices\/triangles groups for various manipulations.<\/p>\n\n\n\n<p>7, Under <em>modeler <\/em>add new header file <strong>Group01.h<\/strong><\/p>\n\n\n\n<p>Location &#8211; <em>C:\\CPP\\engine\\modeler<\/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\nclass Group01\n{\npublic:\n\tint fromVertexN = 0;\n\tint fromTriangleN = 0;\n};\n\n<\/pre><\/div>\n\n\n<p><\/p>\n\n\n\n<p>We mostly need to know where\/when the current group started, that&#8217;s why there is no &#8220;to&#8221; variables.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-css-opacity\"\/>\n\n\n\n<p>Besides, we&#8217;ll need some kind of descriptor of which shape we are drawing\/building. I&#8217;ll call it <strong>VirtualShape<\/strong>. &#8220;Virtual&#8221; because it won&#8217;t create any actual vertices by itself.<\/p>\n\n\n\n<p>8. Under <em>modeler <\/em>add new header file <strong>VirtualShape.h<\/strong><\/p>\n\n\n\n<p>Location &#8211; <em>C:\\CPP\\engine\\modeler<\/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\nclass VirtualShape\n{\npublic:\n\tstd::string type = &quot;box&quot;;\n\tfloat whl&#91;3] = { 0 }; \/\/width\/height\/length (x,y,z sizes\/dimensions)\n};\n\n<\/pre><\/div>\n\n\n<p><\/p>\n\n\n\n<p>The key operation\/command of the <em>modeler <\/em>will be <em>buildFace()<\/em>. let&#8217;s say &#8220;on the left side of current <em>VirtualShape<\/em>&#8220;. This command will create 4 actual vertices on the left side of the shape (in this sample &#8211; of the box with <em>whl <\/em>dimensions) and 2 corresponding triangle indices (6 indices total).<\/p>\n\n\n\n<hr class=\"wp-block-separator has-css-opacity\"\/>\n\n\n\n<p>Some calculations we&#8217;ll move to <strong>utils <\/strong>set.<\/p>\n\n\n\n<p>9. Open <em>utils.h<\/em> and replace code by:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; highlight: [6]; title: ; notranslate\" title=\"\">\n#pragma once\n#include &lt;string&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\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>10. Open utils.cpp and replace code by:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; highlight: [22]; title: ; notranslate\" title=\"\">\n#include &quot;utils.h&quot;\n#include &quot;platform.h&quot;\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}\n\n<\/pre><\/div>\n\n\n<p><\/p>\n\n\n\n<p>This new <em>mat4x4_mul_vec4plus()<\/em> function is a vector\/matrix multiplication. The difference from <em>linmath <\/em>function is that it avoids writing result to the argument vector, plus it overwrites the last position of the argument vector. If it&#8217;s 0, the resulting vector will ignore position part of the transform matrix, so it will be just a direction (used for rotating <em>normals <\/em>for example).<\/p>\n\n\n\n<hr class=\"wp-block-separator has-css-opacity\"\/>\n\n\n\n<p>Also need to extend <strong>DrawJob<\/strong><\/p>\n\n\n\n<p>11. Open <em>DrawJob.h<\/em> and replace code by:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; highlight: [36,37]; title: ; notranslate\" title=\"\">\n#pragma once\n#include &quot;Material.h&quot;\n#include &lt;vector&gt;\n\nstruct AttribRef \/\/attribute reference\/description\n{\n\tunsigned int glVBOid = 0; \/\/buffer object id\n\tint offset = 0; \/\/variable&#039;s offset inside of VBO&#039;s element\n\tint stride = 0; \/\/Buffer&#039;s element size in bytes\n};\n\nclass DrawJob\n{\npublic:\n\tMaterial mt;\n\tint pointsN = 0; \/\/N of points to draw\n\tunsigned int glVAOid = 0; \/\/will hold data stream attributes mapping\/positions\n\tunsigned int glEBOid = 0; \/\/Element Buffer Object (vertex indices)\n\n\t\/\/common attributes\n\tAttribRef aPos;\n\tAttribRef aTuv;\n\n\t\/\/static arrays (vectors) of all loaded DrawJobs, VBO ids\n\tstatic std::vector&lt;DrawJob*&gt; drawJobs;\n\tstatic std::vector&lt;unsigned int&gt; buffersIds;\npublic:\n\tDrawJob();\n\tvirtual ~DrawJob(); \/\/destructor\n\tstatic int cleanUp();\n\tstatic int newBufferId();\n\tint buildVAO() { return buildVAOforShader(this, mt.shaderN); };\n\tstatic int buildVAOforShader(DrawJob* pDJ, int shaderN);\n\tstatic int attachAttribute(int varLocationInShader, int attributeSizeInFloats, AttribRef* pAttribRef);\n\n\tvirtual int setDesirableOffsets(int* pStride, int shaderN, int VBOid) { return setDesirableOffsetsForSingleVBO(this, pStride, shaderN, VBOid); };\n\tstatic int setDesirableOffsetsForSingleVBO(DrawJob* pDJ, int* pStride, int shaderN, int VBOid);\n\n\tint execute(float* uMVP, Material* pMt) { return executeDrawJob(this, uMVP, pMt); };\n\tstatic int executeDrawJob(DrawJob* pDJ, float* uMVP, Material* pMt);\n};\n\n<\/pre><\/div>\n\n\n<p><\/p>\n\n\n\n<p>This new <em>setDesirableOffsets..()<\/em> function fills out <em>DrawJob&#8217;s<\/em> attribute references (<em>AttribRef aPos<\/em> and <em>aTuv<\/em>, later we will have more attributes).<\/p>\n\n\n\n<hr class=\"wp-block-separator has-css-opacity\"\/>\n\n\n\n<p>12.  Open <em>DrawJob.cpp<\/em> and replace code by: <\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; highlight: [118]; title: ; notranslate\" title=\"\">\n#include &quot;DrawJob.h&quot;\n#include &quot;platform.h&quot;\n#include &quot;utils.h&quot;\n#include &quot;Shader.h&quot;\n#include &quot;Texture.h&quot;\n\n\/\/static arrays (vectors) of all loaded DrawJobs, VBO ids\nstd::vector&lt;DrawJob*&gt; DrawJob::drawJobs;\nstd::vector&lt;unsigned int&gt; DrawJob::buffersIds;\n\nDrawJob::DrawJob() {\n\tdrawJobs.push_back(this);\n}\nDrawJob::~DrawJob() {\n\tglBindBuffer(GL_ARRAY_BUFFER, 0);\n\tglBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);\n\tif (glVAOid &gt; 0)\n\t\tglDeleteVertexArrays(1, &amp;glVAOid);\n}\nint DrawJob::newBufferId() {\n\tunsigned int bufferId;\n\tglGenBuffers(1, &amp;bufferId);\n\tbuffersIds.push_back(bufferId);\n\treturn (int)bufferId;\n}\nunsigned int activeVBOid;\nint DrawJob::buildVAOforShader(DrawJob* pDJ, int shaderN) {\n\t\/\/delete VAO if exists already\n\tif (pDJ-&gt;glVAOid &gt; 0) {\n\t\tglBindBuffer(GL_ARRAY_BUFFER, 0);\n\t\tglDeleteVertexArrays(1, &amp;(pDJ-&gt;glVAOid));\n\t}\n\tglGenVertexArrays(1, &amp;pDJ-&gt;glVAOid);\n\tglBindVertexArray(pDJ-&gt;glVAOid);\n\n\t\/\/open shader descriptor to access variables locations\n\tShader* pShader = Shader::shaders.at(pDJ-&gt;mt.shaderN);\n\n\tactiveVBOid = 0;\n\tattachAttribute(pShader-&gt;l_aPos, 3, &amp;pDJ-&gt;aPos);\n\tattachAttribute(pShader-&gt;l_aTuv, 2, &amp;pDJ-&gt;aTuv);\n\n\tif (pDJ-&gt;glEBOid &gt; 0)\n\t\tglBindBuffer(GL_ELEMENT_ARRAY_BUFFER, pDJ-&gt;glEBOid);\n\n\tglBindVertexArray(0);\n\tglBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);\n\tglBindBuffer(GL_ARRAY_BUFFER, 0);\n\treturn 1;\n}\n\nint DrawJob::attachAttribute(int varLocationInShader, int attributeSizeInFloats, AttribRef* pAR) {\n\tif (varLocationInShader &lt; 0)\n\t\treturn 0; \/\/not used in this shader\n\tif (pAR-&gt;glVBOid == 0) {\n\t\tmylog(&quot;ERROR in DrawJob::attachAttribute, nk such attribute\/VBO\\n&quot;);\n\t\treturn -1;\n\t}\n\tglEnableVertexAttribArray(varLocationInShader);\n\tif (activeVBOid != pAR-&gt;glVBOid) {\n\t\tactiveVBOid = pAR-&gt;glVBOid;\n\t\t\/\/attach input stream data\n\t\tglBindBuffer(GL_ARRAY_BUFFER, activeVBOid);\n\t}\n\tglVertexAttribPointer(varLocationInShader, attributeSizeInFloats, GL_FLOAT, GL_FALSE, pAR-&gt;stride, (void*)(long)pAR-&gt;offset);\n\treturn 1;\n}\nint DrawJob::executeDrawJob(DrawJob* pDJ, float* uMVP, Material* pMt) {\n\tif (pMt == NULL)\n\t\tpMt = &amp;(pDJ-&gt;mt);\n\tglBindVertexArray(pDJ-&gt;glVAOid);\n\tShader* pShader = Shader::shaders.at(pMt-&gt;shaderN);\n\tglUseProgram(pShader-&gt;GLid);\n\tglUniformMatrix4fv(pShader-&gt;l_uMVP, 1, GL_FALSE, (const GLfloat*)uMVP);\n\n\t\/\/attach textures\n\tif (pShader-&gt;l_uTex0 &gt;= 0) {\n\t\tint textureId = Texture::getGLid(pMt-&gt;uTex0);\n\t\t\/\/pass textureId to shader program\n\t\tglActiveTexture(GL_TEXTURE0); \/\/ activate the texture unit first before binding texture\n\t\tglBindTexture(GL_TEXTURE_2D, textureId);\n\t\t\/\/ Tell the texture uniform sampler to use this texture in the shader by binding to texture unit 0.    \n\t\tglUniform1i(pShader-&gt;l_uTex0, 0);\n\t}\n\t\/\/other uniforms\n\tif (pShader-&gt;l_uColor &gt;= 0) {\n\t\t\/\/float uColor&#91;4] = { 1.f, 0.f, 1.f, 1.f }; \/\/R,G,B, alpha\n\t\tglUniform4fv(pShader-&gt;l_uColor, 1, pMt-&gt;uColor.forGL());\n\t}\n\tif (pDJ-&gt;glEBOid == 0) {\n\t\tglDrawArrays(pMt-&gt;primitiveType, 0, pDJ-&gt;pointsN);\n\t}\n\telse { \/\/use EBO\n\t\tglDrawElements(pMt-&gt;primitiveType, pDJ-&gt;pointsN, GL_UNSIGNED_SHORT, 0);\n\t}\n\tglBindVertexArray(0);\n\treturn 1;\n}\nint DrawJob::cleanUp() {\n\tint itemsN = drawJobs.size();\n\t\/\/delete all drawJobs\n\tfor (int i = 0; i &lt; itemsN; i++) {\n\t\tDrawJob* pDJ = drawJobs.at(i);\n\t\tdelete pDJ;\n\t}\n\tdrawJobs.clear();\n\t\/\/delete Buffers\n\titemsN = buffersIds.size();\n\t\/\/delete all buffers\n\tfor (int i = 0; i &lt; itemsN; i++) {\n\t\tunsigned int id = buffersIds.at(i);\n\t\tglDeleteBuffers(1, &amp;id);\n\t}\n\tbuffersIds.clear();\n\n\treturn 1;\n}\nint DrawJob::setDesirableOffsetsForSingleVBO(DrawJob* pDJ, int* pStride, int shaderN, int VBOid) {\n\t\/\/sets desirable offsets and stride according to given shader needs\n\t\/\/assuming that we have 1 single VBO\n\tShader* pSh = Shader::shaders.at(shaderN);\n\tint stride = 0;\n\tpDJ-&gt;aPos.offset = 0; \/\/attribute o_aPos, always 0\n\tstride += sizeof(float) * 3; \/\/aPos size - 3 floats (x,y,z)\n\tif (pSh-&gt;l_aTuv &gt;= 0) { \/\/attribute TUV (texture coordinates)\n\t\tpDJ-&gt;aTuv.offset = stride; \/\/attribute TUV (texture coordinates)\n\t\tstride += sizeof(float) * 2;\n\t}\n\t*pStride = stride;\n\t\/\/add stride and VBOid to all attributes\n\tAttribRef* pAR = NULL;\n\tpAR = &amp;pDJ-&gt;aPos; pAR-&gt;glVBOid = VBOid; pAR-&gt;stride = stride;\n\tpAR = &amp;pDJ-&gt;aTuv; pAR-&gt;glVBOid = VBOid; pAR-&gt;stride = stride;\n\n\treturn 1;\n}\n\n<\/pre><\/div>\n\n\n<p><\/p>\n\n\n\n<p>To be continued&#8230;<\/p>\n\n\n\n<hr class=\"wp-block-separator has-css-opacity\"\/>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p class=\"mb-2\">Now it&#8217;s time to think where to get actual 3D models for our Project. Our options are: So, let&#8217;s start with the last option &#8211; generate models programmatically. It will be a set of tools and classes, a considerable part of our engine. Let&#8217;s create a placeholder for it. 1. In Windows File Explorer under [&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-546","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\/546","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=546"}],"version-history":[{"count":24,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/posts\/546\/revisions"}],"predecessor-version":[{"id":1960,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/posts\/546\/revisions\/1960"}],"wp:attachment":[{"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/media?parent=546"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/categories?post=546"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/tags?post=546"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}