{"id":438,"date":"2021-12-04T02:26:46","date_gmt":"2021-12-04T02:26:46","guid":{"rendered":"https:\/\/writingagame.com\/?p=438"},"modified":"2021-12-04T05:31:24","modified_gmt":"2021-12-04T05:31:24","slug":"chapter-10-simple-shader","status":"publish","type":"post","link":"https:\/\/writingagame.com\/index.php\/2021\/12\/04\/chapter-10-simple-shader\/","title":{"rendered":"Chapter 10. Simple shader"},"content":{"rendered":"\n<p>Finally, <strong>shaders<\/strong>! Let&#8217;s start from the easiest one, just flat uniform color, which we will pass to the shader as an input, along with mesh data (in our sample &#8211; a triangle).<\/p>\n\n\n\n<p>Shaders use GLSL (OpenGL Shading Language with syntax similar to C). GLSL is executed directly by the graphics pipeline.<\/p>\n\n\n\n<p>The executable shader program consists of 2 shaders: Vertex Shader, which processes vertex data and transforms vertex (points) positions into 3D drawing coordinates. Output goes to rasterizer, which will call Fragment (Pixel) Shader for every involved pixel.<\/p>\n\n\n\n<ol class=\"wp-block-list\"><li><strong>Vertex Shader<\/strong>:<\/li><\/ol>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; title: ; notranslate\" title=\"\">\n#version 320 es\nprecision lowp float;\nuniform mat4 uMVP; \/\/ transform matrix (Model-View-Projection)\nin vec3 aPos; \/\/ position attribute (x,y,z)\nvoid main(){\n  gl_Position = uMVP * vec4(aPos, 1.0);\n}\n\n<\/pre><\/div>\n\n\n<p><\/p>\n\n\n\n<p>Copy this code to a <strong>Text Editor<\/strong> and save it as a <em>txt <\/em>file in<\/p>\n\n\n\n<p><em>C:\\CPP\\engine\\dt\\shaders\\<strong>flat_ucolor_v.txt<\/strong><\/em><\/p>\n\n\n\n<p>Hope, there is no need to explain what it does, guess it&#8217;s obvious enough. Or you can check the GLSL documentation online.<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>Ok to delete <em>test0.txt<\/em> from there. We won&#8217;t need it any more.<\/li><\/ul>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p>2. <strong>Fragment (Pixel) Shader<\/strong>:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; title: ; notranslate\" title=\"\">\n#version 320 es\nprecision lowp float;\nuniform vec4 uColor;\nout vec4 FragColor; \/\/output pixel color\nvoid main(){\n  FragColor = uColor;\n}\n\n<\/pre><\/div>\n\n\n<p><\/p>\n\n\n\n<p>Save this code as a <em>txt <\/em>file in<\/p>\n\n\n\n<p> <em>C:\\CPP\\engine\\dt\\shaders\\flat_ucolor_<strong>f<\/strong>.txt<\/em> <\/p>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p>Now let&#8217;s try to use them.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Windows<\/h2>\n\n\n\n<p>3. Start VS. Open <em>C:\\CPP\\a999hello\\p_windows\\p_windows.sln<\/em>.<\/p>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p>First, we&#8217;ll need a function checking for GL errors. We&#8217;ll place it in a separate <strong>utils <\/strong>set.<\/p>\n\n\n\n<p>4. Under <em>xEngine <\/em>add <strong>New Item<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><em>Header File (.h)<\/em><\/li><li>Name: <em>utils.h<\/em><\/li><li>Location: <em>C:\\CPP\\engine\\<\/em><\/li><\/ul>\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\nint checkGLerrors(std::string ref);\n\n<\/pre><\/div>\n\n\n<p><\/p>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p>5. Under <em>xEngine <\/em>add <strong>New Item<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><em>C++ File (.cpp)<\/em><\/li><li>Name: <em>utils.cpp<\/em><\/li><li>Location: <em>C:\\CPP\\engine\\<\/em><\/li><\/ul>\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;platform.h&quot;\n#include &lt;string&gt;\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}\n\n<\/pre><\/div>\n\n\n<p><\/p>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p>We&#8217;ll place shaders-related code in a separate <strong>Shaders <\/strong>class. <\/p>\n\n\n\n<p>6. Under <em>xEngine <\/em>add <strong>New Item<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><em>Header File (.h)<\/em><\/li><li>Name: <em>Shader.h<\/em><\/li><li>Location: <em>C:\\CPP\\engine\\<\/em><\/li><\/ul>\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 &quot;platform.h&quot;\n#include &lt;string&gt;\n\nclass Shader\n{\npublic:\n\tstatic int linkShaderProgram(const char* filePathVertexS, const char* filePathFragmentS);\n\tstatic int compileShader(const char* filePath, GLenum shaderType);\n\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\"\/>\n\n\n\n<p>7. Under <em>xEngine <\/em>add <strong>New Item<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><em>C++ File (.cpp)<\/em><\/li><li>Name: <em>Shader.cpp<\/em><\/li><li>Location: <em>C:\\CPP\\engine\\<\/em><\/li><\/ul>\n\n\n\n<p>Code:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; highlight: [31]; title: ; notranslate\" title=\"\">\n#include &quot;Shader.h&quot;\n#include &quot;platform.h&quot;\n#include &quot;utils.h&quot;\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\"\/>\n\n\n\n<p>Please note, that in <em>compileShader(..)<\/em> function, for opening a file, we are using our own custom function <strong>myFopen_s <\/strong>(line 31, same syntax as <em>fopen_s<\/em>). The reason is: in Windows old-fashioned <em>fopen <\/em>is deprecated, must use <em>fopen_s<\/em>, while on Android <em>fopen_s <\/em>is not implemented yet, must use <em>fopen<\/em>. So, we need 2 <strong>platform-specific<\/strong> implementations.<\/p>\n\n\n\n<p>8. Open <em>platform.h<\/em> and replace code by following:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; highlight: [8]; title: ; notranslate\" title=\"\">\n#pragma once\n#include &lt;glad\/glad.h&gt;\n#include &lt;stdio.h&gt;\n\nvoid mylog(const char* _Format, ...);\nvoid mySwapBuffers();\nvoid myPollEvents();\nint myFopen_s(FILE** pFile, const char* filePath, const char* mode);\n\n<\/pre><\/div>\n\n\n<p><\/p>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p>9. Open <em>platform.cpp<\/em> and replace code by following: <\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; highlight: [30]; title: ; notranslate\" title=\"\">\n#include &lt;stdarg.h&gt;\n#include &lt;stdio.h&gt;\n#include &lt;GLFW\/glfw3.h&gt;\n#include &quot;platform.h&quot;\n#include &quot;TheGame.h&quot;\n\nextern GLFWwindow* myMainWindow;\nextern TheGame theGame;\n\nvoid mylog(const char* _Format, ...) {\n#ifdef _DEBUG\n    va_list _ArgList;\n    va_start(_ArgList, _Format);\n    vprintf(_Format, _ArgList);\n    va_end(_ArgList);\n#endif\n};\nvoid mySwapBuffers() {\n    glfwSwapBuffers(myMainWindow);\n}\nvoid myPollEvents() {\n    glfwPollEvents();\n    \/\/check if closing the window\n    theGame.bExitGame = glfwWindowShouldClose(myMainWindow);\n    \/\/check screen size\n    int width, height;\n    glfwGetFramebufferSize(myMainWindow, &amp;width, &amp;height);\n    theGame.onScreenResize(width, height);\n}\nint myFopen_s(FILE** pFile, const char* filePath, const char* mode) {\n    return fopen_s(pFile, filePath, mode);\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. Now, open <em>TheGame.cpp<\/em> and replace code by following:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; highlight: [8,9,10,11,12,13,14,15,16,18,19,32]; title: ; notranslate\" title=\"\">\n#include &quot;TheGame.h&quot;\n#include &quot;platform.h&quot;\n#include &quot;linmath.h&quot;\n#include &quot;Shader.h&quot;\n\nextern std::string filesRoot;\n\nstatic const struct\n{\n    float x, y, z;\n} vertices&#91;3] =\n{\n    { -0.6f, -0.4f, 0.f },\n    {  0.6f, -0.4f, 0.f },\n    {   0.f,  0.6f, 0.f }\n};\n\nunsigned int vao_buffer, vertex_buffer, shaderProgramId;\nint umvp_location, apos_location, ucol_location;\nfloat angle_z = 0;\n\nint TheGame::getReady() {\n    bExitGame = false;\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    shaderProgramId = Shader::linkShaderProgram((filesRoot + &quot;\/dt\/shaders\/flat_ucolor_v.txt&quot;).c_str(), (filesRoot + &quot;\/dt\/shaders\/flat_ucolor_f.txt&quot;).c_str());\n\n    umvp_location = glGetUniformLocation(shaderProgramId, &quot;uMVP&quot;);\n\n    apos_location = glGetAttribLocation(shaderProgramId, &quot;aPos&quot;);\n    glEnableVertexAttribArray(apos_location);\n    glVertexAttribPointer(apos_location, 3, GL_FLOAT, GL_FALSE,\n        sizeof(vertices&#91;0]), (void*)0);\n\n    ucol_location = glGetUniformLocation(shaderProgramId, &quot;uColor&quot;);\n\n    glUseProgram(shaderProgramId);\n\n    float uColor&#91;4] = { 1.f, 0.f, 1.f, 1.f }; \/\/R,G,B, alpha\n    glUniform4fv(ucol_location, 1, uColor);\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_ortho(p, -screenRatio, screenRatio, -1.f, 1.f, 1.f, -1.f);\n    mat4x4_mul(mvp, p, m);\n\n    glUseProgram(shaderProgramId);\n    glUniformMatrix4fv(umvp_location, 1, GL_FALSE, (const GLfloat*)mvp);\n\n    glDrawArrays(GL_TRIANGLES, 0, 3);\n\n    mySwapBuffers();\n    return 1;\n}\nint TheGame::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>Now it&#8217;s almost twice shorter.<\/p>\n\n\n\n<p>Important changes:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>No more hard-coded inline shaders <em>vertex_shader_text <\/em>and <em>fragment_shader_text<\/em>.<\/li><li>Don&#8217;t need variables <em>vertex_shader <\/em>and <em>fragment_shader <\/em>either.<\/li><li>Triangle vertex data structure is different. Instead of (2D coordinates + color) now we have (3D coordinates, no color).<\/li><li>Few variables renamed for the sake of consistency.<\/li><\/ul>\n\n\n\n<p><\/p>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p>11. Build and run:<\/p>\n\n\n\n<p><img decoding=\"async\" src=\"https:\/\/writingagame.com\/img\/b01\/c10\/01.jpg\"><\/p>\n\n\n\n<p><\/p>\n\n\n\n<p>Perfect.<\/p>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p>Now &#8211; on<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Android<\/h2>\n\n\n\n<p>12. Close and re-start Visual Studio, open&nbsp;<em>C:\\CPP\\a999hello\\p_android\\p_android.sln<\/em><\/p>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p>13. Under <em>xEngine <\/em>add <strong>Existing Item<\/strong><\/p>\n\n\n\n<p>Go to <em>C:\\CPP\\engine<\/em>, pick<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>Shader.cpp<\/li><li>Shader.h<\/li><li>utils.cpp<\/li><li>utils.h<\/li><\/ul>\n\n\n\n<p><strong>Add<\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p>Need to add Android&#8217;s <em>myFopen_s(..)<\/em> implementation.<\/p>\n\n\n\n<p>14. Open <em>platform.h<\/em> and replace code by following:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; highlight: [6]; title: ; notranslate\" title=\"\">\n#pragma once\n\nvoid mylog(const char* _Format, ...);\nvoid mySwapBuffers();\nvoid myPollEvents();\nint myFopen_s(FILE** pFile, const char* filePath, const char* mode);\n\n<\/pre><\/div>\n\n\n<p><\/p>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p>15.  Open <em>platform.cpp<\/em> and replace code by following: <\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; highlight: [64]; title: ; notranslate\" title=\"\">\n#include &lt;android\/log.h&gt;\n#include &quot;stdio.h&quot;\n#include &quot;TheGame.h&quot;\n\nextern struct android_app* androidApp;\nextern const ASensor* accelerometerSensor;\nextern ASensorEventQueue* sensorEventQueue;\n\nextern EGLDisplay androidDisplay;\nextern EGLSurface androidSurface;\nextern TheGame theGame;\n\nvoid mylog(const char* _Format, ...) {\n#ifdef _DEBUG\n    char outStr&#91;1024];\n    va_list _ArgList;\n    va_start(_ArgList, _Format);\n    vsprintf(outStr, _Format, _ArgList);\n    __android_log_print(ANDROID_LOG_INFO, &quot;mylog&quot;, outStr, NULL);\n    va_end(_ArgList);\n#endif\n};\n\nvoid mySwapBuffers() {\n\teglSwapBuffers(androidDisplay, androidSurface);\n}\nvoid myPollEvents() {\n\t\/\/ Read all pending events.\n\tint ident;\n\tint events;\n\tstruct android_poll_source* source;\n\n\t\/\/ If not animating, we will block forever waiting for events.\n\t\/\/ If animating, we loop until all events are read, then continue\n\t\/\/ to draw the next frame of animation.\n\twhile ((ident = ALooper_pollAll(0, NULL, &amp;events,\n\t\t(void**)&amp;source)) &gt;= 0) {\n\n\t\t\/\/ Process this event.\n\t\tif (source != NULL) {\n\t\t\tsource-&gt;process(androidApp, source);\n\t\t}\n\n\t\t\/\/ If a sensor has data, process it now.\n\t\tif (ident == LOOPER_ID_USER) {\n\t\t\tif (accelerometerSensor != NULL) {\n\t\t\t\tASensorEvent event;\n\t\t\t\twhile (ASensorEventQueue_getEvents(sensorEventQueue,\n\t\t\t\t\t&amp;event, 1) &gt; 0) {\n\t\t\t\t\t\/\/LOGI(&quot;accelerometer: x=%f y=%f z=%f&quot;,\n\t\t\t\t\t\/\/\tevent.acceleration.x, event.acceleration.y,\n\t\t\t\t\t\/\/\tevent.acceleration.z);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t\/\/ Check if we are exiting.\n\t\tif (androidApp-&gt;destroyRequested != 0) {\n\t\t\ttheGame.bExitGame = true;\n\t\t\tbreak;\n\t\t}\n\t}\n}\nint myFopen_s(FILE** pFile, const char* filePath, const char* mode) {\n\t*pFile = fopen(filePath, mode);\n\tif (*pFile == NULL) {\n\t\tmylog(&quot;ERROR: can&#039;t open file %s\\n&quot;, filePath);\n\t\treturn -1;\n\t}\n\treturn 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>16. Turn on, unlock, plug in, allow.<\/p>\n\n\n\n<p>Build and run.<\/p>\n\n\n\n<p>Good.<\/p>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n","protected":false},"excerpt":{"rendered":"<p class=\"mb-2\">Finally, shaders! Let&#8217;s start from the easiest one, just flat uniform color, which we will pass to the shader as an input, along with mesh data (in our sample &#8211; a triangle). Shaders use GLSL (OpenGL Shading Language with syntax similar to C). GLSL is executed directly by the graphics pipeline. The executable shader program [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":452,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[4],"tags":[],"class_list":["post-438","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-cross-platform-3d"],"_links":{"self":[{"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/posts\/438","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=438"}],"version-history":[{"count":26,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/posts\/438\/revisions"}],"predecessor-version":[{"id":465,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/posts\/438\/revisions\/465"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/media\/452"}],"wp:attachment":[{"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/media?parent=438"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/categories?post=438"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/writingagame.com\/index.php\/wp-json\/wp\/v2\/tags?post=438"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}