Index: vulkan-game.cpp
===================================================================
--- vulkan-game.cpp	(revision 237cbec06d86b83bf2bf33cd9ca41842b69650bf)
+++ vulkan-game.cpp	(revision 1f81eccc69412a326f3861662b1688ee540440b2)
@@ -676,4 +676,26 @@
                      translate(mat4(1.0f), vec3(0.0f, 0.0f, zOffset));
                   texturedSquare.modified = true;
+               } else if (e.key.keycode == SDL_SCANCODE_Z && leftLaserIdx == -1) {
+                  // TODO: When I start actually removing objects from the object vectors,
+                  // I will need to update the indices since they might become incorrect
+                  // or invalid as objects get moved around
+
+                  vec3 offset(shipObjects[0].model_transform * vec4(0.0f, 0.0f, 0.0f, 1.0f));
+
+                  addLaser(
+                     vec3(-0.21f, -1.19f, 1.76f) + offset,
+                     vec3(-0.21f, -1.19f, -3.0f) + offset,
+                     vec3(1.0f, 0.0f, 0.0f), 0.03f);
+
+                  leftLaserIdx = laserObjects.size() - 1;
+               } else if (e.key.keycode == SDL_SCANCODE_X && rightLaserIdx == -1) {
+                  vec3 offset(shipObjects[0].model_transform * vec4(0.0f, 0.0f, 0.0f, 1.0f));
+
+                  addLaser(
+                     vec3(0.21f, -1.19f, 1.76f) + offset,
+                     vec3(0.21f, -1.19f, -3.0f) + offset,
+                     vec3(0.0f, 1.0f, 0.0f), 0.03f);
+
+                  rightLaserIdx = laserObjects.size() - 1;
                } else {
                   cout << "Key event detected" << endl;
@@ -681,4 +703,13 @@
                break;
             case UI_EVENT_KEYUP:
+               if (e.key.keycode == SDL_SCANCODE_Z && leftLaserIdx != -1) {
+                  laserObjects[leftLaserIdx].ssbo.deleted = true;
+                  laserObjects[leftLaserIdx].modified = true;
+                  leftLaserIdx = -1;
+               } else if (e.key.keycode == SDL_SCANCODE_X && rightLaserIdx != -1) {
+                  laserObjects[rightLaserIdx].ssbo.deleted = true;
+                  laserObjects[rightLaserIdx].modified = true;
+                  rightLaserIdx = -1;
+               }
                break;
             case UI_EVENT_MOUSEBUTTONDOWN:
@@ -708,4 +739,11 @@
             * shipObjects[0].model_transform;
          ship.modified = true;
+
+         if (leftLaserIdx != -1) {
+            translateLaser(leftLaserIdx, vec3(distance, 0.0f, 0.0f));
+         }
+         if (rightLaserIdx != -1) {
+            translateLaser(rightLaserIdx, vec3(distance, 0.0f, 0.0f));
+         }
       } else if (gui->keyPressed(SDL_SCANCODE_RIGHT)) {
          float distance = this->shipSpeed * this->elapsedTime;
@@ -714,4 +752,11 @@
             * shipObjects[0].model_transform;
          ship.modified = true;
+
+         if (leftLaserIdx != -1) {
+            translateLaser(leftLaserIdx, vec3(distance, 0.0f, 0.0f));
+         }
+         if (rightLaserIdx != -1) {
+            translateLaser(rightLaserIdx, vec3(distance, 0.0f, 0.0f));
+         }
       }
 
@@ -1496,4 +1541,86 @@
 }
 
