Index: sdl-game.cpp
===================================================================
--- sdl-game.cpp	(revision b8efa561bb310d6d43bc6666ee16750afbcac2f8)
+++ sdl-game.cpp	(revision 4a777d2b3a0f0dff81e2a81bb45124f3c448bf47)
@@ -8,4 +8,5 @@
 
 #include "logger.hpp"
+#include "utils.hpp"
 
 #include "gui/imgui/button-imgui.hpp"
@@ -89,4 +90,71 @@
    initImGuiOverlay();
 
+   // TODO: Figure out how much of ubo creation and associated variables should be in the pipeline class
+   // Maybe combine the ubo-related objects into a new class
+
+   initGraphicsPipelines();
+
+   initMatrices();
+
+   modelPipeline.addAttribute(VK_FORMAT_R32G32B32_SFLOAT, offset_of(&ModelVertex::pos));
+   modelPipeline.addAttribute(VK_FORMAT_R32G32B32_SFLOAT, offset_of(&ModelVertex::color));
+   modelPipeline.addAttribute(VK_FORMAT_R32G32_SFLOAT, offset_of(&ModelVertex::texCoord));
+   modelPipeline.addAttribute(VK_FORMAT_R32G32B32_SFLOAT, offset_of(&ModelVertex::normal));
+   modelPipeline.addAttribute(VK_FORMAT_R32_UINT, offset_of(&ModelVertex::objIndex));
+
+   createBufferSet(sizeof(UBO_VP_mats), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
+      uniformBuffers_modelPipeline, uniformBuffersMemory_modelPipeline, uniformBufferInfoList_modelPipeline);
+
+   modelPipeline.addDescriptorInfo(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
+      VK_SHADER_STAGE_VERTEX_BIT, &uniformBufferInfoList_modelPipeline);
+   modelPipeline.addStorageDescriptor(VK_SHADER_STAGE_VERTEX_BIT);
+   modelPipeline.addDescriptorInfo(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
+      VK_SHADER_STAGE_FRAGMENT_BIT, &floorTextureImageDescriptor);
+
+   SceneObject<ModelVertex, SSBO_ModelObject>* texturedSquare = nullptr;
+
+   texturedSquare = &addObject(modelObjects, modelPipeline,
+      addObjectIndex<ModelVertex>(modelObjects.size(),
+         addVertexNormals<ModelVertex>({
+            {{-0.5f, -0.5f,  0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f}, {1.0f, 0.0f, 0.0f}, 0},
+            {{ 0.5f, -0.5f,  0.0f}, {0.0f, 1.0f, 0.0f}, {1.0f, 1.0f}, {1.0f, 0.0f, 0.0f}, 0},
+            {{ 0.5f,  0.5f,  0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, 0},
+            {{ 0.5f,  0.5f,  0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, 0},
+            {{-0.5f,  0.5f,  0.0f}, {1.0f, 1.0f, 1.0f}, {0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, 0},
+            {{-0.5f, -0.5f,  0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f}, {1.0f, 0.0f, 0.0f}, 0}
+         })), {
+            0, 1, 2, 3, 4, 5
+      }, {
+         mat4(1.0f)
+      }, false);
+
+   texturedSquare->model_base =
+      translate(mat4(1.0f), vec3(0.0f, 0.0f, -2.0f));
+   texturedSquare->modified = true;
+
+   texturedSquare = &addObject(modelObjects, modelPipeline,
+      addObjectIndex<ModelVertex>(modelObjects.size(),
+         addVertexNormals<ModelVertex>({
+            {{-0.5f, -0.5f,  0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f}},
+            {{ 0.5f, -0.5f,  0.0f}, {0.0f, 1.0f, 0.0f}, {1.0f, 1.0f}},
+            {{ 0.5f,  0.5f,  0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f}},
+            {{ 0.5f,  0.5f,  0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f}},
+            {{-0.5f,  0.5f,  0.0f}, {1.0f, 1.0f, 1.0f}, {0.0f, 0.0f}},
+            {{-0.5f, -0.5f,  0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f}}
+         })), {
+            0, 1, 2, 3, 4, 5
+      }, {
+         mat4(1.0f)
+      }, false);
+
+   texturedSquare->model_base =
+      translate(mat4(1.0f), vec3(0.0f, 0.0f, -1.5f));
+   texturedSquare->modified = true;
+
+   modelPipeline.createDescriptorSetLayout();
+   modelPipeline.createPipeline("shaders/model-vert.spv", "shaders/model-frag.spv");
+   modelPipeline.createDescriptorPool(swapChainImages);
+   modelPipeline.createDescriptorSets(swapChainImages);
+
    currentRenderScreenFn = &VulkanGame::renderMainScreen;
 
@@ -182,4 +250,31 @@
    createCommandBuffers();
    createSyncObjects();
+}
+
+void VulkanGame::initGraphicsPipelines() {
+   modelPipeline = GraphicsPipeline_Vulkan<ModelVertex, SSBO_ModelObject>(
+      VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, physicalDevice, device, renderPass,
+      { 0, 0, (int)swapChainExtent.width, (int)swapChainExtent.height }, swapChainImages, 16, 24, 10);
+}
+
+// TODO: Maybe change the name to initScene() or something similar
+void VulkanGame::initMatrices() {
+   cam_pos = vec3(0.0f, 0.0f, 2.0f);
+
+   float cam_yaw = 0.0f;
+   float cam_pitch = -50.0f;
+
+   mat4 yaw_mat = rotate(mat4(1.0f), radians(-cam_yaw), vec3(0.0f, 1.0f, 0.0f));
+   mat4 pitch_mat = rotate(mat4(1.0f), radians(-cam_pitch), vec3(1.0f, 0.0f, 0.0f));
+
+   mat4 R_view = pitch_mat * yaw_mat;
+   mat4 T_view = translate(mat4(1.0f), vec3(-cam_pos.x, -cam_pos.y, -cam_pos.z));
+   viewMat = R_view * T_view;
+
+   projMat = perspective(radians(FOV_ANGLE), (float)swapChainExtent.width / (float)swapChainExtent.height, NEAR_CLIP, FAR_CLIP);
+   projMat[1][1] *= -1; // flip the y-axis so that +y is up
+
+   object_VP_mats.view = viewMat;
+   object_VP_mats.proj = projMat;
 }
 
@@ -241,4 +336,40 @@
                cout << "Window resize event detected" << endl;
                shouldRecreateSwapChain = true;
+               break;
+            case UI_EVENT_KEYDOWN:
+               if (e.key.repeat) {
+                  break;
+               }
+
+               if (e.key.keycode == SDL_SCANCODE_ESCAPE) {
+                  done = true;
+               } else if (e.key.keycode == SDL_SCANCODE_SPACE) {
+                  cout << "Adding a plane" << endl;
+                  float zOffset = -2.0f + (0.5f * modelObjects.size());
+
+                  SceneObject<ModelVertex, SSBO_ModelObject>& texturedSquare =
+                     addObject(modelObjects, modelPipeline,
+                        addObjectIndex<ModelVertex>(modelObjects.size(),
+                           addVertexNormals<ModelVertex>({
+                              {{-0.5f, -0.5f,  0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f}, {1.0f, 0.0f, 0.0f}, 0},
+                              {{ 0.5f, -0.5f,  0.0f}, {0.0f, 1.0f, 0.0f}, {1.0f, 1.0f}, {1.0f, 0.0f, 0.0f}, 0},
+                              {{ 0.5f,  0.5f,  0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, 0},
+                              {{ 0.5f,  0.5f,  0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, 0},
+                              {{-0.5f,  0.5f,  0.0f}, {1.0f, 1.0f, 1.0f}, {0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, 0},
+                              {{-0.5f, -0.5f,  0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f}, {1.0f, 0.0f, 0.0f}, 0}
+                           })), {
+                              0, 1, 2, 3, 4, 5
+                        }, {
+                           mat4(1.0f)
+                        }, true);
+
+                  texturedSquare.model_base =
+                     translate(mat4(1.0f), vec3(0.0f, 0.0f, zOffset));
+                  texturedSquare.modified = true;
+               // START UNREVIEWED SECTION
+               // END UNREVIEWED SECTION
+               } else {
+                  cout << "Key event detected" << endl;
+               }
                break;
             case UI_EVENT_KEYUP:
@@ -277,4 +408,6 @@
       }
 
+      updateScene();
+
       // TODO: Move this into a renderImGuiOverlay() function
       ImGui_ImplVulkan_NewFrame();
@@ -296,4 +429,24 @@
 }
 
+// TODO: The only updates that need to happen once per Vulkan image are the SSBO ones,
+// which are already handled by updateObject(). Move this code to a different place,
+// where it will run just once per frame
+void VulkanGame::updateScene() {
+   for (SceneObject<ModelVertex, SSBO_ModelObject>& model : this->modelObjects) {
+      model.model_transform =
+         translate(mat4(1.0f), vec3(0.0f, -2.0f, -0.0f)) *
+         rotate(mat4(1.0f), curTime * radians(90.0f), vec3(0.0f, 0.0f, 1.0f));
+      model.modified = true;
+   }
+
+   for (size_t i = 0; i < modelObjects.size(); i++) {
+      if (modelObjects[i].modified) {
+         updateObject(modelObjects, modelPipeline, i);
+      }
+   }
+
+   VulkanUtils::copyDataToMemory(device, uniformBuffersMemory_modelPipeline[imageIndex], 0, object_VP_mats);
+}
+
 void VulkanGame::cleanup() {
    // FIXME: We could wait on the Queue if we had the queue in wd-> (otherwise VulkanH functions can't use globals)
@@ -304,4 +457,13 @@
 
    cleanupSwapChain();
+
+   VulkanUtils::destroyVulkanImage(device, floorTextureImage);
+   // START UNREVIEWED SECTION
+
+   vkDestroySampler(device, textureSampler, nullptr);
+
+   modelPipeline.cleanupBuffers();
+
+   // END UNREVIEWED SECTION
 
    vkDestroyCommandPool(device, resourceCommandPool, nullptr);
@@ -638,4 +800,16 @@
    VulkanUtils::createDepthImage(device, physicalDevice, resourceCommandPool, findDepthFormat(), swapChainExtent,
       depthImage, graphicsQueue);
+
+   createTextureSampler();
+
+   // TODO: Move all images/textures somewhere into the assets folder
+
+   VulkanUtils::createVulkanImageFromFile(device, physicalDevice, resourceCommandPool, "textures/texture.jpg",
+      floorTextureImage, graphicsQueue);
+
+   floorTextureImageDescriptor = {};
+   floorTextureImageDescriptor.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
+   floorTextureImageDescriptor.imageView = floorTextureImage.imageView;
+   floorTextureImageDescriptor.sampler = textureSampler;
 }
 
@@ -726,4 +900,29 @@
       }
    }
+}
+
+void VulkanGame::createTextureSampler() {
+   VkSamplerCreateInfo samplerInfo = {};
+   samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
+   samplerInfo.magFilter = VK_FILTER_LINEAR;
+   samplerInfo.minFilter = VK_FILTER_LINEAR;
+
+   samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
+   samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
+   samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
+
+   samplerInfo.anisotropyEnable = VK_TRUE;
+   samplerInfo.maxAnisotropy = 16;
+   samplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
+   samplerInfo.unnormalizedCoordinates = VK_FALSE;
+   samplerInfo.compareEnable = VK_FALSE;
+   samplerInfo.compareOp = VK_COMPARE_OP_ALWAYS;
+   samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
+   samplerInfo.mipLodBias = 0.0f;
+   samplerInfo.minLod = 0.0f;
+   samplerInfo.maxLod = 0.0f;
+
+   VKUTIL_CHECK_RESULT(vkCreateSampler(device, &samplerInfo, nullptr, &textureSampler),
+      "failed to create texture sampler!");
 }
 
