{"id":179,"date":"2025-10-20T07:09:41","date_gmt":"2025-10-20T07:09:41","guid":{"rendered":"https:\/\/leonwoud.com\/devblog\/?p=179"},"modified":"2025-10-21T12:27:12","modified_gmt":"2025-10-21T12:27:12","slug":"a-conscious-decoupling","status":"publish","type":"post","link":"https:\/\/leonwoud.com\/devblog\/a-conscious-decoupling\/","title":{"rendered":"A conscious decoupling"},"content":{"rendered":"\n<figure class=\"wp-block-image size-large\"><img fetchpriority=\"high\" decoding=\"async\" width=\"1024\" height=\"543\" src=\"https:\/\/leonwoud.com\/devblog\/wp-content\/uploads\/2025\/10\/scenegraph-1024x543.png\" alt=\"\" class=\"wp-image-183\" srcset=\"https:\/\/leonwoud.com\/devblog\/wp-content\/uploads\/2025\/10\/scenegraph-1024x543.png 1024w, https:\/\/leonwoud.com\/devblog\/wp-content\/uploads\/2025\/10\/scenegraph-300x159.png 300w, https:\/\/leonwoud.com\/devblog\/wp-content\/uploads\/2025\/10\/scenegraph-768x408.png 768w, https:\/\/leonwoud.com\/devblog\/wp-content\/uploads\/2025\/10\/scenegraph-250x133.png 250w, https:\/\/leonwoud.com\/devblog\/wp-content\/uploads\/2025\/10\/scenegraph-550x292.png 550w, https:\/\/leonwoud.com\/devblog\/wp-content\/uploads\/2025\/10\/scenegraph-800x425.png 800w, https:\/\/leonwoud.com\/devblog\/wp-content\/uploads\/2025\/10\/scenegraph-339x180.png 339w, https:\/\/leonwoud.com\/devblog\/wp-content\/uploads\/2025\/10\/scenegraph-565x300.png 565w, https:\/\/leonwoud.com\/devblog\/wp-content\/uploads\/2025\/10\/scenegraph-942x500.png 942w, https:\/\/leonwoud.com\/devblog\/wp-content\/uploads\/2025\/10\/scenegraph.png 1466w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>At the end of my last blog post StaticRectangle was rendering &#8216;the illustrious spinning cube&#8217;.<\/p>\n\n\n\n<p>We had a bunch new components, like the camera controller, input \/ resource manager as well as a top level <code>Application<\/code> that took ownership of some processes away from the <code>Renderer<\/code>.<\/p>\n\n\n\n<p>The next thing to tackle was splitting up the <code>Renderer<\/code> further, it was still doing more than just rendering. In fact it was the render function of the renderer that was animating the cube! certainly overstepping its responsibilities as a renderer.<\/p>\n\n\n\n<p>If I wanted to do something as scandalous as add another cube I&#8217;d have to do a lot of copy\/paste work, manually creating and allocating new GPU resources and branching logic in places that didn&#8217;t make sense. That isn&#8217;t what we&#8217;d refer to as a&#8217;scalable&#8217; system.<\/p>\n\n\n\n<p>It&#8217;s a logical place start things of course, because it&#8217;s simple. Almost all examples you&#8217;ll come across are like this because of that fact.<\/p>\n\n\n\n<p>Adding new objects to render means we need to take a step back and ask ourselves, what exactly are we rendering? <\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Scene Graph<\/h2>\n\n\n\n<p>What we are rendering, for all intents and purposes, is a scene. In the previous example the scene was a single spinning cube and it was more or less hardcoded into the renderer, but it was a scene nonetheless.<\/p>\n\n\n\n<p>Now what if wanted to take that same scene and render it from multiple angles? (like the typical front, side, top and perspective views of a 3d application) or run a physics simulation or game play logic?<\/p>\n\n\n\n<p>We need to decouple the scene from the renderer completely. Keep all the render specific resources (buffers, layouts, pipelines etc.) we need to render the scene with the renderer, and create a seperate system that tracks the objects and their types, properties and relationships to one another. We can then pass that same scene to the various components to update it before we render it.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>while app.is_running:\n    animation.update(scene, time)       &lt;- update animated scene elements\n    physics.simulate(scene, time.delta) &lt;- perform physics simulation\n    renderer.render(scene)              &lt;- render the scene<\/code><\/pre>\n\n\n\n<p>So why a graph?<\/p>\n\n\n\n<p>We use a graph (technically speaking a &#8216;directed acyclic graph&#8217; or DAG) to describe a scenes hierarchy. It allows for a parent\/child relationship between objects where transformations of a parent are inherited by the child, but the child has its own local transformations.<\/p>\n\n\n\n<p>Changes to a child transform cannot have any impact on the parent, that would create a cycle.<\/p>\n\n\n\n<p>StaticRectangle now has a simple <code>SceneGraph<\/code>, for keeping track of transforms and shapes and the relationships between them. <\/p>\n\n\n\n<p>Here is the verbatim code that the <code>Application<\/code> is running to create the <code>SceneGraph<\/code> in this posts example.<sup data-fn=\"baef9a3c-133f-45b8-a605-f25d99b6553c\" class=\"fn\"><a id=\"baef9a3c-133f-45b8-a605-f25d99b6553c-link\" href=\"#baef9a3c-133f-45b8-a605-f25d99b6553c\">1<\/a><\/sup><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>function createDefaultScene(): SceneGraph {\n\n    const scene = new SceneGraph();\n    const root = scene.createTransform();\n    const geometry = ExampleCube();\n    const shape = scene.createShape(\"cube\", geometry);\n    shape.assignMaterial(defaultMaterial);\n    shape.setParent(root);\n    let previousTransform = root;\n    for (let i = 1; i &lt; 10; i++) {\n        const child = scene.createTransform();\n        child.setTranslate(&#91;0, 1.0, 0]);\n        child.setParent(previousTransform);\n        const childShape = scene.createShape(`cube${i}`, geometry);\n        childShape.assignMaterial(defaultMaterial);\n        childShape.setParent(child);\n        previousTransform = child;\n    }\n    root.setScale(&#91;10, 10, 10]);\n    root.setDirty();\n\n    return scene;\n}<\/code><\/pre>\n\n\n\n<p>At the moment, the <code>SceneGraph<\/code> is barebones. It only has the concept of transforms, shapes and material assignment.<\/p>\n\n\n\n<p>The transform stack is also decoupled (sensing a theme here?) from the shapes despite being part of the same <code>SceneGraph<\/code>. Every new transform and shape is assigned a unique ID (UUID) and when &#8216;parenting&#8217; a shape to a transform, you&#8217;re really just assigning the transform&#8217;s ID to be looked up at render time.<\/p>\n\n\n\n<p>It still yet to be seen if this design works for more complex scenes. I did it this way because while &#8216;physical&#8217; hierarchical relationships are necessary for transforms when calculating matrices (more on that in a moment) this information isn&#8217;t necessary for shapes. It also makes it potentially simpler to optimise where the same shape (geometry\/material combinations) are shared across multiple transforms, useful not just for reducing the memory of the <code>SceneGraph<\/code> but also when rendering the scene (i.e instancing). <\/p>\n\n\n\n<p>It&#8217;s also common for transforms to have no shape (pivots, groups etc.) so when gathering &#8216;renderables&#8217; we can iterate the shapes in the scene and then pair them with the assigned transform via ID lookup, instead of having to flatten the transform stack to find the shapes we want to render.<\/p>\n\n\n\n<p>It makes logical sense<em>\u2014<\/em>at least to me<em>\u2014<\/em>but I&#8217;m unclear at this stage if it is a good idea. I&#8217;m not sure what kind of performance overhead these lookups have vs just iterating the stack. Something I can profile in the future.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Transforms<\/h2>\n\n\n\n<p>The new <code>Transform<\/code> object keeps track of its local position and whether it has a parent, or children. As mentioned above, this is important for calculating where an object.<\/p>\n\n\n\n<p>I will probably do a full blog post on transformations because it&#8217;s quite interesting stuff. But the high level gist is that when we want to know where an object is, we multiply its local matrix by its parents world space matrix, that&#8217;s it. Magic.<\/p>\n\n\n\n<p>The <code>Transform<\/code> caches its own world matrix and it&#8217;s using &#8216;dirty&#8217; flag propagation to avoid recalculating them unnecessarily. A transform is &#8216;dirty&#8217; when its local transformations change, so a call to <code>getWorldMatrix<\/code> when will recalculate it. Doing things this way means we don&#8217;t need to walk the hierarchy multiplying all ancestors local matrices to get a world space position<em>\u2014<\/em>that wouldn&#8217;t be very efficient.<\/p>\n\n\n\n<p>A long with the <code>Transform<\/code> comes the need to handle the GPU resources associated with them. Initially I had a single buffer that held the spinning cube matrix. It was tempting to just turn this single buffer into an Array of buffers, but there are other, better options.<\/p>\n\n\n\n<p>Instead I have added a <code>TransformBufferPool<\/code>, this preallocates an array big enough to store n-number of matrices<em>\u2014<\/em>by default it&#8217;s 1000<em>\u2014<\/em>and stores the bind groups for each, the bind group is how we tell the shader what offset the specific object being rendered is within this larger array of matrices.<\/p>\n\n\n\n<p>Doing it this way is certainly more efficient when updating all matrices in one go, though it doesn&#8217;t support partial updating. In my mind we&#8217;d only ever by drawing a small fraction of a larger scene so I don&#8217;t expect this buffer to be too large.<\/p>\n\n\n\n<p>If it wasn&#8217;t clear, a nice thing about this approach is that the bind groups aren&#8217;t tied to specific transforms. It&#8217;s just a pool to be used by n-number of transforms that may come and go during runtime.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Geometry<\/h2>\n\n\n\n<p>Decoupling the scene from the renderer meant decoupling the render resources from objects in the scene. Until now, I had a <code>mesh<\/code> interface that had GPU resources and functions that created\/converted them from geometry descriptions, all intertwined.<\/p>\n\n\n\n<p>Now StaticRectangle has <code>Geometry<\/code> which provides vertex positions, optional UV\/normal\/colour and indicies.<\/p>\n\n\n\n<p>There is also now a <code>Shape<\/code> which is created by the <code>SceneGraph<\/code>, the shape stores the geometry and the material assigned to it.<\/p>\n\n\n\n<p>Then we have the <code>Mesh<\/code><sup data-fn=\"d99ccba8-cf83-40f8-b29f-800f2e0c0fd3\" class=\"fn\"><a href=\"#d99ccba8-cf83-40f8-b29f-800f2e0c0fd3\" id=\"d99ccba8-cf83-40f8-b29f-800f2e0c0fd3-link\">2<\/a><\/sup>, which is all the GPU resources that the renderer needs to render it. You create a <code>Mesh<\/code> from the the geometry data, which creates various buffers, <\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Materials<\/h2>\n\n\n\n<p>Similarly to decoupling geometry, we need a way to assign a material to an object in the scene without carrying all the rendering specific resources.<\/p>\n\n\n\n<p>So now, we have a <code>Material<\/code> which we assign to a <code>Shape<\/code> in the <code>SceneGraph<\/code>, the <code>Material<\/code> has the name of the <code>Shader<\/code> we&#8217;re using, which is used when creating the <code>MaterialResources<\/code>. Specifically the layouts the shader is expecting given the materials type (e.g an unlit material may expect just an albedo map, where as a PBR material may expect albedo, normal, roughness and metallic maps).<\/p>\n\n\n\n<p>At the moment, the material system is mostly scaffold for future work, my current &#8216;default material&#8217; isn&#8217;t doing anything, the shader isn&#8217;t looking at those properties (though they do exist even now), it&#8217;s simply outputting the vertex colours defined by the example cube <code>Geometry<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Resource Manager<\/h2>\n\n\n\n<p>The <code>ResourceManager<\/code> has been updated, originally I had intended it to beuse to load\/supply external resources (shaders, textures etc.) but it is now serving GPU resources (layouts, pipelines,  for meshes and materials etc.) In retrospect, this makes a lot of sense. It&#8217;s quite satisfying when things settle into place, and pieces fit together.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">StaticRectangle 02<\/h2>\n\n\n\n<p>All of that work, and what do we have to show for it? more cubes! Hopefully by now you are getting the idea that while visual progress may be slow\u2014it actually took me 3 days to go from having a single spinning cube to having a single spinning cube with more infrastructure AND I spent about 8 hours fixing bugs I had introduced adding that infrastructure\u2014these updates build on each other, fairly soon I&#8217;ll be making large visual improvements without the huge infrastructure changes (I hope).<\/p>\n\n\n\n\n\n<iframe src=\"https:\/\/staticrectangle-02.onrender.com\" width=\"800\" height=\"800\"><\/iframe>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Mouse look with Left\/Right Mouse Button<sup data-fn=\"653f6830-fad6-4824-9729-73e9ae2f2f9f\" class=\"fn\"><a id=\"653f6830-fad6-4824-9729-73e9ae2f2f9f-link\" href=\"#653f6830-fad6-4824-9729-73e9ae2f2f9f\">3<\/a><\/sup><\/li>\n\n\n\n<li>W, A, S, D to move<\/li>\n\n\n\n<li>P to pause<\/li>\n<\/ul>\n\n\n<ol class=\"wp-block-footnotes has-small-font-size\"><li id=\"baef9a3c-133f-45b8-a605-f25d99b6553c\">I&#8217;m not expecting scenes to be created by code this way, at least not generally. The idea would be to serialize \/ deserialize the scene graph into a &#8216;scene description&#8217; that can be loaded just like any other resource.   <a href=\"#baef9a3c-133f-45b8-a605-f25d99b6553c-link\" aria-label=\"Jump to footnote reference 1\">\u21a9\ufe0e<\/a><\/li><li id=\"d99ccba8-cf83-40f8-b29f-800f2e0c0fd3\">This is not a great name, I just needed a different name than Geometry or Shape. I could (maybe should) have just called it GeometryResources like I did with Material and MaterialResources, I can always rename it though. <a href=\"#d99ccba8-cf83-40f8-b29f-800f2e0c0fd3-link\" aria-label=\"Jump to footnote reference 2\">\u21a9\ufe0e<\/a><\/li><li id=\"653f6830-fad6-4824-9729-73e9ae2f2f9f\">To highlight the importance of QA, in the previous post I had the Ctrl key to enable mouse look, but on some browsers Ctrl+W (mouse look and move left) closed the browser&#8230; <a href=\"#653f6830-fad6-4824-9729-73e9ae2f2f9f-link\" aria-label=\"Jump to footnote reference 3\">\u21a9\ufe0e<\/a><\/li><\/ol>\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>At the end of my last blog post StaticRectangle was rendering &#8216;the illustrious spinning cube&#8217;. We had a bunch new components, like the camera controller, input \/ resource manager as well as a top level Application that took ownership of some processes away from the Renderer. The next thing to [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"bgseo_title":"","bgseo_description":"","bgseo_robots_index":"index","bgseo_robots_follow":"follow","footnotes":"[{\"content\":\"I'm not expecting scenes to be created by code this way, at least not generally. The idea would be to serialize \/ deserialize the scene graph into a 'scene description' that can be loaded just like any other resource.  \",\"id\":\"baef9a3c-133f-45b8-a605-f25d99b6553c\"},{\"content\":\"This is not a great name, I just needed a different name than Geometry or Shape. I could (maybe should) have just called it GeometryResources like I did with Material and MaterialResources, I can always rename it though.\",\"id\":\"d99ccba8-cf83-40f8-b29f-800f2e0c0fd3\"},{\"content\":\"To highlight the importance of QA, in the previous post I had the Ctrl key to enable mouse look, but on some browsers Ctrl+W (mouse look and move left) closed the browser...\",\"id\":\"653f6830-fad6-4824-9729-73e9ae2f2f9f\"}]"},"categories":[9,23],"tags":[12,22],"class_list":["post-179","post","type-post","status-publish","format-standard","hentry","category-game-dev","category-webgpu","tag-renderer","tag-webgpu"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/leonwoud.com\/devblog\/wp-json\/wp\/v2\/posts\/179","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/leonwoud.com\/devblog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/leonwoud.com\/devblog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/leonwoud.com\/devblog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/leonwoud.com\/devblog\/wp-json\/wp\/v2\/comments?post=179"}],"version-history":[{"count":4,"href":"https:\/\/leonwoud.com\/devblog\/wp-json\/wp\/v2\/posts\/179\/revisions"}],"predecessor-version":[{"id":185,"href":"https:\/\/leonwoud.com\/devblog\/wp-json\/wp\/v2\/posts\/179\/revisions\/185"}],"wp:attachment":[{"href":"https:\/\/leonwoud.com\/devblog\/wp-json\/wp\/v2\/media?parent=179"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/leonwoud.com\/devblog\/wp-json\/wp\/v2\/categories?post=179"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/leonwoud.com\/devblog\/wp-json\/wp\/v2\/tags?post=179"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}