+void VulkanGame::addLaser( vec3 start, vec3 end, vec3 color, float width) {
+   vec3 ray = end - start;
+   float length = glm::length(ray);
+
+   SceneObject<LaserVertex, SSBO_Laser>& laser = addObject(
+      laserObjects, laserPipeline,
+      addObjectIndex<LaserVertex>(laserObjects.size(), {
+         {{ width / 2, 0.0f, -width / 2         }, {1.0f, 0.5f }},
+         {{-width / 2, 0.0f, -width / 2         }, {0.0f, 0.5f }},
+         {{-width / 2, 0.0f, 0.0f               }, {0.0f, 0.0f }},
+         {{ width / 2, 0.0f, 0.0f               }, {1.0f, 0.0f }},
+        {{ width / 2, 0.0f, -length + width / 2}, {1.0f, 0.51f}},
+         {{-width / 2, 0.0f, -length + width / 2}, {0.0f, 0.51f}},
+         {{ width / 2, 0.0f, -length,           }, {1.0f, 1.0f }},
+         {{-width / 2, 0.0f, -length            }, {0.0f, 1.0f }}
+      }), {
+          0, 1, 2, 0, 2, 3,
+          4, 5, 1, 4, 1, 0,
+          6, 7, 5, 6, 5, 4
+      }, {
+         mat4(1.0f),
+         color,
+         false
+      }, true);
+
+   float xAxisRotation = asin(ray.y / length);
+   float yAxisRotation = atan2(-ray.x, -ray.z);
+
+   vec3 normal(rotate(mat4(1.0f), yAxisRotation, vec3(0.0f, 1.0f, 0.0f)) *
+            rotate(mat4(1.0f), xAxisRotation, vec3(1.0f, 0.0f, 0.0f)) *
+            vec4(0.0f, 1.0f, 0.0f, 1.0f));
+
+   // To project point P onto line AB:
+   // projection = A + dot(AP,AB) / dot(AB,AB) * AB
+   vec3 projOnLaser = start + glm::dot(this->cam_pos - start, ray) / (length * length) * ray;
+   vec3 laserToCam = this->cam_pos - projOnLaser;
+
+   float zAxisRotation = -atan2(glm::dot(glm::cross(normal, laserToCam), glm::normalize(ray)), glm::dot(normal, laserToCam));
+
+   laser.model_base =
+      rotate(mat4(1.0f), zAxisRotation, vec3(0.0f, 0.0f, 1.0f));
+
+   laser.model_transform =
+      translate(mat4(1.0f), start) *
+      rotate(mat4(1.0f), yAxisRotation, vec3(0.0f, 1.0f, 0.0f)) *
+      rotate(mat4(1.0f), xAxisRotation, vec3(1.0f, 0.0f, 0.0f));
+
+   laser.modified = true;
+}
+
+void VulkanGame::translateLaser(size_t index, const vec3& translation) {
+   SceneObject<LaserVertex, SSBO_Laser>& laser = this->laserObjects[index];
+
+   // TODO: A lot of the values calculated here can be calculated once and saved when the laser is created,
+   // and then re-used here
+
+   vec3 start = vec3(laser.model_transform * vec4(0.0f, 0.0f, 0.0f, 1.0f));
+   vec3 end = vec3(laser.model_transform * vec4(0.0f, 0.0f, laser.vertices[6].pos.z, 1.0f));
+
+   vec3 ray = end - start;
+   float length = glm::length(ray);
+
+   float xAxisRotation = asin(ray.y / length);
+   float yAxisRotation = atan2(-ray.x, -ray.z);
+
+   vec3 normal(rotate(mat4(1.0f), yAxisRotation, vec3(0.0f, 1.0f, 0.0f)) *
+      rotate(mat4(1.0f), xAxisRotation, vec3(1.0f, 0.0f, 0.0f)) *
+      vec4(0.0f, 1.0f, 0.0f, 1.0f));
+
+   // To project point P onto line AB:
+   // projection = A + dot(AP,AB) / dot(AB,AB) * AB
+   vec3 projOnLaser = start + glm::dot(cam_pos - start, ray) / (length*length) * ray;
+   vec3 laserToCam = cam_pos - projOnLaser;
+
+   float zAxisRotation = -atan2(glm::dot(glm::cross(normal, laserToCam), glm::normalize(ray)), glm::dot(normal, laserToCam));
+
+   laser.model_base = rotate(mat4(1.0f), zAxisRotation, vec3(0.0f, 0.0f, 1.0f));
+   laser.model_transform = translate(mat4(1.0f), translation) * laser.model_transform;
+
+   laser.modified = true;
+}
+
 void VulkanGame::createBufferSet(VkDeviceSize bufferSize, VkBufferUsageFlags flags,
       vector<VkBuffer>& buffers, vector<VkDeviceMemory>& buffersMemory, vector<VkDescriptorBufferInfo>& bufferInfoList) {
Index: vulkan-game.hpp
===================================================================
--- vulkan-game.hpp	(revision 237cbec06d86b83bf2bf33cd9ca41842b69650bf)
+++ vulkan-game.hpp	(revision 1f81eccc69412a326f3861662b1688ee540440b2)
@@ -233,4 +233,8 @@
       float spawnRate_asteroid = 0.5;
       float lastSpawn_asteroid;
+
+      unsigned int leftLaserIdx = -1;
+
+      unsigned int rightLaserIdx = -1;
 
       bool initWindow(int width, int height, unsigned char guiFlags);
@@ -265,4 +269,7 @@
       void createSyncObjects();
 
+      void addLaser(vec3 start, vec3 end, vec3 color, float width);
+      void translateLaser(size_t index, const vec3& translation);
+
       // TODO: Since addObject() returns a reference to the new object now,
       // stop using objects.back() to access the object that was just created
@@ -277,4 +284,8 @@
       void updateObject(vector<SceneObject<VertexType, SSBOType>>& objects,
             GraphicsPipeline_Vulkan<VertexType, SSBOType>& pipeline, size_t index);
+
+      template<class VertexType, class SSBOType>
+      void updateObjectVertices(GraphicsPipeline_Vulkan<VertexType, SSBOType>& pipeline,
+            SceneObject<VertexType, SSBOType>& obj, size_t index);
 
       template<class VertexType>
@@ -327,5 +338,8 @@
 
    SceneObject<VertexType, SSBOType>& obj = objects.back();
-   centerObject(obj);
+
+   if (!is_same_v<VertexType, LaserVertex>) {
+      centerObject(obj);
+   }
 
    bool storageBufferResized = pipeline.addObject(obj.vertices, obj.indices, obj.ssbo,
@@ -366,4 +380,10 @@
 }
 
+template<class VertexType, class SSBOType>
+void VulkanGame::updateObjectVertices(GraphicsPipeline_Vulkan<VertexType, SSBOType>& pipeline,
+      SceneObject<VertexType, SSBOType>& obj, size_t index) {
+   pipeline.updateObjectVertices(index, obj.vertices, this->commandPool, this->graphicsQueue);
+}
+
 template<class VertexType>
 vector<VertexType> VulkanGame::addVertexNormals(vector<VertexType> vertices) {
