{"id":3128,"date":"2026-01-01T21:59:58","date_gmt":"2026-01-01T21:59:58","guid":{"rendered":"https:\/\/timallanwheeler.com\/blog\/?p=3128"},"modified":"2026-01-02T00:51:08","modified_gmt":"2026-01-02T00:51:08","slug":"into-depth-unified-triangle-shader","status":"publish","type":"post","link":"https:\/\/timallanwheeler.com\/blog\/2026\/01\/01\/into-depth-unified-triangle-shader\/","title":{"rendered":"Into Depth &#8211; Unified Triangle Shader"},"content":{"rendered":"\n<p>Into Depth is the name of a little game project I have been working on for over a year now. Its roots go back even further, to <a href=\"https:\/\/timallanwheeler.com\/blog\/2023\/12\/02\/a-side-scroller-physics-system\/\">the 2D platformer I had started working on two years ago<\/a>. The platformer ended up teaching me a lot about OpenGL, which I have continued to use in a fairly low-level manner in this new project.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-medium\"><img loading=\"lazy\" decoding=\"async\" width=\"300\" height=\"300\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2026\/01\/A8B9540D-C5A2-4B04-BB75-DF9DCAA703D0-300x300.png\" alt=\"Concept art of stone ruins with multiple characters running around.\" class=\"wp-image-3134\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2026\/01\/A8B9540D-C5A2-4B04-BB75-DF9DCAA703D0-300x300.png 300w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2026\/01\/A8B9540D-C5A2-4B04-BB75-DF9DCAA703D0-150x150.png 150w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2026\/01\/A8B9540D-C5A2-4B04-BB75-DF9DCAA703D0-768x768.png 768w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2026\/01\/A8B9540D-C5A2-4B04-BB75-DF9DCAA703D0.png 1024w\" sizes=\"auto, (max-width: 300px) 100vw, 300px\" \/><\/figure>\n<\/div>\n\n\n<p class=\"has-text-align-center has-small-font-size\">Concept image by ChatGPT.<\/p>\n\n\n\n<p>In a nutshell, the game is a simplification of the 2D platformer born both from managing complexity and wanting something I could reasonably make multiplayer and play with my friends online. Into Depth is a turn-based 2D sidescroller on a grid. Kind of like <a href=\"https:\/\/store.steampowered.com\/app\/780290\/Gloomhaven\/\">Gloomhaven<\/a>, but vertical:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"455\" height=\"361\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2026\/01\/2026-01-01_09-14.png\" alt=\"A square grid with solid walls, a hero in one tile, a rope extending several tiles down from an entryway in the ceiling, and a key. Two edges are marked 'a'.\" class=\"wp-image-3130\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2026\/01\/2026-01-01_09-14.png 455w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2026\/01\/2026-01-01_09-14-300x238.png 300w\" sizes=\"auto, (max-width: 455px) 100vw, 455px\" \/><\/figure>\n<\/div>\n\n\n<p class=\"has-text-align-center has-small-font-size\">An early mockup from my Google slides design doc.<\/p>\n\n\n\n<p>The team of heroes descends into dungeons (e.g., down the rope in the image above) with the goal of retrieving relics. Oftentimes these relics are heavy and require hauling to get them back to the surface. Collected relics can be equipped and used in other levels, and teams may revisit levels in order to get even deeper. That&#8217;s the rough idea.<\/p>\n\n\n\n<p>There are additional details, of course. For example, I want non-Euclidean geometry to play a role. In the screen above, the two edges labeled &#8216;a&#8217; are joined, such that the hero&#8217;s view is:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"683\" height=\"326\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2026\/01\/2026-01-01_09-19.png\" alt=\"Similar to the previous image, but expanded such that looking through edge 'a' is possible.\" class=\"wp-image-3131\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2026\/01\/2026-01-01_09-19.png 683w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2026\/01\/2026-01-01_09-19-300x143.png 300w\" sizes=\"auto, (max-width: 683px) 100vw, 683px\" \/><\/figure>\n<\/div>\n\n\n<p class=\"has-text-align-center has-small-font-size\">The hero can see the key from two different vantage directions because of the portal.<\/p>\n\n\n\n<p>This is made more interesting by having each hero only be able to see what is in their line of sight. Not only does that add an element of discovery and the need to juggle teammates&#8217; perspectives, it also enables stealth mechanics.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"738\" height=\"291\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2026\/01\/IMG_2948.jpg\" alt=\"An in-game screenshot centered on the hero, with shadow casting blocking things not in sight. An enemy is visible twice due to portal effects.\" class=\"wp-image-3133\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2026\/01\/IMG_2948.jpg 738w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2026\/01\/IMG_2948-300x118.jpg 300w\" sizes=\"auto, (max-width: 738px) 100vw, 738px\" \/><\/figure>\n<\/div>\n\n\n<p class=\"has-text-align-center has-small-font-size\">Solid geometry blocks the view of what is behind it. This image was from before I went orthographic.<\/p>\n\n\n\n<p>I am writing this myself, for fun, in my spare time. My primary goal is to learn how things work, and to actually get something playable for myself and my friends. <\/p>\n\n\n\n<p>I am using C++ and&nbsp;<a href=\"https:\/\/code.visualstudio.com\/\">VS Code<\/a>, on Linux. I try to minimize library use, but leverage&nbsp;<a href=\"https:\/\/www.libsdl.org\/\">SDL2<\/a>&nbsp;as my platform library, use <a href=\"https:\/\/glad.dav1d.de\/\">GLAD<\/a> for OpenGL, the <a href=\"https:\/\/github.com\/g-truc\/glm\">GLM<\/a> types, and several <a href=\"https:\/\/github.com\/nothings\/stb\">Sean Barrett STB libraries<\/a>. This includes avoiding the standard libraries, such as std::vector and std::map, and I&#8217;m trying to use <a href=\"https:\/\/nullprogram.com\/blog\/2023\/10\/08\/\">my own string view s8 as defined by Chris Wellons instead of std::string<\/a>. I actually haven&#8217;t been using <a href=\"https:\/\/github.com\/ocornut\/imgui\">ImGui<\/a> in this project, but I probably will once I have more of an editor.<\/p>\n\n\n\n<p>Why these restrictions? Because I am a big fan of <a href=\"https:\/\/caseymuratori.com\/about\">Casey Muratori<\/a>, <a href=\"https:\/\/en.wikipedia.org\/wiki\/Jonathan_Blow\">Jonathan Blow<\/a>, and the do-it-yourself attitude of understanding your code. It is a great way to learn how it all works, and it is fun! Presumably it leads to smaller executables and faster code too.<\/p>\n\n\n\n<p>The art you see here is programmer art, since I am a programmer. That is good enough for now. My long-term vision is to have a low-poly aesthetic and mostly <em>not<\/em>  use sprites, since I should be able to author low-poly meshes myself. The scene will be 3D with effects like lighting, but will be rendered in orthographic since that just makes it so much easier to see and work with the grid. <\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Font Glyphs + Sprites<\/h1>\n\n\n\n<p>I was most recently working on some basic shaders for the game. I had just added font rendering with its own shader for rendering glyph quads. A UI system typically needs things beyond just font glyphs, like UI panels and sprite icons, and those are mostly just basic quads as well. It seemed obvious that I could share a shader for the dual uses of rendering font quads and rendering sprite quads:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full is-resized\"><img loading=\"lazy\" decoding=\"async\" width=\"791\" height=\"253\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2026\/01\/2026-01-01_09-42.png\" alt=\"A vertex consisting of pos, uv, and color. Uniforms are a proj_view matrix and a texture. A font glyph is defined as a 2-triangle quad with uv coordinates indexing into an atlas.\" class=\"wp-image-3136\" style=\"width:683px;height:auto\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2026\/01\/2026-01-01_09-42.png 791w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2026\/01\/2026-01-01_09-42-300x96.png 300w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2026\/01\/2026-01-01_09-42-768x246.png 768w\" sizes=\"auto, (max-width: 791px) 100vw, 791px\" \/><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>The shader is really simple. In the vertex shader, the position is just:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">gl_Position = proj_view * vec4(aPosWorld, 1.0);<\/code><\/pre>\n\n\n\n<p>And then the fragment shader uses the input color as  a componentwise multiplier after looking up the texture value via the UV coordinate:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">vec4 tex_color = texture(u_texture, uv);<br>FragColor = color * texture_rgba;<\/code><\/pre>\n\n\n\n<p><\/p>\n\n\n\n<p>The font atlas has white glyphs, which means this color multiplier produces fonts that look like we want. Colored sprites are typically rendered with <code>color<\/code> = [1,1,1,1] so that we don&#8217;t affect them, but you can also tint them by changing up the values.<\/p>\n\n\n\n<p>Rendering quads is also straightforward, but when you&#8217;re doing it all yourself, there is still a fair amount of stuff to keep track of. Every glyph in the font has a packed quad that tells us its size and its UV coordinates in the font atlas. We write the quad vertices into a <a href=\"https:\/\/en.wikipedia.org\/wiki\/Vertex_buffer_object\">VBO<\/a>, and rather than writing six vertices to get the two triangles per quad, we only write the four unique ones and use an <a href=\"https:\/\/opentk.net\/learn\/chapter1\/3-element-buffer-objects.html\">EBO<\/a> to define the quads by indexing into the vertex array (as is standard practice):<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full is-resized\"><img loading=\"lazy\" decoding=\"async\" width=\"625\" height=\"221\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2026\/01\/2026-01-01_10-06.png\" alt=\"A two-triangle quad showing four vertices in a VBO and six indices in an EBO. This uses 4 vertices * 9 f32s = 4*36 = 144 bytes in the VBO and 6*4 = 24 bytes in the EBO.\" class=\"wp-image-3137\" style=\"width:479px;height:auto\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2026\/01\/2026-01-01_10-06.png 625w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2026\/01\/2026-01-01_10-06-300x106.png 300w\" sizes=\"auto, (max-width: 625px) 100vw, 625px\" \/><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>Each frame, the backend fills up a vertex buffer and sends it to the VBO for rendering. This only needs to happen once, if the sprites and the font glyphs are all packed into the same texture and everything uses the same projection and view matrix.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Render Commands<\/h1>\n\n\n\n<p>The core game code does not directly fill the vertex buffer. Instead the core game code issues more simple <em>render commands<\/em>. These commands are simple, higher-level descriptions of what we want rendered, making the core game code fast and separating out the task of deciding how to render everything up to the backend.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full is-resized\"><img loading=\"lazy\" decoding=\"async\" width=\"615\" height=\"261\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2026\/01\/2026-01-01_12-28.png\" alt=\"A simple flowchart showing Core::Tick() followed by Backend::Render().\" class=\"wp-image-3138\" style=\"width:489px;height:auto\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2026\/01\/2026-01-01_12-28.png 615w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2026\/01\/2026-01-01_12-28-300x127.png 300w\" sizes=\"auto, (max-width: 615px) 100vw, 615px\" \/><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>For example, a render text command is:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"cpp\" class=\"language-cpp\">struct RenderCommandTextLine {\n    FontHandle font_handle;\n    glm::vec2 pos;       \/\/ Bottom-left of the first glyph\n    f32 letter_size;     \/\/ Multiplier off of default\n    f32 letter_spacing;  \/\/ Multiplier off of default\n    s8 text;             \/\/ Must last until rendered\n    glm::vec4 color;     \/\/ RGBA componentwise color multipliers\n};<\/code><\/pre>\n\n\n\n<p>It is up to the backend to convert that higher-level specification into the actual quad vertices, moving that non-trivial logic out of the core game loop and into its own location. The render command thus acts as an API between the game and the backend. Any new backend that we support in the future must be able to render these things.<\/p>\n\n\n\n<p>In the future, when I might have partially transparent objects, the backend can do more sophisticated things like render all opaque geometry first and then sort the transparent objects by depth and render them in a second pass. Something like that is very hard to do when the core logic is rendering as it goes.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Including Triangles<\/h1>\n\n\n\n<p>So far, we have a shader that can render both font glyphs and sprites. That&#8217;s nice, but I want Into Depth to have a low-poly, flat-color aesthetic. I wanted to add UI panels that underlay the text, and I want those to be tessellated:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"255\" height=\"236\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2026\/01\/2026-01-01_12-55.png\" alt=\"A text box backed by a panel consisting of a solid quad surounded by a low-poly border.\" class=\"wp-image-3140\"\/><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>A panel like that is comprised of a relatively small number of triangles when compared to a modern 3D character mesh (~40 vs. 1000&#8217;s). It should be cheap enough for us to directly write that geometry out every frame.<\/p>\n\n\n\n<p>Our font + sprite shader can actually be used to draw basic triangles, we just need to point all three vertices of the triangle to all-white uv coordinates so that we get the color we want. This is achieved by including a \\(1 \\times 1\\) <span id=\"su_tooltip_6a044c20984e3_button\" class=\"su-tooltip-button su-tooltip-button-outline-yes\" aria-describedby=\"su_tooltip_6a044c20984e3\" data-settings='{\"position\":\"top\",\"behavior\":\"hover\",\"hideDelay\":0}' tabindex=\"0\"><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-vivid-cyan-blue-color\">white sprite<\/mark><\/span><span style=\"display:none;z-index:100\" id=\"su_tooltip_6a044c20984e3\" class=\"su-tooltip\" role=\"tooltip\"><span class=\"su-tooltip-inner su-tooltip-shadow-no\" style=\"z-index:100;background:#222222;color:#FFFFFF;font-size:16px;border-radius:5px;text-align:left;max-width:300px;line-height:1.25\"><span class=\"su-tooltip-title\"><\/span><span class=\"su-tooltip-content su-u-trim\">This technique was mentioned in the Wookash Podcast with Omar Cornut of Dear ImGui.<\/span><\/span><span id=\"su_tooltip_6a044c20984e3_arrow\" class=\"su-tooltip-arrow\" style=\"z-index:100;background:#222222\" data-popper-arrow><\/span><\/span>.<\/p>\n\n\n\n<p>I thus define some additional render commands, such as <code>RenderCommandPanel<\/code>, that act as high-level descriptions of what should be rendered, and then have the backend construct the mesh live in the vertex buffer and send it over. Doing this does <em>not<\/em> use the quad-specific EBO, so for now I&#8217;m just using a <code>glDrawArrays<\/code> call instead of a <code>glDrawElements<\/code> call.<\/p>\n\n\n\n<p>I did make one adjustment to the shader &#8211; rather than switching out the textures depending on my use, I just have an array of textures that I can leave bound and have each vertex declare which one its UV coordinate applies to: <\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"887\" height=\"381\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2026\/01\/2026-01-01_13-09.png\" alt=\"The shader now includes `int texture_index` for each vertex and the uniforms now include an array of textures.\" class=\"wp-image-3143\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2026\/01\/2026-01-01_13-09.png 887w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2026\/01\/2026-01-01_13-09-300x129.png 300w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2026\/01\/2026-01-01_13-09-768x330.png 768w\" sizes=\"auto, (max-width: 887px) 100vw, 887px\" \/><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>I pack my fonts into a font atlas that is uploaded as the first texture, then have a series of sprite atlases.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Sprite Packing<\/h1>\n\n\n\n<p>I need to bake my font glyphs and my sprites into a series of textures that my shader can then use. There are plenty of tools that do sprite packing for you. I was already using <code>stb_truetype<\/code> and <code>stb_rect_pack<\/code> to create my font atlas, so I decided to write my own sprite packer on top of <code>stb_rect_pack<\/code>.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full is-resized\"><img loading=\"lazy\" decoding=\"async\" width=\"763\" height=\"350\" src=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2026\/01\/2026-01-01_13-23.png\" alt=\"A collection of rectangular images on the left are closely packed into a texture on the right.\" class=\"wp-image-3145\" style=\"width:619px;height:auto\" srcset=\"https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2026\/01\/2026-01-01_13-23.png 763w, https:\/\/timallanwheeler.com\/blog\/wp-content\/uploads\/2026\/01\/2026-01-01_13-23-300x138.png 300w\" sizes=\"auto, (max-width: 763px) 100vw, 763px\" \/><\/figure>\n<\/div>\n\n\n<p><\/p>\n\n\n\n<p>Conceptually, it is really simple. I give it a set of individual sprites, and it keeps trying to pack as many as possible into a new texture until everything is packed. The only little gotcha is that a \\(w \\times h\\) sprite is packed as a \\(w+2 \\times h+2\\) rectangle in order for it to be padded with a 1-pixel extended border to fight edge bleeding caused by inaccuracies in UV coordinates between mipmap levels.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Shader Code Organization<\/h1>\n\n\n\n<p>Beyond assets, shaders themselves tend to more complexity than you&#8217;d think because the code to use them ends up spread all over the codebase. The vertex and fragment shader definitions are often two separate <code>.glsl<\/code> files, which live in their own folder alongside the other shaders. The C++ side typically needs a vertex struct defined somewhere, and then we need both CPU-side vertex buffers and references to the GPU-side shader program IDs, VAO, VBO, and EBOs. Whenever we use a shader, we need to activate it, which involves more code that has to use these references and set various shader uniforms.<\/p>\n\n\n\n<p>I&#8217;ve adopted a few enhancements to reduce the degree of code spread.<\/p>\n\n\n\n<p>The first thing I did was unify the vertex and fragment shaders into a single <code>.glsl<\/code> file. Since we just read in the file and upload it to the GPU before calling <code>glCompileShader<\/code>, we can actually process out the necessary portions ourselves. Writing both shaders in the same file helps keep their interdependencies consistent.<\/p>\n\n\n\n<div data-wp-context=\"{ &quot;autoclose&quot;: false, &quot;accordionItems&quot;: [] }\" data-wp-interactive=\"core\/accordion\" role=\"group\" class=\"wp-block-accordion is-layout-flow wp-block-accordion-is-layout-flow\">\n<div data-wp-class--is-open=\"state.isOpen\" data-wp-context=\"{ &quot;id&quot;: &quot;accordion-item-1&quot;, &quot;openByDefault&quot;: false }\" data-wp-init=\"callbacks.initAccordionItems\" data-wp-on-window--hashchange=\"callbacks.hashChange\" class=\"wp-block-accordion-item is-layout-flow wp-block-accordion-item-is-layout-flow\">\n<h3 class=\"wp-block-accordion-heading\"><button aria-expanded=\"false\" aria-controls=\"accordion-item-1-panel\" data-wp-bind--aria-expanded=\"state.isOpen\" data-wp-on--click=\"actions.toggle\" data-wp-on--keydown=\"actions.handleKeyDown\" id=\"accordion-item-1\" class=\"wp-block-accordion-heading__toggle\"><span class=\"wp-block-accordion-heading__toggle-title\">font.glsl &#8211; click to expand<\/span><span class=\"wp-block-accordion-heading__toggle-icon\" aria-hidden=\"true\">+<\/span><\/button><\/h3>\n\n\n\n<div inert aria-labelledby=\"accordion-item-1\" data-wp-bind--inert=\"!state.isOpen\" id=\"accordion-item-1-panel\" role=\"region\" class=\"wp-block-accordion-panel is-layout-flow wp-block-accordion-panel-is-layout-flow\">\n<pre class=\"wp-block-code\"><code class=\"\">#version 330 core<br><br>#ifdef VERTEX_SHADER<br><br>layout (location = 0) in vec3 aPosWorld;<br>layout (location = 1) in vec4 aVertexColor; \/\/ rgba<br>layout (location = 2) in vec2 aTexCoords;<br><br>out vec4 vertex_color;<br>out vec2 tex_coords;<br><br>uniform mat4 model;<br>uniform mat4 view;<br>uniform mat4 projection;<br><br>void main()<br>{<br>   gl_Position = projection * view * model * vec4(aPosWorld, 1.0);<br>   vertex_color = aVertexColor;<br>   tex_coords = aTexCoords;<br>}<br><br>#endif<br><br>\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/<br><br>#ifdef FRAGMENT_SHADER<br><br>in vec4 vertex_color;<br>in vec2 tex_coords;<br><br>out vec4 FragColor;<br><br>uniform sampler2D atlas;<br><br>void main()<br>{<br>    float atlas_value = texture(atlas, tex_coords).r;<br>    FragColor = vec4(vertex_color.rgb, vertex_color.a * atlas_value);<br>}<br><br>#endif<\/code><\/pre>\n<\/div>\n<\/div>\n<\/div>\n\n\n\n<p>This was nice, but I actually ended up further simplifying by having a single <code>.h<\/code> file that defines the shader data relevant to the CPU side. The vertex and fragment shaders are still defined in one place, but as explicit strings, and we additionally define a <em>vertex layout:<\/em><\/p>\n\n\n\n<div data-wp-context=\"{ &quot;autoclose&quot;: false, &quot;accordionItems&quot;: [] }\" data-wp-interactive=\"core\/accordion\" role=\"group\" class=\"wp-block-accordion is-layout-flow wp-block-accordion-is-layout-flow\">\n<div data-wp-class--is-open=\"state.isOpen\" data-wp-context=\"{ &quot;id&quot;: &quot;accordion-item-2&quot;, &quot;openByDefault&quot;: false }\" data-wp-init=\"callbacks.initAccordionItems\" data-wp-on-window--hashchange=\"callbacks.hashChange\" class=\"wp-block-accordion-item is-layout-flow wp-block-accordion-item-is-layout-flow\">\n<h3 class=\"wp-block-accordion-heading\"><button aria-expanded=\"false\" aria-controls=\"accordion-item-2-panel\" data-wp-bind--aria-expanded=\"state.isOpen\" data-wp-on--click=\"actions.toggle\" data-wp-on--keydown=\"actions.handleKeyDown\" id=\"accordion-item-2\" class=\"wp-block-accordion-heading__toggle\"><span class=\"wp-block-accordion-heading__toggle-title\">unlit_triangle_shader.h &#8211; click to expand<\/span><span class=\"wp-block-accordion-heading__toggle-icon\" aria-hidden=\"true\">+<\/span><\/button><\/h3>\n\n\n\n<div inert aria-labelledby=\"accordion-item-2\" data-wp-bind--inert=\"!state.isOpen\" id=\"accordion-item-2-panel\" role=\"region\" class=\"wp-block-accordion-panel is-layout-flow wp-block-accordion-panel-is-layout-flow\">\n<p><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"cpp\" class=\"language-cpp\">#pragma once\n\n#include \"util\/strings.h\"\n#include \"util\/vertex_layout.h\"\n\nconstexpr int kNumUnlitTriangleShaderTextures = 8;\n\n\/\/ The vertex data structure (CPU-side)\nstruct UnlitTriangleVertex {\n    glm::vec3 pos;\n    glm::vec4 rgba;\n    glm::vec2 uv;\n    int texture_index;\n};\n\n\/\/ The shader definition\nstruct UnlitTriangleShaderDefinition {\n    static constexpr const char* kName = \"UnlitTriangle\";\n\n    static constexpr VertexElement kVertexElements[4] = {\n        {ShaderDataType::Vec3F, \"aPosWorld\", false, offsetof(UnlitTriangleVertex, pos          )},\n        {ShaderDataType::Vec4F, \"aRGBA\",     false, offsetof(UnlitTriangleVertex, rgba         )},\n        {ShaderDataType::Vec2F, \"aUV\",       false, offsetof(UnlitTriangleVertex, uv           )},\n        {ShaderDataType::I32,   \"aTexIndex\", false, offsetof(UnlitTriangleVertex, texture_index)},\n    };\n\n    static constexpr VertexLayout kVertexLayout = {\n        kVertexElements,\n        sizeof(kVertexElements) \/ sizeof(kVertexElements[0]),\n        sizeof(UnlitTriangleVertex)\n    };\n\n    static constexpr const char* kVertexShaderSource = R\"(\n        #version 330 core\n\n        layout (location = 0) in vec3 aPosWorld;\n        layout (location = 1) in vec4 aRGBA;\n        layout (location = 2) in vec2 aUV;\n        layout (location = 3) in int  aTexIndex;\n\n             out vec4 rgba;\n             out vec2 uv;\n        flat out int  texture_index; \/\/ 'flat' means do not interpolate\n\n        uniform mat4 proj_view; \/\/ proj * view\n\n        void main()\n        {\n            gl_Position = proj_view * vec4(aPosWorld, 1.0);\n            rgba = aRGBA;\n            uv = aUV;\n            texture_index = aTexIndex;\n        }\n    )\";\n\n    static constexpr const char* kFragmentShaderSource = R\"(\n        #version 330 core\n\n             in vec4 rgba;\n             in vec2 uv;\n        flat in int  texture_index;\n\n        out vec4 FragColor;\n\n        uniform sampler2D textures[8];\n\n        void main()\n        {\n            vec4 texture_rgba;\n            \n            \/\/ The 'switch trick' is faster\/safer on some drivers.\n            switch(texture_index) {\n                case 0:  texture_rgba = texture(textures[0], uv); break;\n                case 1:  texture_rgba = texture(textures[1], uv); break;\n                case 2:  texture_rgba = texture(textures[2], uv); break;\n                case 3:  texture_rgba = texture(textures[3], uv); break;\n                case 4:  texture_rgba = texture(textures[4], uv); break;\n                case 5:  texture_rgba = texture(textures[5], uv); break;\n                case 6:  texture_rgba = texture(textures[6], uv); break;\n                case 7:  texture_rgba = texture(textures[7], uv); break;\n                default: texture_rgba = vec4(1.0,0.0,1.0,1.0); break; \/\/ magenta\n            }\n            \n            FragColor = rgba * texture_rgba;\n        }\n    )\";\n};<\/code><\/pre>\n<\/div>\n<\/div>\n<\/div>\n\n\n\n<p>The vertex layout is just a programmatic representation of the inputs to the Vertex shader. This avoids having to have VBO-creation code that lives elsewhere depend on this shader. We can go from code like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"cpp\" class=\"language-cpp\">\/\/ vertex positions\nu8 attribute_index = 0;\nglEnableVertexAttribArray(attribute_index);\nglVertexAttribPointer(attribute_index++, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),\n                      (void*)offsetof(Vertex, position));\n\/\/ vertex normals\nglEnableVertexAttribArray(attribute_index);\nglVertexAttribPointer(attribute_index++, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),\n                      (void*)offsetof(Vertex, normal));\n\/\/ uv coordinates\nglEnableVertexAttribArray(attribute_index);\nglVertexAttribPointer(attribute_index++, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex),\n                      (void*)offsetof(Vertex, uv));\n\/\/ bone ids (max 4)\nglEnableVertexAttribArray(attribute_index);\nglVertexAttribIPointer(attribute_index++, 4, GL_INT, sizeof(Vertex),\n                       (void*)offsetof(Vertex, bone_ids));\n\/\/ bone weights (max 4)\nglEnableVertexAttribArray(attribute_index);\nglVertexAttribPointer(attribute_index++, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex),\n                      (void*)offsetof(Vertex, bone_weights));<\/code><\/pre>\n\n\n\n<p>to just running:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"cpp\" class=\"language-cpp\">for (u32 i = 0; i &lt; vertex_layout.n_elements; i++) {\n    SetUpVertexAttrib(i, vertex_layout.elements[i], vertex_layout.stride);\n}<\/code><\/pre>\n\n\n\n<p>where <code>SetUpVertexAttrib<\/code> looks at the element type and executes the appropriate logic.<\/p>\n\n\n\n<div data-wp-context=\"{ &quot;autoclose&quot;: false, &quot;accordionItems&quot;: [] }\" data-wp-interactive=\"core\/accordion\" role=\"group\" class=\"wp-block-accordion is-layout-flow wp-block-accordion-is-layout-flow\">\n<div data-wp-class--is-open=\"state.isOpen\" data-wp-context=\"{ &quot;id&quot;: &quot;accordion-item-3&quot;, &quot;openByDefault&quot;: false }\" data-wp-init=\"callbacks.initAccordionItems\" data-wp-on-window--hashchange=\"callbacks.hashChange\" class=\"wp-block-accordion-item is-layout-flow wp-block-accordion-item-is-layout-flow\">\n<h3 class=\"wp-block-accordion-heading\"><button aria-expanded=\"false\" aria-controls=\"accordion-item-3-panel\" data-wp-bind--aria-expanded=\"state.isOpen\" data-wp-on--click=\"actions.toggle\" data-wp-on--keydown=\"actions.handleKeyDown\" id=\"accordion-item-3\" class=\"wp-block-accordion-heading__toggle\"><span class=\"wp-block-accordion-heading__toggle-title\">SetUpVertexAttrib &#8211; click to expand<\/span><span class=\"wp-block-accordion-heading__toggle-icon\" aria-hidden=\"true\">+<\/span><\/button><\/h3>\n\n\n\n<div inert aria-labelledby=\"accordion-item-3\" data-wp-bind--inert=\"!state.isOpen\" id=\"accordion-item-3-panel\" role=\"region\" class=\"wp-block-accordion-panel is-layout-flow wp-block-accordion-panel-is-layout-flow\">\n<pre class=\"wp-block-code\"><code lang=\"cpp\" class=\"language-cpp\">void SetUpVertexAttrib(const u8 attribute_index, const VertexElement&amp; element, const size_t stride) {\n    glEnableVertexAttribArray(attribute_index);\n    \n    switch (element.type) {\n    case (ShaderDataType::F32):\n        glVertexAttribPointer(attribute_index, 1, GL_FLOAT, GL_FALSE, stride, (void*)element.offset);\n        break;\n\n    case (ShaderDataType::Vec2F):\n        glVertexAttribPointer(attribute_index, 2, GL_FLOAT, GL_FALSE, stride, (void*)element.offset);\n        break;\n\n    case (ShaderDataType::Vec3F):\n        glVertexAttribPointer(attribute_index, 3, GL_FLOAT, GL_FALSE, stride, (void*)element.offset);\n        break;\n\n    case (ShaderDataType::Vec4F):\n        glVertexAttribPointer(attribute_index, 4, GL_FLOAT, GL_FALSE, stride, (void*)element.offset);\n        break;\n\n    case (ShaderDataType::I32):\n        glVertexAttribIPointer(attribute_index, 1, GL_INT, stride, (void*)element.offset);\n        break;\n    }\n}<\/code><\/pre>\n<\/div>\n<\/div>\n<\/div>\n\n\n\n<p>My OpenGL backend has the general <code>SetUpVertexAttrib<\/code>, but it doesn&#8217;t have to change if we change the shader. The backend additionally has structs for the GPU object references, the shader uniforms, and their locations:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"cpp\" class=\"language-cpp\">struct UnlitTriangleShaderRefs {\n    GLuint shader_program_id;\n\n    GLuint vao; \/\/ vertex array object\n    GLuint vbo; \/\/ vertex buffer object\n    size_t vbo_tri_capacity;  \/\/ Number of triangles we have space for in the VBO.\n\n    GLuint ebo_quads; \/\/ prepopulated with quad indices\n};\n\nstruct UnlitTriangleShaderUniforms {\n    GLint uniformloc_proj_view; \/\/ proj * view from glGetUniformLocation\n    GLint uniformloc_textures;\n\n    GLuint texture_ids[kNumUnlitTriangleShaderTextures];\n};<\/code><\/pre>\n\n\n\n<p>This organization scheme has, so far, been much nicer to work with.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Conclusion<\/h1>\n\n\n\n<p>When you are writing everything yourself, there is a lot to keep track of. Most of my previous attempts at coding a game &#8220;from scratch&#8221; struggled under mounting complexity. Unifying font, sprite, and basic unlit geometry rendering under one shader helps alleviate that complexity. Even more so, having good separations of concern between the core game logic and the backend lets the game logic just have simple references to assets and focus on the game, and the backend figure out how to fill vertex buffers and ship them to the GPU.<\/p>\n\n\n\n<p>Happy New Year!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Into Depth is the name of a little game project I have been working on for over a year now. Its roots go back even further, to the 2D platformer I had started working on two years ago. The platformer ended up teaching me a lot about OpenGL, which I have continued to use in [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[13],"tags":[14,10],"class_list":["post-3128","post","type-post","status-publish","format-standard","hentry","category-sidescroller","tag-into_depth","tag-sidescroller"],"_links":{"self":[{"href":"https:\/\/timallanwheeler.com\/blog\/wp-json\/wp\/v2\/posts\/3128","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/timallanwheeler.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/timallanwheeler.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/timallanwheeler.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/timallanwheeler.com\/blog\/wp-json\/wp\/v2\/comments?post=3128"}],"version-history":[{"count":19,"href":"https:\/\/timallanwheeler.com\/blog\/wp-json\/wp\/v2\/posts\/3128\/revisions"}],"predecessor-version":[{"id":3161,"href":"https:\/\/timallanwheeler.com\/blog\/wp-json\/wp\/v2\/posts\/3128\/revisions\/3161"}],"wp:attachment":[{"href":"https:\/\/timallanwheeler.com\/blog\/wp-json\/wp\/v2\/media?parent=3128"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/timallanwheeler.com\/blog\/wp-json\/wp\/v2\/categories?post=3128"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/timallanwheeler.com\/blog\/wp-json\/wp\/v2\/tags?post=3128"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}