@@ -881,4 +1080,22 @@
 }
 
+void VulkanGame::createBufferSet(VkDeviceSize bufferSize, VkBufferUsageFlags flags,
+                                 vector<VkBuffer>& buffers, vector<VkDeviceMemory>& buffersMemory,
+                                 vector<VkDescriptorBufferInfo>& bufferInfoList) {
+   buffers.resize(swapChainImageCount);
+   buffersMemory.resize(swapChainImageCount);
+   bufferInfoList.resize(swapChainImageCount);
+
+   for (size_t i = 0; i < swapChainImageCount; i++) {
+      VulkanUtils::createBuffer(device, physicalDevice, bufferSize, flags,
+         VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
+         buffers[i], buffersMemory[i]);
+
+      bufferInfoList[i].buffer = buffers[i];
+      bufferInfoList[i].offset = 0; // This is the offset from the start of the buffer, so always 0 for now
+      bufferInfoList[i].range = bufferSize; // Size of the update starting from offset, or VK_WHOLE_SIZE
+   }
+}
+
 void VulkanGame::renderFrame(ImDrawData* draw_data) {
    VkResult result = vkAcquireNextImageKHR(device, swapChain, numeric_limits<uint64_t>::max(),
@@ -926,4 +1143,13 @@
 
    vkCmdBeginRenderPass(commandBuffers[imageIndex], &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE);
+
+   // TODO: Find a more elegant, per-screen solution for this
+   if (currentRenderScreenFn == &VulkanGame::renderGameScreen) {
+      modelPipeline.createRenderCommands(commandBuffers[imageIndex], imageIndex);
+
+
+
+
+   }
 
    ImGui_ImplVulkan_RenderDrawData(draw_data, commandBuffers[imageIndex]);
@@ -999,5 +1225,15 @@
    createSyncObjects();
 
-   // TODO: Update pipelines here
+   // TODO: Move UBO creation/management into GraphicsPipeline_Vulkan, like I did with SSBOs
+   // TODO: Check if the shader stages and maybe some other properties of the pipeline can be re-used
+   // instead of recreated every time
+
+   createBufferSet(sizeof(UBO_VP_mats), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
+      uniformBuffers_modelPipeline, uniformBuffersMemory_modelPipeline, uniformBufferInfoList_modelPipeline);
+
+   modelPipeline.updateRenderPass(renderPass);
+   modelPipeline.createPipeline("shaders/model-vert.spv", "shaders/model-frag.spv");
+   modelPipeline.createDescriptorPool(swapChainImages);
+   modelPipeline.createDescriptorSets(swapChainImages);
 
    imageIndex = 0;
@@ -1014,4 +1250,11 @@
       vkFreeCommandBuffers(device, commandPools[i], 1, &commandBuffers[i]);
       vkDestroyCommandPool(device, commandPools[i], nullptr);
+   }
+
+   modelPipeline.cleanup();
+
+   for (size_t i = 0; i < uniformBuffers_modelPipeline.size(); i++) {
+      vkDestroyBuffer(device, uniformBuffers_modelPipeline[i], nullptr);
+      vkFreeMemory(device, uniformBuffersMemory_modelPipeline[i], nullptr);
    }
 
Index: sdl-game.hpp
===================================================================
--- sdl-game.hpp	(revision b8efa561bb310d6d43bc6666ee16750afbcac2f8)
+++ sdl-game.hpp	(revision 4a777d2b3a0f0dff81e2a81bb45124f3c448bf47)
@@ -10,11 +10,19 @@
 #include <SDL2/SDL.h>
 
+#define GLM_FORCE_RADIANS
+#define GLM_FORCE_DEPTH_ZERO_TO_ONE // Since, in Vulkan, the depth range is 0 to 1 instead of -1 to 1
+#define GLM_FORCE_RIGHT_HANDED
+
+#include <glm/glm.hpp>
+#include <glm/gtc/matrix_transform.hpp>
+
 #include "IMGUI/imgui_impl_vulkan.h"
 
 #include "consts.hpp"
 #include "vulkan-utils.hpp"
-
+#include "graphics-pipeline_vulkan.hpp"
 #include "game-gui-sdl.hpp"
 
+using namespace glm;
 using namespace std;
 using namespace std::chrono;
@@ -28,4 +36,71 @@
 #endif
 
+// TODO: Consider if there is a better way of dealing with all the vertex types and ssbo types, maybe
+// by consolidating some and trying to keep new ones to a minimum
+
+struct ModelVertex {
+   vec3 pos;
+   vec3 color;
+   vec2 texCoord;
+   vec3 normal;
+   unsigned int objIndex;
+};
+
+struct LaserVertex {
+   vec3 pos;
+   vec2 texCoord;
+   unsigned int objIndex;
+};
+
+struct ExplosionVertex {
+   vec3 particleStartVelocity;
+   float particleStartTime;
+   unsigned int objIndex;
+};
+
+struct SSBO_ModelObject {
+   alignas(16) mat4 model;
+};
+
+struct SSBO_Asteroid {
+   alignas(16) mat4 model;
+   alignas(4) float hp;
+   alignas(4) unsigned int deleted;
+};
+
+struct UBO_VP_mats {
+   alignas(16) mat4 view;
+   alignas(16) mat4 proj;
+};
+
+// TODO: Change the index type to uint32_t and check the Vulkan Tutorial loading model section as a reference
+// TODO: Create a typedef for index type so I can easily change uin16_t to something else later
+// TODO: Maybe create a typedef for each of the templated SceneObject types
+template<class VertexType, class SSBOType>
+struct SceneObject {
+   vector<VertexType> vertices;
+   vector<uint16_t> indices;
+   SSBOType ssbo;
+
+   mat4 model_base;
+   mat4 model_transform;
+
+   bool modified;
+
+   // TODO: Figure out if I should make child classes that have these fields instead of putting them in the
+   // parent class
+   vec3 center; // currently only matters for asteroids
+   float radius; // currently only matters for asteroids
+   SceneObject<ModelVertex, SSBO_Asteroid>* targetAsteroid; // currently only used for lasers
+};
+
+// TODO: Have to figure out how to include an optional ssbo parameter for each object
+// Could probably use the same approach to make indices optional
+// Figure out if there are sufficient use cases to make either of these optional or is it fine to make
+// them mamdatory
+
+
+// TODO: Look into using dynamic_cast to check types of SceneObject and EffectOverTime
+
 // TODO: Maybe move this to a different header
 
@@ -60,5 +135,12 @@
          void* pUserData);
 
