#ifndef _VULKAN_GAME_H #define _VULKAN_GAME_H #include #include #include #include #include #include #include #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 #include #include "IMGUI/imgui_impl_vulkan.h" #include "consts.hpp" #include "graphics-pipeline_vulkan.hpp" #include "game-gui-sdl.hpp" #include "utils.hpp" #include "vulkan-utils.hpp" using namespace glm; using namespace std::chrono; #ifdef NDEBUG const bool ENABLE_VALIDATION_LAYERS = false; #else const bool ENABLE_VALIDATION_LAYERS = true; #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 OverlayVertex { vec3 pos; vec2 texCoord; }; 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 SSBO_Laser { alignas(16) mat4 model; alignas(4) vec3 color; alignas(4) unsigned int deleted; }; struct SSBO_Explosion { alignas(16) mat4 model; alignas(4) float explosionStartTime; alignas(4) float explosionDuration; alignas(4) unsigned int deleted; }; struct UBO_VP_mats { alignas(16) mat4 view; alignas(16) mat4 proj; }; struct UBO_Explosion { alignas(16) mat4 view; alignas(16) mat4 proj; alignas(4) float cur_time; }; // 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 struct SceneObject { vector vertices; vector 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* 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 struct BaseEffectOverTime { bool deleted; virtual void applyEffect(float curTime) = 0; BaseEffectOverTime() : deleted(false) { } virtual ~BaseEffectOverTime() { } }; template struct EffectOverTime : public BaseEffectOverTime { GraphicsPipeline_Vulkan& pipeline; vector>& objects; unsigned int objectIndex; size_t effectedFieldOffset; float startValue; float startTime; float changePerSecond; EffectOverTime(GraphicsPipeline_Vulkan& pipeline, vector>& objects, unsigned int objectIndex, size_t effectedFieldOffset, float startTime, float changePerSecond) : pipeline(pipeline) , objects(objects) , objectIndex(objectIndex) , effectedFieldOffset(effectedFieldOffset) , startTime(startTime) , changePerSecond(changePerSecond) { size_t ssboOffset = offset_of(&SceneObject::ssbo); unsigned char* effectedFieldPtr = reinterpret_cast(&objects[objectIndex]) + ssboOffset + effectedFieldOffset; startValue = *reinterpret_cast(effectedFieldPtr); } void applyEffect(float curTime) { if (objects[objectIndex].ssbo.deleted) { this->deleted = true; return; } size_t ssboOffset = offset_of(&SceneObject::ssbo); unsigned char* effectedFieldPtr = reinterpret_cast(&objects[objectIndex]) + ssboOffset + effectedFieldOffset; *reinterpret_cast(effectedFieldPtr) = startValue + (curTime - startTime) * changePerSecond; objects[objectIndex].modified = true; } }; // TODO: Maybe move this to a different header enum UIValueType { UIVALUE_INT, UIVALUE_DOUBLE, }; struct UIValue { UIValueType type; string label; void* value; UIValue(UIValueType _type, string _label, void* _value) : type(_type), label(_label), value(_value) {} }; /* TODO: The following syntax (note the const keyword) means the function will not modify * its params. I should use this where appropriate * * [return-type] [func-name](params...) const { ... } */ class VulkanGame { public: VulkanGame(); ~VulkanGame(); void run(int width, int height, unsigned char guiFlags); private: static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback( VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, VkDebugUtilsMessageTypeFlagsEXT messageType, const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, 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 const int EXPLOSION_PARTICLE_COUNT = 300; const vec3 LASER_COLOR = vec3(0.2f, 1.0f, 0.2f); bool done; vec3 cam_pos; // TODO: Good place to start using smart pointers GameGui* gui; SDL_version sdlVersion; SDL_Window* window = nullptr; int drawableWidth, drawableHeight; VkInstance instance; VkDebugUtilsMessengerEXT debugMessenger; VkSurfaceKHR vulkanSurface; VkPhysicalDevice physicalDevice = VK_NULL_HANDLE; VkDevice device; VkQueue graphicsQueue; VkQueue presentQueue; // TODO: Maybe make a swapchain struct for convenience VkSurfaceFormatKHR swapChainSurfaceFormat; VkPresentModeKHR swapChainPresentMode; VkExtent2D swapChainExtent; uint32_t swapChainMinImageCount; uint32_t swapChainImageCount; VkSwapchainKHR swapChain; vector swapChainImages; vector swapChainImageViews; vector swapChainFramebuffers; VkRenderPass renderPass; VkCommandPool resourceCommandPool; vector commandPools; vector commandBuffers; VulkanImage depthImage; // These are per frame vector imageAcquiredSemaphores; vector renderCompleteSemaphores; // These are per swap chain image vector inFlightFences; uint32_t imageIndex; uint32_t currentFrame; bool shouldRecreateSwapChain; VkSampler textureSampler; VulkanImage floorTextureImage; VkDescriptorImageInfo floorTextureImageDescriptor; VulkanImage laserTextureImage; VkDescriptorImageInfo laserTextureImageDescriptor; 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 modesl 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 modelPipeline; GraphicsPipeline_Vulkan shipPipeline; GraphicsPipeline_Vulkan asteroidPipeline; GraphicsPipeline_Vulkan laserPipeline; GraphicsPipeline_Vulkan explosionPipeline; // 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> modelObjects; vector uniformBuffers_modelPipeline; vector uniformBuffersMemory_modelPipeline; vector uniformBufferInfoList_modelPipeline; UBO_VP_mats object_VP_mats; vector> shipObjects; vector uniformBuffers_shipPipeline; vector uniformBuffersMemory_shipPipeline; vector uniformBufferInfoList_shipPipeline; UBO_VP_mats ship_VP_mats; vector> asteroidObjects; vector uniformBuffers_asteroidPipeline; vector uniformBuffersMemory_asteroidPipeline; vector uniformBufferInfoList_asteroidPipeline; UBO_VP_mats asteroid_VP_mats; vector> laserObjects; vector uniformBuffers_laserPipeline; vector uniformBuffersMemory_laserPipeline; vector uniformBufferInfoList_laserPipeline; UBO_VP_mats laser_VP_mats; vector> explosionObjects; vector uniformBuffers_explosionPipeline; vector uniformBuffersMemory_explosionPipeline; vector uniformBufferInfoList_explosionPipeline; UBO_Explosion explosion_UBO; vector effects; float shipSpeed = 0.5f; float asteroidSpeed = 2.0f; float spawnRate_asteroid = 0.5; float lastSpawn_asteroid; unsigned int leftLaserIdx = -1; EffectOverTime* leftLaserEffect = nullptr; unsigned int rightLaserIdx = -1; EffectOverTime* rightLaserEffect = nullptr; /*** High-level vars ***/ // TODO: Just typedef the type of this function to RenderScreenFn or something since it's used in a few places void (VulkanGame::* currentRenderScreenFn)(int width, int height); map> valueLists; int score; float fps; // TODO: Make a separate TImer class time_point startTime; float fpsStartTime, curTime, prevTime, elapsedTime; int frameCount; /*** Functions ***/ bool initUI(int width, int height, unsigned char guiFlags); void initVulkan(); void initGraphicsPipelines(); void initMatrices(); void renderLoop(); void updateScene(); void cleanup(); void createVulkanInstance(const vector& validationLayers); void setupDebugMessenger(); void populateDebugMessengerCreateInfo(VkDebugUtilsMessengerCreateInfoEXT& createInfo); void createVulkanSurface(); void pickPhysicalDevice(const vector& deviceExtensions); bool isDeviceSuitable(VkPhysicalDevice physicalDevice, const vector& deviceExtensions); void createLogicalDevice(const vector& validationLayers, const vector& deviceExtensions); void chooseSwapChainProperties(); void createSwapChain(); void createImageViews(); void createResourceCommandPool(); void createImageResources(); VkFormat findDepthFormat(); // TODO: Declare/define (in the cpp file) this function in some util functions section void createRenderPass(); void createCommandPools(); void createFramebuffers(); void createCommandBuffers(); void createSyncObjects(); void createTextureSampler(); void initImGuiOverlay(); void cleanupImGuiOverlay(); // TODO: Maybe move these to a different class, possibly VulkanBuffer or some new related class void createBufferSet(VkDeviceSize bufferSize, VkBufferUsageFlags flags, VkMemoryPropertyFlags properties, vector& buffers, vector& buffersMemory, vector& bufferInfoList); // TODO: See if it makes sense to rename this to resizeBufferSet() and use it to resize other types of buffers as well // TODO: Remove the need for templating, which is only there so a GraphicsPupeline_Vulkan can be passed in template void resizeStorageBufferSet(StorageBufferSet& set, VkCommandPool commandPool, VkQueue graphicsQueue, GraphicsPipeline_Vulkan& pipeline); template void updateStorageBuffer(StorageBufferSet& storageBufferSet, size_t objIndex, SSBOType& ssbo); // TODO: Since addObject() returns a reference to the new object now, // stop using objects.back() to access the object that was just created template SceneObject& addObject(vector>& objects, GraphicsPipeline_Vulkan& pipeline, const vector& vertices, vector indices, SSBOType ssbo, bool pipelinesCreated); template vector addObjectIndex(unsigned int objIndex, vector vertices); template vector addVertexNormals(vector vertices); template void centerObject(SceneObject& object); template void updateObject(vector>& objects, GraphicsPipeline_Vulkan& pipeline, size_t index); template void updateObjectVertices(GraphicsPipeline_Vulkan& pipeline, SceneObject& obj, size_t index); void addLaser(vec3 start, vec3 end, vec3 color, float width); void translateLaser(size_t index, const vec3& translation); void updateLaserTarget(size_t index); bool getLaserAndAsteroidIntersection(SceneObject& asteroid, vec3& start, vec3& end, vec3& intersection); void addExplosion(mat4 model_mat, float duration, float cur_time); void renderFrame(ImDrawData* draw_data); void presentFrame(); void recreateSwapChain(); void cleanupSwapChain(); /*** High-level functions ***/ void renderMainScreen(int width, int height); void renderGameScreen(int width, int height); void initGuiValueLists(map>& valueLists); void renderGuiValueList(vector& values); void goToScreen(void (VulkanGame::* renderScreenFn)(int width, int height)); void quitGame(); }; // Start of specialized no-op functions template<> inline void VulkanGame::centerObject(SceneObject& object) { } // End of specialized no-op functions template void VulkanGame::resizeStorageBufferSet(StorageBufferSet& set, VkCommandPool commandPool, VkQueue graphicsQueue, GraphicsPipeline_Vulkan& pipeline) { pipeline.objectCapacity *= 2; VkDeviceSize bufferSize = pipeline.objectCapacity * sizeof(SSBOType); for (size_t i = 0; i < set.buffers.size(); i++) { VkBuffer newStorageBuffer; VkDeviceMemory newStorageBufferMemory; VulkanUtils::createBuffer(device, physicalDevice, bufferSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, newStorageBuffer, newStorageBufferMemory); VulkanUtils::copyBuffer(device, commandPool, set.buffers[i], newStorageBuffer, 0, 0, pipeline.numObjects * sizeof(SSBOType), graphicsQueue); vkDestroyBuffer(device, set.buffers[i], nullptr); vkFreeMemory(device, set.memory[i], nullptr); set.buffers[i] = newStorageBuffer; set.memory[i] = newStorageBufferMemory; set.infoSet[i].buffer = set.buffers[i]; set.infoSet[i].offset = 0; // This is the offset from the start of the buffer, so always 0 for now set.infoSet[i].range = bufferSize; // Size of the update starting from offset, or VK_WHOLE_SIZE } } // TODO: See if it makes sense to pass in the current swapchain index instead of updating all of them template void VulkanGame::updateStorageBuffer(StorageBufferSet& storageBufferSet, size_t objIndex, SSBOType& ssbo) { for (size_t i = 0; i < storageBufferSet.memory.size(); i++) { VulkanUtils::copyDataToMemory(device, ssbo, storageBufferSet.memory[i], objIndex * sizeof(SSBOType)); } } // 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 object 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 SceneObject& VulkanGame::addObject(vector>& objects, GraphicsPipeline_Vulkan& pipeline, const vector& vertices, vector 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& 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 && !is_same_v) { centerObject(obj); } pipeline.addObject(obj.vertices, obj.indices, resourceCommandPool, graphicsQueue); bool resizeStorageBuffer = pipeline.numObjects == pipeline.objectCapacity; if (resizeStorageBuffer) { resizeStorageBufferSet(pipeline.storageBufferSet, resourceCommandPool, graphicsQueue, pipeline); pipeline.cleanup(); // Assume the SSBO is always the 2nd binding pipeline.updateDescriptorInfo(1, &pipeline.storageBufferSet.infoSet); } pipeline.numObjects++; updateStorageBuffer(pipeline.storageBufferSet, pipeline.numObjects - 1, obj.ssbo); // TODO: Figure out why I am destroying and recreating the ubos when the swap chain is recreated, // but am reusing the same ssbos. Maybe I don't need to recreate the ubos. 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 // TODO: Also, verify if I actually need to recreate all of these, or maybe just the descriptor sets, for instance if (resizeStorageBuffer) { pipeline.createPipeline(pipeline.vertShaderFile, pipeline.fragShaderFile); pipeline.createDescriptorPool(swapChainImages); pipeline.createDescriptorSets(swapChainImages); } createCommandBuffers(); } return obj; } template vector VulkanGame::addObjectIndex(unsigned int objIndex, vector vertices) { for (VertexType& vertex : vertices) { vertex.objIndex = objIndex; } return vertices; } // This function sets all the normals for a face to be parallel // This is good for models that should have distinct faces, but bad for models that should appear smooth // Maybe add an option to set all copies of a point to have the same normal and have the direction of // that normal be the weighted average of all the faces it is a part of, where the weight from each face // is its surface area. // TODO: Since the current approach to normal calculation basicaly makes indexed drawing useless, see if it's // feasible to automatically enable/disable indexed drawing based on which approach is used template vector VulkanGame::addVertexNormals(vector 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 void VulkanGame::centerObject(SceneObject& object) { vector& 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 void VulkanGame::updateObject(vector>& objects, GraphicsPipeline_Vulkan& pipeline, size_t index) { SceneObject& 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)); updateStorageBuffer(pipeline.storageBufferSet, index, obj.ssbo); obj.modified = false; } template void VulkanGame::updateObjectVertices(GraphicsPipeline_Vulkan& pipeline, SceneObject& obj, size_t index) { pipeline.updateObjectVertices(index, obj.vertices, resourceCommandPool, graphicsQueue); } #endif // _VULKAN_GAME_H