From 27ff505b6daaf5b0ec5f6af422f727a032f83c6b Mon Sep 17 00:00:00 2001
From: 3gg <3gg@shellblade.net>
Date: Wed, 4 Jan 2023 15:46:22 -0800
Subject: Move ShaderProgram from Material to Mesh in preparation for shader
 permutations.

---
 gfx/include/gfx/scene/README.md  |   8 +-
 gfx/include/gfx/scene/material.h |   7 +-
 gfx/include/gfx/scene/mesh.h     |   6 +-
 gfx/include/gfx/util/scene.h     |   5 +-
 gfx/shaders/cook_torrance.frag   |   1 +
 gfx/shaders/cook_torrance.vert   |   1 +
 gfx/src/renderer/renderer.c      | 103 ++++++-------
 gfx/src/scene/material.c         |   6 +-
 gfx/src/scene/material_impl.h    |   5 +-
 gfx/src/scene/mesh.c             |   2 +
 gfx/src/scene/mesh_impl.h        |   1 +
 gfx/src/util/scene.c             | 310 ++++++++++++++++++++-------------------
 gfx/src/util/skyquad.c           |  23 +--
 gltfview/src/game.c              | 104 ++++++-------
 14 files changed, 303 insertions(+), 279 deletions(-)

diff --git a/gfx/include/gfx/scene/README.md b/gfx/include/gfx/scene/README.md
index 916596b..1910abe 100644
--- a/gfx/include/gfx/scene/README.md
+++ b/gfx/include/gfx/scene/README.md
@@ -21,7 +21,7 @@ former, the API could create the illusion that the hierarchy can be a DAG.
 The strict tree hierarchy should not be that restrictive in practice. Even the
 glTF 2.0 spec [enforces this](https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#nodes-and-hierarchy):
 
-*For Version 2.0 conformance, the glTF node hierarchy is not a directed acyclic
-graph (DAG) or scene graph, but a disjoint union of strict trees. That is, no
-node may be a direct descendant of more than one node. This restriction is meant
-to simplify implementation and facilitate conformance.*
+> *For Version 2.0 conformance, the glTF node hierarchy is not a directed
+> acyclic graph (DAG) or scene graph, but a disjoint union of strict trees. That
+> is, no node may be a direct descendant of more than one node. This restriction
+> is meant to simplify implementation and facilitate conformance.*
diff --git a/gfx/include/gfx/scene/material.h b/gfx/include/gfx/scene/material.h
index 6c22515..07e31d4 100644
--- a/gfx/include/gfx/scene/material.h
+++ b/gfx/include/gfx/scene/material.h
@@ -11,13 +11,8 @@ typedef struct Material Material;
 /// variables. Two materials can share the same shader, but shader parameters
 /// generally give two materials a different appearance.
 typedef struct MaterialDesc {
-  ShaderProgram* shader; // TODO: Move to Mesh? Cannot fully determine shader
-                         // permutation without geometry. Or move the creation
-                         // of permutations to the renderer? A multi-pass
-                         // renderer will juggle multiple shader programs
-                         // anyway.
   ShaderUniform uniforms[GFX_MAX_UNIFORMS_PER_MATERIAL];
-  int num_uniforms;
+  int           num_uniforms;
 } MaterialDesc;
 
 /// Create a material.
diff --git a/gfx/include/gfx/scene/mesh.h b/gfx/include/gfx/scene/mesh.h
index f16572a..0d3b4d4 100644
--- a/gfx/include/gfx/scene/mesh.h
+++ b/gfx/include/gfx/scene/mesh.h
@@ -1,7 +1,8 @@
 #pragma once
 
-typedef struct Geometry Geometry;
-typedef struct Material Material;
+typedef struct Geometry      Geometry;
+typedef struct Material      Material;
+typedef struct ShaderProgram ShaderProgram;
 
 typedef struct Mesh Mesh;
 
@@ -9,6 +10,7 @@ typedef struct Mesh Mesh;
 typedef struct MeshDesc {
   const Geometry* geometry;
   const Material* material;
+  ShaderProgram*  shader;
 } MeshDesc;
 
 /// Create a mesh.
diff --git a/gfx/include/gfx/util/scene.h b/gfx/include/gfx/util/scene.h
index 98267aa..7340d1d 100644
--- a/gfx/include/gfx/util/scene.h
+++ b/gfx/include/gfx/util/scene.h
@@ -24,7 +24,10 @@ typedef struct LoadSceneCmd {
 /// Load a scene.
 ///
 /// |root_node| is the node under which scene elements are loaded.
-/// |shader| is the shader program assigned to the loaded scene objects.
+///
+/// |shader| is an optional shader program assigned to the loaded scene objects.
+/// If no shader is given, a Cook-Torrance shader based on the object's
+/// characteristics (presence of normals, tangents, etc) is assigned.
 ///
 /// Currently only supports the GLTF format.
 bool gfx_load_scene(
diff --git a/gfx/shaders/cook_torrance.frag b/gfx/shaders/cook_torrance.frag
index b4cf590..b2bfd7d 100644
--- a/gfx/shaders/cook_torrance.frag
+++ b/gfx/shaders/cook_torrance.frag
@@ -11,6 +11,7 @@ uniform sampler2D EmissiveTexture;
 uniform sampler2D AmbientOcclusionTexture;
 uniform sampler2D NormalMap;
 
+// TODO: Handle case in which there is no sky. Pass a boolean.
 uniform samplerCube Sky;
 uniform samplerCube IrradianceMap;
 uniform samplerCube PrefilteredEnvironmentMap;
diff --git a/gfx/shaders/cook_torrance.vert b/gfx/shaders/cook_torrance.vert
index c66efa6..56dfeda 100644
--- a/gfx/shaders/cook_torrance.vert
+++ b/gfx/shaders/cook_torrance.vert
@@ -4,6 +4,7 @@ uniform mat4 ModelMatrix;
 uniform mat4 MVP;
 
 layout (location = 0) in vec3 vPosition;
+// TODO: Add HAS_NORMALS
 layout (location = 1) in vec3 vNormal;
 #ifdef HAS_TANGENTS
 layout (location = 2) in vec4 vTangent;
diff --git a/gfx/src/renderer/renderer.c b/gfx/src/renderer/renderer.c
index ed781c9..6b77ffe 100644
--- a/gfx/src/renderer/renderer.c
+++ b/gfx/src/renderer/renderer.c
@@ -18,12 +18,12 @@
 
 #include <assert.h>
 
-static const int IRRADIANCE_MAP_WIDTH = 1024;
-static const int IRRADIANCE_MAP_HEIGHT = 1024;
-static const int PREFILTERED_ENVIRONMENT_MAP_WIDTH = 128;
+static const int IRRADIANCE_MAP_WIDTH               = 1024;
+static const int IRRADIANCE_MAP_HEIGHT              = 1024;
+static const int PREFILTERED_ENVIRONMENT_MAP_WIDTH  = 128;
 static const int PREFILTERED_ENVIRONMENT_MAP_HEIGHT = 128;
-static const int BRDF_INTEGRATION_MAP_WIDTH = 512;
-static const int BRDF_INTEGRATION_MAP_HEIGHT = 512;
+static const int BRDF_INTEGRATION_MAP_WIDTH         = 512;
+static const int BRDF_INTEGRATION_MAP_HEIGHT        = 512;
 
 bool renderer_make(Renderer* renderer, RenderBackend* render_backend) {
   assert(renderer);
@@ -63,9 +63,9 @@ void renderer_destroy(Renderer* renderer, RenderBackend* render_backend) {
 
 /// Computes irradiance and prefiltered environment maps for the light if they
 /// have not been already computed.
-static bool setup_environment_light(Renderer* renderer,
-                                    RenderBackend* render_backend,
-                                    EnvironmentLight* light) {
+static bool setup_environment_light(
+    Renderer* renderer, RenderBackend* render_backend,
+    EnvironmentLight* light) {
   assert(renderer);
   assert(light);
   assert(renderer->ibl);
@@ -75,7 +75,7 @@ static bool setup_environment_light(Renderer* renderer,
     return true;
   }
 
-  Texture* irradiance_map = 0;
+  Texture* irradiance_map              = 0;
   Texture* prefiltered_environment_map = 0;
 
   if (!(irradiance_map = gfx_make_irradiance_map(
@@ -92,9 +92,9 @@ static bool setup_environment_light(Renderer* renderer,
     goto cleanup;
   }
 
-  light->irradiance_map = irradiance_map;
+  light->irradiance_map              = irradiance_map;
   light->prefiltered_environment_map = prefiltered_environment_map;
-  light->max_reflection_lod = max_mip_level;
+  light->max_reflection_lod          = max_mip_level;
 
   return true;
 
@@ -110,21 +110,21 @@ cleanup:
 
 typedef struct RenderState {
   RenderBackend* render_backend;
-  Renderer* renderer;
-  const Scene* scene;
-  const Camera* camera;
-  const mat4* view_matrix;
-  const mat4* projection;
-  Light* environment_light;
-  const float fovy;
-  const float aspect;
+  Renderer*      renderer;
+  const Scene*   scene;
+  const Camera*  camera;
+  const mat4*    view_matrix;
+  const mat4*    projection;
+  Light*         environment_light;
+  const float    fovy;
+  const float    aspect;
 } RenderState;
 
-static void draw_recursively(RenderState* state, mat4 parent_transform,
-                             node_idx node_index) {
+static void draw_recursively(
+    RenderState* state, mat4 parent_transform, node_idx node_index) {
   assert(state);
-  const SceneNode* node = mem_get_node(node_index);
-  const mat4 node_transform = mat4_mul(parent_transform, node->transform);
+  const SceneNode* node           = mem_get_node(node_index);
+  const mat4       node_transform = mat4_mul(parent_transform, node->transform);
 
   // Activate light.
   if (node->type == LightNode) {
@@ -145,13 +145,13 @@ static void draw_recursively(RenderState* state, mat4 parent_transform,
     assert(object);
 
     const mat4 model_matrix = mat4_mul(node_transform, object->transform);
-    const mat4 modelview = mat4_mul(*state->view_matrix, model_matrix);
-    const mat4 mvp = mat4_mul(*state->projection, modelview);
+    const mat4 modelview    = mat4_mul(*state->view_matrix, model_matrix);
+    const mat4 mvp          = mat4_mul(*state->projection, modelview);
 
     for (mesh_link_idx mesh_link_index = object->mesh_link;
          mesh_link_index.val;) {
       const MeshLink* mesh_link = mem_get_mesh_link(mesh_link_index);
-      mesh_link_index = mesh_link->next;
+      mesh_link_index           = mesh_link->next;
 
       const Mesh* mesh = mem_get_mesh(mesh_link->mesh);
       if (!mesh) {
@@ -159,11 +159,10 @@ static void draw_recursively(RenderState* state, mat4 parent_transform,
       }
       assert(mesh->geometry);
       assert(mesh->material);
-      material_activate(mesh->material);
       // Apply common shader uniforms not captured by materials.
       // TODO: Avoid computing matrices like Modelview or MVP if the shader does
       // not use them.
-      ShaderProgram* shader = mesh->material->shader;
+      ShaderProgram* shader = mesh->shader;
       gfx_set_mat4_uniform(shader, "ModelMatrix", &model_matrix);
       gfx_set_mat4_uniform(shader, "Modelview", &modelview);
       gfx_set_mat4_uniform(shader, "Projection", state->projection);
@@ -177,19 +176,19 @@ static void draw_recursively(RenderState* state, mat4 parent_transform,
         assert(light->irradiance_map);
         assert(light->prefiltered_environment_map);
         assert(state->renderer->brdf_integration_map);
-        gfx_set_texture_uniform(shader, "BRDFIntegrationMap",
-                                state->renderer->brdf_integration_map);
+        gfx_set_texture_uniform(
+            shader, "BRDFIntegrationMap",
+            state->renderer->brdf_integration_map);
         gfx_set_texture_uniform(shader, "Sky", light->environment_map);
         gfx_set_texture_uniform(shader, "IrradianceMap", light->irradiance_map);
-        gfx_set_texture_uniform(shader, "PrefilteredEnvironmentMap",
-                                light->prefiltered_environment_map);
-        gfx_set_float_uniform(shader, "MaxReflectionLOD",
-                              light->max_reflection_lod);
+        gfx_set_texture_uniform(
+            shader, "PrefilteredEnvironmentMap",
+            light->prefiltered_environment_map);
+        gfx_set_float_uniform(
+            shader, "MaxReflectionLOD", light->max_reflection_lod);
       }
-      // TODO: Remove this altogether.
-      // This is not needed because material_activate() already activates the
-      // shader.
-      // gfx_activate_shader_program(shader);
+      material_activate(shader, mesh->material);
+      gfx_activate_shader_program(shader);
       gfx_apply_uniforms(shader);
       gfx_render_geometry(mesh->geometry);
     }
@@ -200,12 +199,13 @@ static void draw_recursively(RenderState* state, mat4 parent_transform,
     draw_recursively(state, node_transform, child_index);
 
     const SceneNode* child = mem_get_node(child_index);
-    child_index = child->next;
+    child_index            = child->next;
   }
 }
 
-void gfx_render_scene(Renderer* renderer, RenderBackend* render_backend,
-                      const Scene* scene, const SceneCamera* camera) {
+void gfx_render_scene(
+    Renderer* renderer, RenderBackend* render_backend, const Scene* scene,
+    const SceneCamera* camera) {
   assert(renderer);
   assert(render_backend);
 
@@ -221,16 +221,17 @@ void gfx_render_scene(Renderer* renderer, RenderBackend* render_backend,
   gfx_get_viewport(render_backend, &width, &height);
   const float aspect = (float)width / (float)height;
 
-  RenderState state = {.render_backend = render_backend,
-                       .renderer = renderer,
-                       .scene = scene,
-                       .camera = &camera->camera,
-                       .view_matrix = &view_matrix,
-                       .projection = &projection,
-                       .environment_light = 0,
-                       // Assuming a perspective matrix.
-                       .fovy = atan(1.0 / (mat4_at(projection, 1, 1))) * 2,
-                       .aspect = aspect};
+  RenderState state = {
+      .render_backend    = render_backend,
+      .renderer          = renderer,
+      .scene             = scene,
+      .camera            = &camera->camera,
+      .view_matrix       = &view_matrix,
+      .projection        = &projection,
+      .environment_light = 0,
+      // Assuming a perspective matrix.
+      .fovy   = atan(1.0 / (mat4_at(projection, 1, 1))) * 2,
+      .aspect = aspect};
 
   gfx_start_frame(render_backend);
   draw_recursively(&state, mat4_id(), scene->root);
diff --git a/gfx/src/scene/material.c b/gfx/src/scene/material.c
index d44746a..e5856d0 100644
--- a/gfx/src/scene/material.c
+++ b/gfx/src/scene/material.c
@@ -8,7 +8,6 @@ static void material_make(Material* material, const MaterialDesc* desc) {
   assert(material);
   assert(desc);
   assert(desc->num_uniforms < GFX_MAX_UNIFORMS_PER_MATERIAL);
-  material->shader = desc->shader;
   material->num_uniforms = desc->num_uniforms;
   for (int i = 0; i < desc->num_uniforms; ++i) {
     material->uniforms[i] = desc->uniforms[i];
@@ -47,11 +46,10 @@ static void set_uniform(ShaderProgram* prog, const ShaderUniform* uniform) {
   }
 }
 
-void material_activate(const Material* material) {
+void material_activate(ShaderProgram* shader, const Material* material) {
   assert(material);
-  gfx_activate_shader_program(material->shader);
   for (int i = 0; i < material->num_uniforms; ++i) {
     const ShaderUniform* uniform = &material->uniforms[i];
-    set_uniform(material->shader, uniform);
+    set_uniform(shader, uniform);
   }
 }
diff --git a/gfx/src/scene/material_impl.h b/gfx/src/scene/material_impl.h
index c680ccf..a6aa95b 100644
--- a/gfx/src/scene/material_impl.h
+++ b/gfx/src/scene/material_impl.h
@@ -5,13 +5,12 @@
 typedef struct ShaderProgram ShaderProgram;
 
 typedef struct Material {
-  ShaderProgram* shader;
   ShaderUniform uniforms[GFX_MAX_UNIFORMS_PER_MATERIAL];
-  int num_uniforms;
+  int           num_uniforms;
 } Material;
 
 /// Activate the material.
 ///
 /// This activates the material's shader and configures the shader uniforms that
 /// are specific to the material.
-void material_activate(const Material* material);
+void material_activate(ShaderProgram* shader, const Material* material);
diff --git a/gfx/src/scene/mesh.c b/gfx/src/scene/mesh.c
index 722eae7..689105c 100644
--- a/gfx/src/scene/mesh.c
+++ b/gfx/src/scene/mesh.c
@@ -9,8 +9,10 @@ static void mesh_make(Mesh* mesh, const MeshDesc* desc) {
   assert(desc);
   assert(desc->geometry);
   assert(desc->material);
+  assert(desc->shader);
   mesh->geometry = desc->geometry;
   mesh->material = desc->material;
+  mesh->shader   = desc->shader;
 }
 
 Mesh* gfx_make_mesh(const MeshDesc* desc) {
diff --git a/gfx/src/scene/mesh_impl.h b/gfx/src/scene/mesh_impl.h
index 858b147..560b77e 100644
--- a/gfx/src/scene/mesh_impl.h
+++ b/gfx/src/scene/mesh_impl.h
@@ -5,6 +5,7 @@
 typedef struct Mesh {
   const Geometry* geometry;
   const Material* material;
+  ShaderProgram*  shader;
 } Mesh;
 
 // TODO: a mesh_render() that takes a transform, applies the material and the
diff --git a/gfx/src/util/scene.c b/gfx/src/util/scene.c
index 9511a71..dc97259 100644
--- a/gfx/src/util/scene.c
+++ b/gfx/src/util/scene.c
@@ -141,8 +141,9 @@ typedef struct MeshPermutation {
   union {
     struct {
       // Vertex attributes.
-      bool has_normals  : 1;
-      bool has_tangents : 1;
+      bool has_texcoords : 1;
+      bool has_normals   : 1;
+      bool has_tangents  : 1;
       // Textures.
       bool has_normal_map        : 1;
       bool has_occlusion_texture : 1;
@@ -282,36 +283,31 @@ cleanup:
   return 0;
 }
 
-/// Load all textures from the glTF scene.
+/// Lazily load all textures from the glTF scene.
 ///
-/// Return an array of Textures such that the index of each glTF texture in the
-/// original array matches the same Texture in the resulting array.
+/// Colour textures like albedo are in sRGB colour space. Non-colour textures
+/// like normal maps are in linear space (e.g. DamagedHelmet sample). Since we
+/// don't know how the texture is going to be used at this point, we can't tell
+/// what colour space it should be loaded in (ideally this would be part of the
+/// image file format, but not all formats specify colour space.) Therefore, we
+/// load the textures lazily and don't actually commit them to GPU memory until
+/// we know their colour space when loading glTF materials.
 ///
-/// Most textures are in sRGB colour space. One exception is normal maps, which
-/// are typically authored in linear space (e.g. DamagedHelmet sample). Since
-/// we don't know what colour space we should use at this point, we load the
-/// textures lazily and don't actually commit them to GPU memory until we know
-/// their colour space.
+/// Return an array of LoadTextureCmds such that the index of each cmd matches
+/// the index of each glTF texture in the scene. Also return the number of
+/// textures.
 ///
-/// This function returns an array of Texture objects, all of which are null due
-/// to lazy loading. It also returns an array of LoadTextureCmd which describes
-/// how each Texture in the first array should be loaded.
-static Texture** load_textures(
+/// Return true on success (all textures processed or no textures in the
+/// scene), false otherwise.
+static bool load_textures_lazy(
     const cgltf_data* data, RenderBackend* render_backend,
     const char* directory, LoadTextureCmd** load_texture_cmds,
     cgltf_size* num_textures) {
   assert(data);
   assert(render_backend);
+  assert(load_texture_cmds);
   assert(num_textures);
 
-  Texture** textures = 0;
-  *load_texture_cmds = 0;
-
-  textures = calloc(data->textures_count, sizeof(Texture*));
-  if (!textures) {
-    goto cleanup;
-  }
-
   *load_texture_cmds = calloc(data->textures_count, sizeof(LoadTextureCmd));
   if (!*load_texture_cmds) {
     goto cleanup;
@@ -374,29 +370,21 @@ static Texture** load_textures(
   }
 
   *num_textures = data->textures_count;
-  return textures;
+  return true;
 
 cleanup:
-  if (textures) {
-    for (cgltf_size i = 0; i < data->textures_count; ++i) {
-      if (textures[i]) {
-        gfx_destroy_texture(render_backend, &textures[i]);
-      }
-    }
-    free(textures);
-  }
   if (*load_texture_cmds) {
     free(*load_texture_cmds);
   }
   *num_textures = 0;
-  return 0;
+  return false;
 }
 
 /// Load a texture uniform.
 ///
 /// This determines a texture's colour space based on its intended use, loads
 /// the texture, and then defines the sampler shader uniform.
-static bool load_texture_uniform(
+static bool load_texture_and_uniform(
     const cgltf_data* data, RenderBackend* render_backend,
     const cgltf_texture_view* texture_view, TextureType texture_type,
     Texture** textures, LoadTextureCmd* load_texture_cmds, int* next_uniform,
@@ -412,24 +400,31 @@ static bool load_texture_uniform(
   const size_t texture_index = texture_view->texture - data->textures;
   assert(texture_index < data->textures_count);
 
-  LoadTextureCmd* cmd = &load_texture_cmds[texture_index];
-  if (texture_type == NormalMap) {
-    cmd->colour_space = LinearColourSpace;
-  }
-
-  LOGD(
-      "Load texture: %s (mipmaps: %d, filtering: %d)",
-      mstring_cstring(&cmd->data.texture.filepath), cmd->mipmaps,
-      cmd->filtering);
-
-  textures[texture_index] = gfx_load_texture(render_backend, cmd);
+  // Here we are assuming that if a texture is re-used, it is re-used with the
+  // same texture view. This should be fine because, e.g., a normal map would
+  // not be used as albedo and vice versa.
   if (!textures[texture_index]) {
-    gfx_prepend_error(
-        "Failed to load texture: %s",
-        mstring_cstring(&cmd->data.texture.filepath));
-    return false;
+    LoadTextureCmd* cmd = &load_texture_cmds[texture_index];
+    // TODO: Check for colour textures and default to LinearColourSpace instead.
+    if (texture_type == NormalMap) {
+      cmd->colour_space = LinearColourSpace;
+    }
+
+    LOGD(
+        "Load texture: %s (mipmaps: %d, filtering: %d)",
+        mstring_cstring(&cmd->data.texture.filepath), cmd->mipmaps,
+        cmd->filtering);
+
+    textures[texture_index] = gfx_load_texture(render_backend, cmd);
+    if (!textures[texture_index]) {
+      gfx_prepend_error(
+          "Failed to load texture: %s",
+          mstring_cstring(&cmd->data.texture.filepath));
+      return false;
+    }
   }
 
+  assert(*next_uniform < GFX_MAX_UNIFORMS_PER_MATERIAL);
   desc->uniforms[(*next_uniform)++] = (ShaderUniform){
       .name          = sstring_make(TextureUniformName(texture_type)),
       .type          = UniformTexture,
@@ -440,97 +435,102 @@ static bool load_texture_uniform(
 
 /// Load all materials from the glTF scene.
 ///
-/// Return an array of Materials such that the index of each glTF material in
-/// the original array matches the same Material in the resulting array.
+/// Return an array of Materials such that the index of each descriptor matches
+/// the index of each glTF material in the scene. Also return the number of
+/// materials and the textures used by them.
 static Material** load_materials(
     const cgltf_data* data, RenderBackend* render_backend,
-    ShaderProgram* shader, Texture** textures,
-    LoadTextureCmd* load_texture_cmds, cgltf_size* num_materials) {
+    LoadTextureCmd* load_texture_cmds, Texture*** textures,
+    cgltf_size* num_materials) {
   assert(data);
   assert(render_backend);
-  assert(shader);
-  assert(textures);
   assert(load_texture_cmds);
+  assert(textures);
   assert(num_materials);
 
-  Material** materials = 0;
-
-  materials = calloc(data->materials_count, sizeof(Material*));
+  Material** materials = calloc(data->materials_count, sizeof(Material*));
   if (!materials) {
     goto cleanup;
   }
 
+  *textures = calloc(data->textures_count, sizeof(Texture*));
+  if (!*textures) {
+    goto cleanup;
+  }
+
   for (cgltf_size i = 0; i < data->materials_count; ++i) {
     const cgltf_material* mat = &data->materials[i];
 
     int          next_uniform = 0;
-    MaterialDesc desc         = (MaterialDesc){.shader = shader};
+    MaterialDesc desc         = {0};
 
     // TODO: emissive texture/factor and other material parameters.
     if (mat->has_pbr_metallic_roughness) {
       const cgltf_pbr_metallic_roughness* pbr = &mat->pbr_metallic_roughness;
 
-      assert(next_uniform + 3 < GFX_MAX_UNIFORMS_PER_MATERIAL);
-
+      assert(next_uniform < GFX_MAX_UNIFORMS_PER_MATERIAL);
       desc.uniforms[next_uniform++] = (ShaderUniform){
           .name       = sstring_make(UNIFORM_BASE_COLOR_FACTOR),
           .type       = UniformVec4,
           .value.vec4 = vec4_from_array(pbr->base_color_factor)};
 
+      assert(next_uniform < GFX_MAX_UNIFORMS_PER_MATERIAL);
       desc.uniforms[next_uniform++] = (ShaderUniform){
           .name         = sstring_make(UNIFORM_METALLIC_FACTOR),
           .type         = UniformFloat,
           .value.scalar = pbr->metallic_factor};
 
+      assert(next_uniform < GFX_MAX_UNIFORMS_PER_MATERIAL);
       desc.uniforms[next_uniform++] = (ShaderUniform){
           .name         = sstring_make(UNIFORM_ROUGHNESS_FACTOR),
           .type         = UniformFloat,
           .value.scalar = pbr->roughness_factor};
 
+      assert(next_uniform < GFX_MAX_UNIFORMS_PER_MATERIAL);
+      desc.uniforms[next_uniform++] = (ShaderUniform){
+          .name       = sstring_make(UNIFORM_EMISSIVE_FACTOR),
+          .type       = UniformVec3,
+          .value.vec3 = vec3_from_array(mat->emissive_factor)};
+
       if (pbr->base_color_texture.texture) {
-        if (!load_texture_uniform(
+        if (!load_texture_and_uniform(
                 data, render_backend, &pbr->base_color_texture,
-                BaseColorTexture, textures, load_texture_cmds, &next_uniform,
+                BaseColorTexture, *textures, load_texture_cmds, &next_uniform,
                 &desc)) {
           goto cleanup;
         }
       }
 
       if (pbr->metallic_roughness_texture.texture) {
-        if (!load_texture_uniform(
+        if (!load_texture_and_uniform(
                 data, render_backend, &pbr->metallic_roughness_texture,
-                MetallicRoughnessTexture, textures, load_texture_cmds,
+                MetallicRoughnessTexture, *textures, load_texture_cmds,
                 &next_uniform, &desc)) {
           goto cleanup;
         }
       }
     }
 
-    desc.uniforms[next_uniform++] = (ShaderUniform){
-        .name       = sstring_make(UNIFORM_EMISSIVE_FACTOR),
-        .type       = UniformVec3,
-        .value.vec3 = vec3_from_array(mat->emissive_factor)};
-
     if (mat->emissive_texture.texture) {
-      if (!load_texture_uniform(
+      if (!load_texture_and_uniform(
               data, render_backend, &mat->emissive_texture, EmissiveTexture,
-              textures, load_texture_cmds, &next_uniform, &desc)) {
+              *textures, load_texture_cmds, &next_uniform, &desc)) {
         goto cleanup;
       }
     }
 
     if (mat->occlusion_texture.texture) {
-      if (!load_texture_uniform(
+      if (!load_texture_and_uniform(
               data, render_backend, &mat->occlusion_texture,
-              AmbientOcclusionTexture, textures, load_texture_cmds,
+              AmbientOcclusionTexture, *textures, load_texture_cmds,
               &next_uniform, &desc)) {
         goto cleanup;
       }
     }
 
     if (mat->normal_texture.texture) {
-      if (!load_texture_uniform(
-              data, render_backend, &mat->normal_texture, NormalMap, textures,
+      if (!load_texture_and_uniform(
+              data, render_backend, &mat->normal_texture, NormalMap, *textures,
               load_texture_cmds, &next_uniform, &desc)) {
         goto cleanup;
       }
@@ -557,28 +557,25 @@ cleanup:
     }
     free(materials);
   }
+  if (*textures) {
+    for (cgltf_size i = 0; i < data->textures_count; ++i) {
+      if ((*textures)[i]) {
+        gfx_destroy_texture(render_backend, &(*textures)[i]);
+      }
+    }
+    free(*textures);
+    *textures = 0;
+  }
   *num_materials = 0;
   return 0;
 }
 
-/// Configures the MeshPermutation based on the given material.
-static void configure_material_permutation(
-    MeshPermutation* perm, cgltf_material* material) {
-  assert(perm);
-  assert(material);
-
-  perm->has_normal_map        = material->normal_texture.texture != 0;
-  perm->has_occlusion_texture = material->occlusion_texture.texture != 0;
-  perm->has_emissive_texture  = material->emissive_texture.texture != 0;
-}
-
 /// Load all meshes from the glTF scene.
 static SceneObject** load_meshes(
     const cgltf_data* data, Gfx* gfx, Buffer** buffers,
-    Buffer** tangent_buffers, Material** materials,
-    const cgltfTangentBuffer* cgltf_tangent_buffers,
-    cgltf_size num_tangent_buffers, Geometry*** geometries, Mesh*** meshes,
-    MeshPermutation** mesh_permutations, cgltf_size* num_geometries,
+    Buffer** tangent_buffers, const cgltfTangentBuffer* cgltf_tangent_buffers,
+    cgltf_size num_tangent_buffers, Material** materials, ShaderProgram* shader,
+    Geometry*** geometries, Mesh*** meshes, cgltf_size* num_geometries,
     cgltf_size* num_meshes, cgltf_size* num_scene_objects) {
   // Walk through the mesh primitives to create Meshes. A GLTF mesh primitive
   // has a material (Mesh) and vertex data (Geometry). A GLTF mesh maps to
@@ -594,9 +591,9 @@ static SceneObject** load_meshes(
   assert(gfx);
   assert(buffers);
   assert(materials);
+  assert(shader);
   assert(geometries);
   assert(meshes);
-  assert(mesh_permutations);
   assert(num_geometries);
   assert(num_meshes);
   assert(num_scene_objects);
@@ -618,10 +615,6 @@ static SceneObject** load_meshes(
   if (!*meshes) {
     goto cleanup;
   }
-  *mesh_permutations = calloc(primitive_count, sizeof(MeshPermutation));
-  if (!*mesh_permutations) {
-    goto cleanup;
-  }
   SceneObject** objects = calloc(data->meshes_count, sizeof(SceneObject*));
   if (!objects) {
     goto cleanup;
@@ -642,7 +635,12 @@ static SceneObject** load_meshes(
     for (cgltf_size p = 0; p < mesh->primitives_count; ++p) {
       assert(next_mesh < primitive_count);
       const cgltf_primitive* prim = &mesh->primitives[p];
-      MeshPermutation*       perm = mesh_permutations[next_mesh];
+      const cgltf_material*  mat  = prim->material;
+
+      MeshPermutation perm       = {0};
+      perm.has_normal_map        = mat->normal_texture.texture != 0;
+      perm.has_occlusion_texture = mat->occlusion_texture.texture != 0;
+      perm.has_emissive_texture  = mat->emissive_texture.texture != 0;
 
       GeometryDesc geometry_desc = {
           .type = from_gltf_primitive_type(prim->type)};
@@ -693,18 +691,21 @@ static SceneObject** load_meshes(
             assert(false);
             break;
           }
+          // It is assumed that meshes have positions, so there is nothing to
+          // do for the mesh permutation in this case.
           break;
         }
         case cgltf_attribute_type_normal:
-          buffer_view_3d    = &geometry_desc.normals;
-          perm->has_normals = true;
+          buffer_view_3d   = &geometry_desc.normals;
+          perm.has_normals = true;
           break;
         case cgltf_attribute_type_tangent:
-          buffer_view_4d     = &geometry_desc.tangents;
-          perm->has_tangents = true;
+          buffer_view_4d    = &geometry_desc.tangents;
+          perm.has_tangents = true;
           break;
         case cgltf_attribute_type_texcoord:
-          buffer_view_2d = &geometry_desc.texcoords;
+          buffer_view_2d     = &geometry_desc.texcoords;
+          perm.has_texcoords = true;
           break;
         default:
           // Attribute ignored.
@@ -747,8 +748,8 @@ static SceneObject** load_meshes(
       }
 
       // Set the number of vertices in the geometry. Since a geometry can have
-      // either 2d or 3d positions but not both, here we can perform addition to
-      // compute the total number of vertices.
+      // either 2d or 3d positions but not both, here we can perform addition
+      // to compute the total number of vertices.
       geometry_desc.num_verts =
           (geometry_desc.positions2d.size_bytes / sizeof(vec2)) +
           (geometry_desc.positions3d.size_bytes / sizeof(vec3));
@@ -771,17 +772,40 @@ static SceneObject** load_meshes(
             geometry_desc.num_verts);
       }
 
-      (*geometries)[next_mesh] =
-          gfx_make_geometry(render_backend, &geometry_desc);
-
       const cgltf_size material_index = prim->material - data->materials;
       assert(material_index < data->materials_count);
-      const Material* material = materials[material_index];
+      Material* material = materials[material_index];
+
+      // TODO: We need a better way to handle clean-up, specifically of
+      // materials. One is to add materials to a dynamically-allocated list or
+      // vector. Another is to expose some kind of scene purge that deletes the
+      // resources of a given scene. The latter would make clean-up much simpler
+      // in general, not just for materials.
+
+      (*geometries)[next_mesh] =
+          gfx_make_geometry(render_backend, &geometry_desc);
+      if (!(*geometries)[next_mesh]) {
+        goto cleanup;
+      }
 
-      configure_material_permutation(perm, prim->material);
+      // If the user specifies a custom shader, use that instead.
+      // else TODO: Build a shader based on permutation.
+      //
+      // TODO: We should cache shader permutations to re-use shader programs.
+      // Caching should not be done locally here because a caller may call
+      // gfx_load_scene() multiple times to load multiple scenes, and we want
+      // shader re-use across scenes too.
+      //
+      // On the other hand, caching materials is not necessary since, provided
+      // they can share shaders, the renderer can check later whether uniforms
+      // have the same values. Also, changing uniforms is much faster than
+      // swapping shaders, so shader caching is the most important thing here.
+      assert(shader);
 
       (*meshes)[next_mesh] = gfx_make_mesh(&(MeshDesc){
-          .geometry = (*geometries)[next_mesh], .material = material});
+          .geometry = (*geometries)[next_mesh],
+          .material = material,
+          .shader   = shader});
 
       gfx_add_object_mesh(objects[m], (*meshes)[next_mesh]);
 
@@ -814,10 +838,6 @@ cleanup:
     free(*meshes);
     *meshes = 0;
   }
-  if (*mesh_permutations) {
-    free(*mesh_permutations);
-    mesh_permutations = 0;
-  }
   if (objects) {
     for (cgltf_size i = 0; i < data->meshes_count; ++i) {
       if (objects[i]) {
@@ -939,8 +959,8 @@ static SceneNode** load_nodes(
     gfx_set_node_transform(nodes[n], &transform);
 
     // By default, set nodes as children of the root node. The second pass will
-    // properly set the parent of the relevant nodes, and leave the root node
-    // as the parent for top-level nodes.
+    // properly set the parent of the relevant nodes, and leave the root node as
+    // the parent for top-level nodes.
     gfx_set_node_parent(nodes[n], root_node);
   } // SceneNode.
 
@@ -1010,26 +1030,28 @@ static bool load_scene(
   LOGD("Filepath: %s", filepath);
   LOGD("Directory: %s", mstring_cstring(&directory));
 
-  Buffer**         buffers           = 0;
-  Buffer**         tangent_buffers   = 0;
-  Geometry**       geometries        = 0;
-  Material**       materials         = 0;
-  Mesh**           meshes            = 0;
-  SceneObject**    scene_objects     = 0;
-  SceneCamera**    scene_cameras     = 0;
-  SceneNode**      scene_nodes       = 0;
-  Texture**        textures          = 0;
-  LoadTextureCmd*  load_texture_cmds = 0;
-  MeshPermutation* mesh_permutations = 0;
-  cgltf_size       num_buffers       = 0;
-  cgltf_size       num_geometries    = 0;
-  cgltf_size       num_materials     = 0;
-  cgltf_size       num_meshes        = 0;
-  cgltf_size       num_scene_objects = 0;
-  cgltf_size       num_scene_cameras = 0;
-  cgltf_size       num_scene_nodes   = 0;
-  cgltf_size       num_textures      = 0;
-
+  Buffer**        buffers           = 0;
+  Buffer**        tangent_buffers   = 0;
+  Geometry**      geometries        = 0;
+  Material**      materials         = 0;
+  Mesh**          meshes            = 0;
+  SceneObject**   scene_objects     = 0;
+  SceneCamera**   scene_cameras     = 0;
+  SceneNode**     scene_nodes       = 0;
+  Texture**       textures          = 0;
+  LoadTextureCmd* load_texture_cmds = 0;
+  cgltf_size      num_buffers       = 0;
+  cgltf_size      num_geometries    = 0;
+  cgltf_size      num_materials     = 0;
+  cgltf_size      num_meshes        = 0;
+  cgltf_size      num_scene_objects = 0;
+  cgltf_size      num_scene_cameras = 0;
+  cgltf_size      num_scene_nodes   = 0;
+  cgltf_size      num_textures      = 0;
+
+  // TODO: Let this function handle all the cleanup. Let the other functions
+  // return pass/failure booleans and the arrays as in/out parameters. This way
+  // we do not need the individual functions to duplicate cleanup code.
   buffers = load_buffers(data, render_backend, &num_buffers);
   if (!buffers) {
     goto cleanup;
@@ -1043,23 +1065,21 @@ static bool load_scene(
     }
   }
 
-  textures = load_textures(
-      data, render_backend, mstring_cstring(&directory), &load_texture_cmds,
-      &num_textures);
-  if (!textures || !load_texture_cmds) {
+  if (!load_textures_lazy(
+          data, render_backend, mstring_cstring(&directory), &load_texture_cmds,
+          &num_textures)) {
     goto cleanup;
   }
 
   materials = load_materials(
-      data, render_backend, shader, textures, load_texture_cmds,
-      &num_materials);
+      data, render_backend, load_texture_cmds, &textures, &num_materials);
   if (!materials) {
     goto cleanup;
   }
 
   scene_objects = load_meshes(
-      data, gfx, buffers, tangent_buffers, materials, cgltf_tangent_buffers,
-      num_tangent_buffers, &geometries, &meshes, &mesh_permutations,
+      data, gfx, buffers, tangent_buffers, cgltf_tangent_buffers,
+      num_tangent_buffers, materials, shader, &geometries, &meshes,
       &num_geometries, &num_meshes, &num_scene_objects);
   if (!scene_objects) {
     goto cleanup;
@@ -1132,9 +1152,6 @@ cleanup:
   if (load_texture_cmds) {
     free(load_texture_cmds);
   }
-  if (mesh_permutations) {
-    free(mesh_permutations);
-  }
   return false;
 }
 
@@ -1143,7 +1160,6 @@ bool gfx_load_scene(
     const LoadSceneCmd* cmd) {
   assert(gfx);
   assert(root_node);
-  assert(shader);
   assert(cmd);
 
   bool success = false;
diff --git a/gfx/src/util/skyquad.c b/gfx/src/util/skyquad.c
index 51c250b..2461f8c 100644
--- a/gfx/src/util/skyquad.c
+++ b/gfx/src/util/skyquad.c
@@ -22,11 +22,11 @@ SceneObject* gfx_make_skyquad(Gfx* gfx, Scene* scene, const Texture* texture) {
   RenderBackend* render_backend = gfx_get_render_backend(gfx);
   assert(render_backend);
 
-  ShaderProgram* shader = 0;
-  Geometry* geometry = 0;
-  Material* material = 0;
-  Mesh* mesh = 0;
-  SceneObject* object = 0;
+  ShaderProgram* shader   = 0;
+  Geometry*      geometry = 0;
+  Material*      material = 0;
+  Mesh*          mesh     = 0;
+  SceneObject*   object   = 0;
 
   shader = gfx_make_skyquad_shader(render_backend);
   if (!shader) {
@@ -39,12 +39,12 @@ SceneObject* gfx_make_skyquad(Gfx* gfx, Scene* scene, const Texture* texture) {
   }
 
   MaterialDesc material_desc = (MaterialDesc){0};
-  material_desc.shader = shader;
-  material_desc.uniforms[0] = (ShaderUniform){.type = UniformTexture,
-                                              .value.texture = texture,
-                                              .name = sstring_make("Skyquad")};
+  material_desc.uniforms[0]  = (ShaderUniform){
+       .type          = UniformTexture,
+       .value.texture = texture,
+       .name          = sstring_make("Skyquad")};
   material_desc.num_uniforms = 1;
-  material = gfx_make_material(&material_desc);
+  material                   = gfx_make_material(&material_desc);
   if (!material) {
     goto cleanup;
   }
@@ -52,7 +52,8 @@ SceneObject* gfx_make_skyquad(Gfx* gfx, Scene* scene, const Texture* texture) {
   MeshDesc mesh_desc = (MeshDesc){0};
   mesh_desc.geometry = geometry;
   mesh_desc.material = material;
-  mesh = gfx_make_mesh(&mesh_desc);
+  mesh_desc.shader   = shader;
+  mesh               = gfx_make_mesh(&mesh_desc);
   if (!mesh) {
     goto cleanup;
   }
diff --git a/gltfview/src/game.c b/gltfview/src/game.c
index d7352d4..f2e5a88 100644
--- a/gltfview/src/game.c
+++ b/gltfview/src/game.c
@@ -24,7 +24,7 @@
 #include <unistd.h> // usleep; TODO Remove.
 
 // Paths to various scene files.
-static const char* BOX = "/assets/models/box.gltf";
+static const char* BOX     = "/assets/models/box.gltf";
 static const char* SUZANNE = "/assets/models/suzanne.gltf";
 static const char* SPONZA =
     "/assets/glTF-Sample-Models/2.0/Sponza/glTF/Sponza.gltf";
@@ -37,8 +37,8 @@ static const char* DAMAGED_HELMET =
 
 static const char* CLOUDS1_TEXTURE = "/assets/skybox/clouds1/clouds1_west.bmp";
 
-static ShaderProgram* load_shader(RenderBackend* render_backend,
-                                  const char* view_mode) {
+static ShaderProgram* load_shader(
+    RenderBackend* render_backend, const char* view_mode) {
   ShaderProgram* shader = 0;
   if (strcmp(view_mode, "debug") == 0) {
     shader = gfx_make_debug3d_shader(render_backend);
@@ -59,23 +59,24 @@ static Texture* load_environment_map(RenderBackend* render_backend) {
   return gfx_load_texture(
       render_backend,
       &(LoadTextureCmd){
-          .origin = TextureFromFile,
-          .type = LoadCubemap,
-          .colour_space = sRGB,
-          .filtering = NearestFiltering,
-          .mipmaps = false,
+          .origin                 = TextureFromFile,
+          .type                   = LoadCubemap,
+          .colour_space           = sRGB,
+          .filtering              = NearestFiltering,
+          .mipmaps                = false,
           .data.cubemap.filepaths = {
-              mstring_make("/assets/skybox/clouds1/clouds1_east.bmp"),
-              mstring_make("/assets/skybox/clouds1/clouds1_west.bmp"),
-              mstring_make("/assets/skybox/clouds1/clouds1_up.bmp"),
-              mstring_make("/assets/skybox/clouds1/clouds1_down.bmp"),
-              mstring_make("/assets/skybox/clouds1/clouds1_north.bmp"),
-              mstring_make("/assets/skybox/clouds1/clouds1_south.bmp")}});
+                                     mstring_make("/assets/skybox/clouds1/clouds1_east.bmp"),
+                                     mstring_make("/assets/skybox/clouds1/clouds1_west.bmp"),
+                                     mstring_make("/assets/skybox/clouds1/clouds1_up.bmp"),
+                                     mstring_make("/assets/skybox/clouds1/clouds1_down.bmp"),
+                                     mstring_make("/assets/skybox/clouds1/clouds1_north.bmp"),
+                                     mstring_make("/assets/skybox/clouds1/clouds1_south.bmp")}
+  });
 }
 
 /// Creates an object to render the skyquad in the background.
-static SceneNode* make_skyquad_object_node(Game* game,
-                                           const Texture* environment_map) {
+static SceneNode* make_skyquad_object_node(
+    Game* game, const Texture* environment_map) {
   assert(game);
 
   SceneObject* skyquad_object =
@@ -92,12 +93,12 @@ static SceneNode* make_skyquad_object_node(Game* game,
 }
 
 /// Creates an environment light.
-static SceneNode* make_environment_light(Game* game,
-                                         const Texture* environment_light) {
+static SceneNode* make_environment_light(
+    Game* game, const Texture* environment_light) {
   assert(game);
 
   Light* light = gfx_make_light(&(LightDesc){
-      .type = EnvironmentLightType,
+      .type  = EnvironmentLightType,
       .light = (EnvironmentLightDesc){.environment_map = environment_light}});
   if (!light) {
     return 0;
@@ -127,8 +128,8 @@ static bool load_skyquad(Game* game, SceneNode** node) {
 }
 
 /// Loads the 3D scene.
-static bool load_scene(Game* game, const char* scene_filepath,
-                       const char* view_mode) {
+static bool load_scene(
+    Game* game, const char* scene_filepath, const char* view_mode) {
   assert(game);
 
   game->camera = gfx_make_camera();
@@ -151,9 +152,10 @@ static bool load_scene(Game* game, const char* scene_filepath,
     return false;
   }
 
-  if (!gfx_load_scene(game->gfx, sky_node, shader,
-                      &(LoadSceneCmd){.origin = SceneFromFile,
-                                      .filepath = scene_filepath})) {
+  if (!gfx_load_scene(
+          game->gfx, sky_node, shader,
+          &(LoadSceneCmd){
+              .origin = SceneFromFile, .filepath = scene_filepath})) {
     return false;
   }
 
@@ -164,14 +166,14 @@ static bool load_scene(Game* game, const char* scene_filepath,
 static bool load_texture_debugger_scene(Game* game) {
   assert(game);
 
-  Texture* texture =
-      gfx_load_texture(game->render_backend,
-                       &(LoadTextureCmd){.origin = TextureFromFile,
-                                         .type = LoadTexture,
-                                         .filtering = LinearFiltering,
-                                         .mipmaps = false,
-                                         .data.texture.filepath =
-                                             mstring_make(CLOUDS1_TEXTURE)});
+  Texture* texture = gfx_load_texture(
+      game->render_backend,
+      &(LoadTextureCmd){
+          .origin                = TextureFromFile,
+          .type                  = LoadTexture,
+          .filtering             = LinearFiltering,
+          .mipmaps               = false,
+          .data.texture.filepath = mstring_make(CLOUDS1_TEXTURE)});
 
   game->camera = gfx_make_camera();
   if (!game->camera) {
@@ -191,12 +193,12 @@ static bool load_texture_debugger_scene(Game* game) {
   }
 
   MaterialDesc material_desc = (MaterialDesc){0};
-  material_desc.shader = shader;
-  material_desc.uniforms[0] = (ShaderUniform){.type = UniformTexture,
-                                              .value.texture = texture,
-                                              .name = sstring_make("Texture")};
+  material_desc.uniforms[0]  = (ShaderUniform){
+       .type          = UniformTexture,
+       .value.texture = texture,
+       .name          = sstring_make("Texture")};
   material_desc.num_uniforms = 1;
-  Material* material = gfx_make_material(&material_desc);
+  Material* material         = gfx_make_material(&material_desc);
   if (!material) {
     return false;
   }
@@ -204,7 +206,8 @@ static bool load_texture_debugger_scene(Game* game) {
   MeshDesc mesh_desc = (MeshDesc){0};
   mesh_desc.geometry = geometry;
   mesh_desc.material = material;
-  Mesh* mesh = gfx_make_mesh(&mesh_desc);
+  mesh_desc.shader   = shader;
+  Mesh* mesh         = gfx_make_mesh(&mesh_desc);
   if (!mesh) {
     return false;
   }
@@ -224,7 +227,7 @@ static bool load_texture_debugger_scene(Game* game) {
 
 bool game_new(Game* game, int argc, const char** argv) {
   // TODO: getopt() to implement proper argument parsing.
-  const char* view_mode = argc > 1 ? argv[1] : "";
+  const char* view_mode      = argc > 1 ? argv[1] : "";
   const char* scene_filepath = argc > 2 ? argv[2] : DEFAULT_SCENE_FILE;
 
   game->gfx = gfx_init();
@@ -233,7 +236,7 @@ bool game_new(Game* game, int argc, const char** argv) {
   }
 
   game->render_backend = gfx_get_render_backend(game->gfx);
-  game->renderer = gfx_get_renderer(game->gfx);
+  game->renderer       = gfx_get_renderer(game->gfx);
 
   game->scene = gfx_make_scene(game->gfx);
   if (!game->scene) {
@@ -270,26 +273,27 @@ void game_update(Game* game, double t, double dt) {
     game->elapsed -= 1.0;
   }
   Camera* camera = gfx_get_camera_camera(game->camera);
-  spatial3_orbit(&camera->spatial, vec3_make(0, 0, 0),
-                 /*radius=*/2,
-                 /*azimuth=*/t * 0.5, /*zenith=*/0);
+  spatial3_orbit(
+      &camera->spatial, vec3_make(0, 0, 0),
+      /*radius=*/2,
+      /*azimuth=*/t * 0.5, /*zenith=*/0);
   spatial3_lookat(&camera->spatial, vec3_make(0, 0, 0));
 }
 
 void game_render(const Game* game) {
-  gfx_render_scene(game->renderer, game->render_backend, game->scene,
-                   game->camera);
+  gfx_render_scene(
+      game->renderer, game->render_backend, game->scene, game->camera);
 }
 
 void game_set_viewport(Game* game, int width, int height) {
   gfx_set_viewport(game->render_backend, width, height);
 
-  const R fovy = 90 * TO_RAD;
-  const R aspect = (R)width / (R)height;
-  const R near = 0.1;
-  const R far = 1000;
+  const R    fovy       = 90 * TO_RAD;
+  const R    aspect     = (R)width / (R)height;
+  const R    near       = 0.1;
+  const R    far        = 1000;
   const mat4 projection = mat4_perspective(fovy, aspect, near, far);
 
-  Camera* camera = gfx_get_camera_camera(game->camera);
+  Camera* camera     = gfx_get_camera_camera(game->camera);
   camera->projection = projection;
 }
-- 
cgit v1.2.3