+      // TODO: Maybe pass these in as parameters to some Camera class
+      const float NEAR_CLIP = 0.1f;
+      const float FAR_CLIP = 100.0f;
+      const float FOV_ANGLE = 67.0f; // means the camera lens goes from -33 deg to 33 deg
+
       bool done;
+
+      vec3 cam_pos;
 
       // TODO: Good place to start using smart pointers
@@ -110,6 +192,36 @@
       bool shouldRecreateSwapChain;
 
+      VkSampler textureSampler;
+
+      VulkanImage floorTextureImage;
+      VkDescriptorImageInfo floorTextureImageDescriptor;
+
+      mat4 viewMat, projMat;
+
       // Maybe at some point create an imgui pipeline class, but I don't think it makes sense right now
       VkDescriptorPool imguiDescriptorPool;
+
+      // TODO: Probably restructure the GraphicsPipeline_Vulkan class based on what I learned about descriptors and textures
+      // while working on graphics-library. Double-check exactly what this was and note it down here.
+      // Basically, I think the point was that if I have several models that all use the same shaders and, therefore,
+      // the same pipeline, but use different textures, the approach I took when initially creating GraphicsPipeline_Vulkan
+      // wouldn't work since the whole pipeline couldn't have a common set of descriptors for the textures
+      GraphicsPipeline_Vulkan<ModelVertex, SSBO_ModelObject> modelPipeline;
+
+      // TODO: Maybe make the ubo objects part of the pipeline class since there's only one ubo
+      // per pipeline.
+      // Or maybe create a higher level wrapper around GraphicsPipeline_Vulkan to hold things like
+      // the objects vector, the ubo, and the ssbo
+
+      // TODO: Rename *_VP_mats to *_uniforms and possibly use different types for each one
+      // if there is a need to add other uniform variables to one or more of the shaders
+
+      vector<SceneObject<ModelVertex, SSBO_ModelObject>> modelObjects;
+
+      vector<VkBuffer> uniformBuffers_modelPipeline;
+      vector<VkDeviceMemory> uniformBuffersMemory_modelPipeline;
+      vector<VkDescriptorBufferInfo> uniformBufferInfoList_modelPipeline;
+
+      UBO_VP_mats object_VP_mats;
 
       /*** High-level vars ***/
@@ -133,5 +245,8 @@
       bool initUI(int width, int height, unsigned char guiFlags);
       void initVulkan();
+      void initGraphicsPipelines();
+      void initMatrices();
       void renderLoop();
+      void updateScene();
       void cleanup();
 
@@ -156,7 +271,35 @@
       void createSyncObjects();
 
+      void createTextureSampler();
+
       void initImGuiOverlay();
       void cleanupImGuiOverlay();
 
+      void createBufferSet(VkDeviceSize bufferSize, VkBufferUsageFlags flags,
+         vector<VkBuffer>& buffers, vector<VkDeviceMemory>& buffersMemory,
+         vector<VkDescriptorBufferInfo>& bufferInfoList);
+
+      // TODO: Since addObject() returns a reference to the new object now,
+      // stop using objects.back() to access the object that was just created
+      template<class VertexType, class SSBOType>
+      SceneObject<VertexType, SSBOType>& addObject(
+         vector<SceneObject<VertexType, SSBOType>>& objects,
+         GraphicsPipeline_Vulkan<VertexType, SSBOType>& pipeline,
+         const vector<VertexType>& vertices, vector<uint16_t> indices, SSBOType ssbo,
+         bool pipelinesCreated);
+
+      template<class VertexType>
+      vector<VertexType> addObjectIndex(unsigned int objIndex, vector<VertexType> vertices);
+
+      template<class VertexType>
+      vector<VertexType> addVertexNormals(vector<VertexType> vertices);
+
+      template<class VertexType, class SSBOType>
+      void centerObject(SceneObject<VertexType, SSBOType>& object);
+
+      template<class VertexType, class SSBOType>
+      void updateObject(vector<SceneObject<VertexType, SSBOType>>& objects,
+         GraphicsPipeline_Vulkan<VertexType, SSBOType>& pipeline, size_t index);
+
       void renderFrame(ImDrawData* draw_data);
       void presentFrame();
@@ -178,3 +321,152 @@
 };
 
+// TODO: Right now, it's basically necessary to pass the identity matrix in for ssbo.model
+// and to change the model matrix later by setting model_transform and then calling updateObject()
+// Figure out a better way to allow the model matrix to be set during objecting creation
+
+// TODO: Maybe return a reference to the object from this method if I decide that updating it
+// immediately after creation is a good idea (such as setting model_base)
+// Currently, model_base is set like this in a few places and the radius is set for asteroids
+// to account for scaling
+template<class VertexType, class SSBOType>
+SceneObject<VertexType, SSBOType>& VulkanGame::addObject(
+   vector<SceneObject<VertexType, SSBOType>>& objects,
+   GraphicsPipeline_Vulkan<VertexType, SSBOType>& pipeline,
+   const vector<VertexType>& vertices, vector<uint16_t> indices, SSBOType ssbo,
+   bool pipelinesCreated) {
+   // TODO: Use the model field of ssbo to set the object's model_base
+   // currently, the passed in model is useless since it gets overridden in updateObject() anyway
+   size_t numVertices = pipeline.getNumVertices();
+
+   for (uint16_t& idx : indices) {
+      idx += numVertices;
+   }
+
+   objects.push_back({ vertices, indices, ssbo, mat4(1.0f), mat4(1.0f), false });
+
+   SceneObject<VertexType, SSBOType>& obj = objects.back();
+
+   // TODO: Specify whether to center the object outside of this function or, worst case, maybe
+   // with a boolean being passed in here, so that I don't have to rely on checking the specific object
+   // type
+   if (!is_same_v<VertexType, LaserVertex> && !is_same_v<VertexType, ExplosionVertex>) {
+      centerObject(obj);
+   }
+
+   bool storageBufferResized = pipeline.addObject(obj.vertices, obj.indices, obj.ssbo,
+      resourceCommandPool, graphicsQueue);
+
+   if (pipelinesCreated) {
+      vkDeviceWaitIdle(device);
+
+      for (uint32_t i = 0; i < swapChainImageCount; i++) {
+         vkFreeCommandBuffers(device, commandPools[i], 1, &commandBuffers[i]);
+      }
+
+      // TODO: The pipeline recreation only has to be done once per frame where at least
+      // one SSBO is resized.
+      // Refactor the logic to check for any resized SSBOs after all objects for the frame
+      // are created and then recreate each of the corresponding pipelines only once per frame
+      if (storageBufferResized) {
+         pipeline.createPipeline(pipeline.vertShaderFile, pipeline.fragShaderFile);
+         pipeline.createDescriptorPool(swapChainImages);
+         pipeline.createDescriptorSets(swapChainImages);
+      }
+
+      createCommandBuffers();
+   }
+
+   return obj;
+}
+
+template<class VertexType>
+vector<VertexType> VulkanGame::addObjectIndex(unsigned int objIndex, vector<VertexType> vertices) {
+   for (VertexType& vertex : vertices) {
+      vertex.objIndex = objIndex;
+   }
+
+   return vertices;
+}
+
+template<class VertexType>
+vector<VertexType> VulkanGame::addVertexNormals(vector<VertexType> vertices) {
+   for (unsigned int i = 0; i < vertices.size(); i += 3) {
+      vec3 p1 = vertices[i].pos;
+      vec3 p2 = vertices[i + 1].pos;
+      vec3 p3 = vertices[i + 2].pos;
+
+      vec3 normal = normalize(cross(p2 - p1, p3 - p1));
+
+      // Add the same normal for all 3 vertices
+      vertices[i].normal = normal;
+      vertices[i + 1].normal = normal;
+      vertices[i + 2].normal = normal;
+   }
+
+   return vertices;
+}
+
+template<class VertexType, class SSBOType>
+void VulkanGame::centerObject(SceneObject<VertexType, SSBOType>& object) {
+   vector<VertexType>& vertices = object.vertices;
+
+   float min_x = vertices[0].pos.x;
+   float max_x = vertices[0].pos.x;
+   float min_y = vertices[0].pos.y;
+   float max_y = vertices[0].pos.y;
+   float min_z = vertices[0].pos.z;
+   float max_z = vertices[0].pos.z;
+
+   // start from the second point
+   for (unsigned int i = 1; i < vertices.size(); i++) {
+      vec3& pos = vertices[i].pos;
+
+      if (min_x > pos.x) {
+         min_x = pos.x;
+      }
+      else if (max_x < pos.x) {
+         max_x = pos.x;
+      }
+
+      if (min_y > pos.y) {
+         min_y = pos.y;
+      }
+      else if (max_y < pos.y) {
+         max_y = pos.y;
+      }
+
+      if (min_z > pos.z) {
+         min_z = pos.z;
+      }
+      else if (max_z < pos.z) {
+         max_z = pos.z;
+      }
+   }
+
+   vec3 center = vec3(min_x + max_x, min_y + max_y, min_z + max_z) / 2.0f;
+
+   for (unsigned int i = 0; i < vertices.size(); i++) {
+      vertices[i].pos -= center;
+   }
+
+   object.radius = std::max(max_x - center.x, max_y - center.y);
+   object.radius = std::max(object.radius, max_z - center.z);
+
+   object.center = vec3(0.0f, 0.0f, 0.0f);
+}
+
+// TODO: Just pass in the single object instead of a list of all of them
+template<class VertexType, class SSBOType>
+void VulkanGame::updateObject(vector<SceneObject<VertexType, SSBOType>>& objects,
+   GraphicsPipeline_Vulkan<VertexType, SSBOType>& pipeline, size_t index) {
+   SceneObject<VertexType, SSBOType>& obj = objects[index];
+
+   obj.ssbo.model = obj.model_transform * obj.model_base;
+   obj.center = vec3(obj.ssbo.model * vec4(0.0f, 0.0f, 0.0f, 1.0f));
+
+   pipeline.updateObject(index, obj.ssbo);
+
+   obj.modified = false;
+}
+
 #endif // _SDL_GAME_H
