diff --git a/INSTRUCTION.md b/INSTRUCTION.md index 455c0f9..d35249a 100644 --- a/INSTRUCTION.md +++ b/INSTRUCTION.md @@ -6,7 +6,7 @@ This is due **Tuesday 10/29 at 11:59pm**. ## Summary -In this project, you will use Vulkan to implement a grass simulator and renderer. You will use compute shaders to perform physics calculations on Bezier curves that represent individual grass blades in your application. Since rendering every grass blade on every frame will is fairly inefficient, you will also use compute shaders to cull grass blades that don't contribute to a given frame. The remaining blades will be passed to a graphics pipeline, in which you will write several shaders. You will write a vertex shader to transform Bezier control points, tessellation shaders to dynamically create the grass geometry from the Bezier curves, and a fragment shader to shade the grass blades. +In this project, you will use Vulkan to implement a grass simulator and renderer. You will use compute shaders to perform physics calculations on Bezier curves that represent individual grass blades in your application. Since rendering every grass blade on every frame is fairly inefficient, you will also use compute shaders to cull grass blades that don't contribute to a given frame. The remaining blades will be passed to a graphics pipeline, in which you will write several shaders. You will write a vertex shader to transform Bezier control points, tessellation shaders to dynamically create the grass geometry from the Bezier curves, and a fragment shader to shade the grass blades. The base code provided includes all of the basic Vulkan setup, including a compute pipeline that will run your compute shaders and two graphics pipelines, one for rendering the geometry that grass will be placed on and the other for rendering the grass itself. Your job will be to write the shaders for the grass graphics pipeline and the compute pipeline, as well as binding any resources (descriptors) you may need to accomplish the tasks described in this assignment. @@ -53,7 +53,7 @@ You need to implement the following features/pipeline stages: * Compute shader (`shaders/compute.comp`) * Grass pipeline stages - * Vertex shader (`shaders/grass.vert') + * Vertex shader (`shaders/grass.vert`) * Tessellation control shader (`shaders/grass.tesc`) * Tessellation evaluation shader (`shaders/grass.tese`) * Fragment shader (`shaders/grass.frag`) @@ -171,7 +171,7 @@ You are free to define two parameters here. Define a function such that the grass blades in the bucket closest to the camera are kept while an increasing number of grass blades are culled with each farther bucket. -#### Occlusion culling (extra credit) +#### **Extra Credit**: Occlusion culling This type of culling only makes sense if our scene has additional objects aside from the plane and the grass blades. We want to cull grass blades that are occluded by other geometry. Think about how you can use a depth map to accomplish this! @@ -183,7 +183,7 @@ In the tessellation control shader, specify the amount of tessellation you want The generated vertices will be passed to the tessellation evaluation shader, where you will place the vertices in world space, respecting the width, height, and orientation information of each blade. Once you have determined the world space position of each vector, make sure to set the output `gl_Position` in clip space! -** Extra Credit**: Tessellate to varying levels of detail as a function of how far the grass blade is from the camera. For example, if the blade is very far, only generate four vertices in the tessellation control shader. +**Extra Credit**: Tessellate to varying levels of detail as a function of how far the grass blade is from the camera. For example, if the blade is very far, only generate four vertices in the tessellation control shader. To build more intuition on how tessellation works, I highly recommend playing with the [helloTessellation sample](https://github.com/CIS565-Fall-2017/Vulkan-Samples/tree/master/samples/5_helloTessellation) and reading this [tutorial on tessellation](https://ogldev.org/www/tutorial30/tutorial30.html). diff --git a/README.md b/README.md index 20ee451..32a0939 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,93 @@ Vulkan Grass Rendering **University of Pennsylvania, CIS 565: GPU Programming and Architecture, Project 5** -* (TODO) YOUR NAME HERE -* Tested on: (TODO) Windows 22, i7-2222 @ 2.22GHz 22GB, GTX 222 222MB (Moore 2222 Lab) +* Xinran Tao + * [LinkedIn](https://www.linkedin.com/in/xinran-tao/), [GitHub](https://github.com/theBoilingPoint), [Portfolio](https://www.xinrantao.com/) +* Tested on: Windows 11 Enterprise, AMD Ryzen 7 7800X3D 8 Core Processor @ 4.201GHz, RTX 2080Ti (Personal PC) -### (TODO: Your README) +# Introduction +In this project, I am implementing a grass simulation introduced by [Responsive Real-Time Grass Rendering for General 3D Scenes](https://doi.org/10.1145/3023368.3023380) in [Vulkan](https://www.vulkan.org/). -*DO NOT* leave the README to the last minute! It is a crucial part of the -project, and we will not be able to grade you without a good README. +The features include: +- Grass blades are rendered with gravity, recovery, and wind forces applied. +- Grass blades maintain realistic shapes when forces are applied. +- Grass blades are culled by orientation, view frustum, and distance from the camera. +- Grass blades tessellation levels are determined by the distance from the camera. The closer they are to the camera, the higher the tessellation levels. + +# Showcase +|![](img/my/grass_without_forces.gif)| +|:--:| +|**Stage 1:** Grass Rendering without Forces| + +|![](img/my/grass_with_forces.gif)| +|:--:| +|**Stage 2:** Grass Rendering with Forces| + +|![](img/my/grass_with_tesslv_col.gif)| +|:--:| +|**Stage 3:** Grass Rendering with Varying Tessellation Levels| +|*The brighter the colour, the higher the tessellation level.*| + +# Performance Analysis +>To analyse the performance, I use the `VK_LAYER_LUNARG_monitor` layer to measure the FPS. I did this by adding the following code to the `Instance.cpp` file: +> ```cpp +> Instance::Instance(const char* applicationName, unsigned int additionalExtensionCount, > const char** additionalExtensions) { +> ... +> +> /** Note that I didn't check if this layer exists! **/ +> const char* instance_layers[] = { "VK_LAYER_LUNARG_monitor" }; +> createInfo.enabledLayerCount = 1; +> createInfo.ppEnabledLayerNames = instance_layers; +> /*******************************************************/ +> +> ... +> } +> ``` +> As stated in the comment, I didn't check if the extension exists. Please be mindful about this when running the code. + +## Handling Increasing Numbers of Grass Blades +|![](img/my/FPS%20-%20Without%20Culling%20and%20With%20Culling.png)| +|:--:| +|**FPS Comparison:** Without Culling and With Culling| + +As the number of grass blades increases, the FPS declines significantly, revealing the renderer’s sensitivity to the computational load. Rendering each blade requires calculating transformations, applying shaders, and performing memory operations. With each additional blade, the rendering time per frame increases, causing the FPS to drop substantially. + +For instance, with 2^10 blades, the renderer achieves high FPS values around 11,000-12,000, indicating efficient handling of the workload. However, as the blade count doubles to 2^12, the FPS decreases by about 58%, ranging between 5,000-7,000 FPS. By 2^14 blades, FPS drops further by 69%, reaching values around 1,500-3,200. At very high blade counts, such as 2^18 and 2^20 blades, FPS falls to below 200, with the lowest values at around 30 FPS, representing an additional 93% decrease. These results highlight the renderer's challenges in handling high blade counts, as the computational requirements grow exponentially, leading to severe FPS reductions. + +### Why Culling Improves Performance + +Culling improves performance by reducing the number of blades that the renderer processes each frame. Instead of calculating transformations, shading, and visibility for every blade in the scene, culling allows the renderer to ignore certain blades based on specific criteria (such as visibility or distance from the camera). This reduction in workload directly translates into improved FPS, as fewer blades mean fewer computations and less memory access per frame. + +Each culling technique applies different criteria to determine which blades can be safely ignored, which influences how effectively each method reduces the workload. In particular: +- **Orientation Culling** discards blades facing away from the camera, as they are likely not visible to the viewer. This reduces the number of blades processed based on their orientation relative to the camera, cutting down on the processing time for blades that contribute minimally or not at all to the final image. +- **View Frustum Culling** focuses on blades outside the camera’s view frustum (the visible area in 3D space), discarding those that fall outside the field of view. This method allows the renderer to ignore blades that cannot be seen, concentrating only on visible ones and thereby improving performance. +- **Distance Culling** removes blades beyond a certain distance from the camera. Distant blades contribute less detail due to their smaller screen size, so culling them has minimal impact on visual fidelity while reducing the workload significantly. + +By selectively excluding blades based on these criteria, each culling technique reduces the number of computations per frame, directly boosting the FPS. + +## Performance Comparison of Culling Methods +|![](img/my/FPS%20-%20Orientation%20Culling,%20View%20Frustrum%20Culling%20and%20Distance%20Culling.png)| +|:--:| +|**FPS Comparison:** Orientation Culling, View Frustrum Culling and Distance Culling| + +The data demonstrates that each culling method offers performance benefits, though the effectiveness of each method varies depending on the number of blades and the criteria used: + +1. **Orientation Culling**: + - Orientation Culling provides a modest performance improvement by discarding blades not facing the camera. At lower blade counts, the effect is limited because most blades are still within the camera's view and orientation. For instance, with 2^10 blades, Orientation Culling results in an FPS decrease of only about 5% compared to no culling, as relatively few blades are excluded. + - As blade count increases, Orientation Culling becomes slightly more beneficial. At 2^14 blades, Orientation Culling achieves 2,151 FPS, which is approximately 40% higher than without culling. However, this method has diminishing returns with further increases in blade count. By 2^16 blades, it only provides a 22% improvement over rendering without culling, as a large number of blades are still in view and facing the camera. + +2. **View Frustum Culling**: + - View Frustum Culling provides a slightly higher improvement than Orientation Culling because it discards all blades outside the visible area, regardless of their orientation. This allows the renderer to ignore a larger portion of the scene, particularly when the camera is focused on a specific area. + - At 2^14 blades, View Frustum Culling yields around 1,705 FPS, which is 10% lower than Orientation Culling, likely because blades outside the frustum are still limited at that range. As blade count increases, however, this method becomes more effective. For instance, at 2^16 blades, it achieves 454 FPS, which is approximately 13% higher than without culling. + - This method is particularly beneficial when the camera's view is focused on a narrow area, as it effectively reduces the number of off-screen blades that need to be processed. + +3. **Distance Culling**: + - Distance Culling is the most effective culling technique, especially at high blade counts, as it discards blades based on their distance from the camera. This method ensures that only nearby, detailed blades are processed, reducing workload without sacrificing significant visual detail. + - For 2^10 blades, Distance Culling provides similar FPS (around 11,215) to Orientation and View Frustum Culling, as there aren’t many distant blades. However, at 2^14 blades, it achieves 2,249 FPS, which is 32% higher than View Frustum Culling, as it discards more blades based solely on distance. + - At 2^16 blades, Distance Culling results in 585 FPS, an improvement of 46% compared to rendering without culling. This benefit becomes even more apparent at 2^18 blades, where Distance Culling achieves 148 FPS, 42% higher than View Frustum Culling, demonstrating its effectiveness in handling extremely high blade counts. + +## Summary + +As the blade count increases, the renderer's FPS declines sharply due to the increased computational workload. Culling mitigates this effect by reducing the number of blades processed, with each method offering varying levels of improvement. Orientation Culling is useful for discarding blades facing away from the camera, but it provides limited benefits at high blade counts. View Frustum Culling performs better by focusing only on visible blades, offering consistent performance gains, particularly when the camera view is narrow. Distance Culling is the most effective method, especially at high blade counts, as it removes distant blades that contribute little to the scene, resulting in a substantial 42-46% improvement at extreme blade counts. + +To optimize rendering performance effectively, combining these culling methods would allow the renderer to maintain visual fidelity while managing performance across different scene conditions. Distance Culling should be prioritized for scenes with high blade counts, while Orientation and View Frustum Culling are beneficial for moderate counts or when the camera view focuses on specific regions. \ No newline at end of file diff --git a/bin/Release/vulkan_grass_rendering.exe b/bin/Release/vulkan_grass_rendering.exe deleted file mode 100644 index f68db3a..0000000 Binary files a/bin/Release/vulkan_grass_rendering.exe and /dev/null differ diff --git a/img/my/FPS - Orientation Culling, View Frustrum Culling and Distance Culling.png b/img/my/FPS - Orientation Culling, View Frustrum Culling and Distance Culling.png new file mode 100644 index 0000000..0fe8887 Binary files /dev/null and b/img/my/FPS - Orientation Culling, View Frustrum Culling and Distance Culling.png differ diff --git a/img/my/FPS - Without Culling and With Culling.png b/img/my/FPS - Without Culling and With Culling.png new file mode 100644 index 0000000..95d6160 Binary files /dev/null and b/img/my/FPS - Without Culling and With Culling.png differ diff --git a/img/my/grass_with_forces.gif b/img/my/grass_with_forces.gif new file mode 100644 index 0000000..5731278 Binary files /dev/null and b/img/my/grass_with_forces.gif differ diff --git a/img/my/grass_with_tesslv_col.gif b/img/my/grass_with_tesslv_col.gif new file mode 100644 index 0000000..9b4d0ea Binary files /dev/null and b/img/my/grass_with_tesslv_col.gif differ diff --git a/img/my/grass_without_forces.gif b/img/my/grass_without_forces.gif new file mode 100644 index 0000000..71efe5a Binary files /dev/null and b/img/my/grass_without_forces.gif differ diff --git a/src/Blades.cpp b/src/Blades.cpp index 80e3d76..0142372 100644 --- a/src/Blades.cpp +++ b/src/Blades.cpp @@ -45,7 +45,7 @@ Blades::Blades(Device* device, VkCommandPool commandPool, float planeDim) : Mode indirectDraw.firstInstance = 0; BufferUtils::CreateBufferFromData(device, commandPool, blades.data(), NUM_BLADES * sizeof(Blade), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, bladesBuffer, bladesBufferMemory); - BufferUtils::CreateBuffer(device, NUM_BLADES * sizeof(Blade), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, culledBladesBuffer, culledBladesBufferMemory); + BufferUtils::CreateBuffer(device, NUM_BLADES * sizeof(Blade), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, culledBladesBuffer, culledBladesBufferMemory); BufferUtils::CreateBufferFromData(device, commandPool, &indirectDraw, sizeof(BladeDrawIndirect), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT, numBladesBuffer, numBladesBufferMemory); } diff --git a/src/Blades.h b/src/Blades.h index 9bd1eed..f573779 100644 --- a/src/Blades.h +++ b/src/Blades.h @@ -4,7 +4,7 @@ #include #include "Model.h" -constexpr static unsigned int NUM_BLADES = 1 << 13; +constexpr static unsigned int NUM_BLADES = 1 << 13; // default 1 << 13 constexpr static float MIN_HEIGHT = 1.3f; constexpr static float MAX_HEIGHT = 2.5f; constexpr static float MIN_WIDTH = 0.1f; diff --git a/src/Camera.cpp b/src/Camera.cpp index 3afb5b8..bdfa199 100644 --- a/src/Camera.cpp +++ b/src/Camera.cpp @@ -41,6 +41,12 @@ void Camera::UpdateOrbit(float deltaX, float deltaY, float deltaZ) { memcpy(mappedData, &cameraBufferObject, sizeof(CameraBufferObject)); } +void Camera::UpdateAspectRatio(float aspectRatio) { + cameraBufferObject.projectionMatrix = glm::perspective(glm::radians(45.0f), aspectRatio, 0.1f, 100.0f); + cameraBufferObject.projectionMatrix[1][1] *= -1; // y-coordinate is flipped + memcpy(mappedData, &cameraBufferObject, sizeof(CameraBufferObject)); +} + Camera::~Camera() { vkUnmapMemory(device->GetVkDevice(), bufferMemory); vkDestroyBuffer(device->GetVkDevice(), buffer, nullptr); diff --git a/src/Camera.h b/src/Camera.h index 6b10747..72b156e 100644 --- a/src/Camera.h +++ b/src/Camera.h @@ -29,4 +29,5 @@ class Camera { VkBuffer GetBuffer() const; void UpdateOrbit(float deltaX, float deltaY, float deltaZ); + void UpdateAspectRatio(float aspectRatio); // Add this method }; diff --git a/src/Instance.cpp b/src/Instance.cpp index 7f6b01c..58b96f6 100644 --- a/src/Instance.cpp +++ b/src/Instance.cpp @@ -1,6 +1,12 @@ #include #include #include +#include +#include +#include +#ifdef _WIN32 +#include +#endif #include "Instance.h" #ifdef NDEBUG @@ -13,6 +19,27 @@ namespace { const std::vector validationLayers = { "VK_LAYER_KHRONOS_validation" }; + + // Monitor layer for displaying frametime/FPS on window + const std::vector monitorLayers = { + "VK_LAYER_LUNARG_monitor" + }; + + // Check if a layer is available at instance level + bool checkLayerSupport(const char* layerName) { + uint32_t layerCount; + vkEnumerateInstanceLayerProperties(&layerCount, nullptr); + + std::vector availableLayers(layerCount); + vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data()); + + for (const auto& layerProperties : availableLayers) { + if (strcmp(layerName, layerProperties.layerName) == 0) { + return true; + } + } + return false; + } // Get the required list of extensions based on whether validation layers are enabled std::vector getRequiredExtensions() { @@ -42,6 +69,18 @@ namespace { } Instance::Instance(const char* applicationName, unsigned int additionalExtensionCount, const char** additionalExtensions) { + // Configure monitor layer to display frametime in addition to FPS + // Set environment variables before creating instance + #ifdef _WIN32 + // Windows: Set environment variable for current process + SetEnvironmentVariableA("VK_LAYER_LUNARG_MONITOR_LAYER_DISPLAY_FRAMETIME", "1"); + SetEnvironmentVariableA("VK_LAYER_LUNARG_MONITOR_LAYER_DISPLAY_FPS", "1"); + #else + // Linux/Unix: Set environment variable + setenv("VK_LAYER_LUNARG_MONITOR_LAYER_DISPLAY_FRAMETIME", "1", 0); + setenv("VK_LAYER_LUNARG_MONITOR_LAYER_DISPLAY_FPS", "1", 0); + #endif + // --- Specify details about our application --- VkApplicationInfo appInfo = {}; appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; @@ -65,9 +104,22 @@ Instance::Instance(const char* applicationName, unsigned int additionalExtension createInfo.ppEnabledExtensionNames = extensions.data(); // Specify global validation layers + // Combine validation layers and monitor layer (if available) if validation is enabled + enabledLayers.clear(); if (ENABLE_VALIDATION) { - createInfo.enabledLayerCount = static_cast(validationLayers.size()); - createInfo.ppEnabledLayerNames = validationLayers.data(); + enabledLayers.insert(enabledLayers.end(), validationLayers.begin(), validationLayers.end()); + // Add monitor layer to display frametime/FPS on window (if available) + for (const char* monitorLayer : monitorLayers) { + if (checkLayerSupport(monitorLayer)) { + enabledLayers.push_back(monitorLayer); + fprintf(stderr, "Monitor layer enabled: %s\n", monitorLayer); + } else { + fprintf(stderr, "Monitor layer not available: %s (frametime overlay will not be displayed)\n", monitorLayer); + } + } + + createInfo.enabledLayerCount = static_cast(enabledLayers.size()); + createInfo.ppEnabledLayerNames = enabledLayers.data(); } else { createInfo.enabledLayerCount = 0; } @@ -337,12 +389,12 @@ Device* Instance::CreateDevice(QueueFlagBits requiredQueues, VkPhysicalDeviceFea createInfo.enabledExtensionCount = static_cast(deviceExtensions.size()); createInfo.ppEnabledExtensionNames = deviceExtensions.data(); - if (ENABLE_VALIDATION) { - createInfo.enabledLayerCount = static_cast(validationLayers.size()); - createInfo.ppEnabledLayerNames = validationLayers.data(); - } else { - createInfo.enabledLayerCount = 0; - } + // Note: Device layers are deprecated in Vulkan 1.1+ + // Layers should only be enabled at instance level, not device level + // All layers (including validation and monitor) are automatically applied to devices + // when enabled at instance level, so we don't need to enable them here + createInfo.enabledLayerCount = 0; + createInfo.ppEnabledLayerNames = nullptr; VkDevice vkDevice; // Create logical device diff --git a/src/Instance.h b/src/Instance.h index afc54c5..63c3064 100644 --- a/src/Instance.h +++ b/src/Instance.h @@ -37,6 +37,7 @@ class Instance { VkInstance instance; VkDebugReportCallbackEXT debugReportCallback; std::vector deviceExtensions; + std::vector enabledLayers; // Store enabled layers for reuse in device creation VkPhysicalDevice physicalDevice = VK_NULL_HANDLE; QueueFamilyIndices queueFamilyIndices; VkSurfaceCapabilitiesKHR surfaceCapabilities; diff --git a/src/Renderer.cpp b/src/Renderer.cpp index b445d04..a5b2218 100644 --- a/src/Renderer.cpp +++ b/src/Renderer.cpp @@ -24,7 +24,6 @@ Renderer::Renderer(Device* device, SwapChain* swapChain, Scene* scene, Camera* c CreateDescriptorPool(); CreateCameraDescriptorSet(); CreateModelDescriptorSets(); - CreateGrassDescriptorSets(); CreateTimeDescriptorSet(); CreateComputeDescriptorSets(); CreateFrameResources(); @@ -196,15 +195,50 @@ void Renderer::CreateTimeDescriptorSetLayout() { void Renderer::CreateComputeDescriptorSetLayout() { // TODO: Create the descriptor set layout for the compute pipeline - // Remember this is like a class definition stating why types of information + // Remember this is like a class definition stating what types of information // will be stored at each binding + VkDescriptorSetLayoutBinding inputBladesLayoutBinding = {}; + inputBladesLayoutBinding.binding = 0; + inputBladesLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + inputBladesLayoutBinding.descriptorCount = 1; + inputBladesLayoutBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; // NOTE: So far only seen in compute shader. Might need in vertex shader. + inputBladesLayoutBinding.pImmutableSamplers = nullptr; + + VkDescriptorSetLayoutBinding outputBladesLayoutBinding = {}; + outputBladesLayoutBinding.binding = 1; + outputBladesLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + outputBladesLayoutBinding.descriptorCount = 1; + outputBladesLayoutBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; // NOTE: So far only seen in compute shader. Might need in vertex shader. + outputBladesLayoutBinding.pImmutableSamplers = nullptr; + + VkDescriptorSetLayoutBinding numBladesLayoutBinding = {}; + numBladesLayoutBinding.binding = 2; + numBladesLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + numBladesLayoutBinding.descriptorCount = 1; + numBladesLayoutBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; // NOTE: So far only seen in compute shader. Might need in vertex shader. + numBladesLayoutBinding.pImmutableSamplers = nullptr; + + std::vector bindings = { inputBladesLayoutBinding, outputBladesLayoutBinding, numBladesLayoutBinding }; + + // Create the descriptor set layout + VkDescriptorSetLayoutCreateInfo layoutInfo = {}; + layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + layoutInfo.bindingCount = static_cast(bindings.size()); + layoutInfo.pBindings = bindings.data(); + + if (vkCreateDescriptorSetLayout(logicalDevice, &layoutInfo, nullptr, &computeDescriptorSetLayout) != VK_SUCCESS) { + throw std::runtime_error("Failed to create descriptor set layout"); + } + else { + std::cout << "Descriptor set layout created successfully" << std::endl; + } } void Renderer::CreateDescriptorPool() { // Describe which descriptor types that the descriptor sets will contain std::vector poolSizes = { // Camera - { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER , 1}, + { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER , 1 }, // Models + Blades { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER , static_cast(scene->GetModels().size() + scene->GetBlades().size()) }, @@ -216,6 +250,9 @@ void Renderer::CreateDescriptorPool() { { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER , 1 }, // TODO: Add any additional types and counts of descriptors you will need to allocate + // Input blades buffer, output blades buffer, and num blades buffer. 3 in total + /*{ VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 3 }*/ + { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 3 * static_cast(scene->GetBlades().size()) } }; VkDescriptorPoolCreateInfo poolInfo = {}; @@ -317,11 +354,6 @@ void Renderer::CreateModelDescriptorSets() { vkUpdateDescriptorSets(logicalDevice, static_cast(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr); } -void Renderer::CreateGrassDescriptorSets() { - // TODO: Create Descriptor sets for the grass. - // This should involve creating descriptor sets which point to the model matrix of each group of grass blades -} - void Renderer::CreateTimeDescriptorSet() { // Describe the desciptor set VkDescriptorSetLayout layouts[] = { timeDescriptorSetLayout }; @@ -360,6 +392,78 @@ void Renderer::CreateTimeDescriptorSet() { void Renderer::CreateComputeDescriptorSets() { // TODO: Create Descriptor sets for the compute pipeline // The descriptors should point to Storage buffers which will hold the grass blades, the culled grass blades, and the output number of grass blades + computeDescriptorSets.resize(scene->GetBlades().size()); + + // Describe the desciptor set + VkDescriptorSetLayout layouts[] = { computeDescriptorSetLayout }; + VkDescriptorSetAllocateInfo allocInfo = {}; + allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + allocInfo.descriptorPool = descriptorPool; + allocInfo.descriptorSetCount = static_cast(computeDescriptorSets.size()); + allocInfo.pSetLayouts = layouts; + + // Allocate descriptor sets + if (vkAllocateDescriptorSets(logicalDevice, &allocInfo, computeDescriptorSets.data()) != VK_SUCCESS) { + throw std::runtime_error("Failed to allocate compute descriptor sets."); + } + else { + std::cout << "Compute descriptor sets allocated successfully" << std::endl; + + } + + std::vector descriptorWrites(3 * computeDescriptorSets.size()); + + for (uint32_t i = 0; i < scene->GetBlades().size(); ++i) { + const auto curBlades = scene->GetBlades()[i]; + + VkDescriptorBufferInfo inputBladesBufferInfo = {}; + inputBladesBufferInfo.buffer = curBlades->GetBladesBuffer(); + inputBladesBufferInfo.offset = 0; + inputBladesBufferInfo.range = NUM_BLADES * sizeof(Blade); + + VkDescriptorBufferInfo outputBladesBufferInfo = {}; + outputBladesBufferInfo.buffer = curBlades->GetCulledBladesBuffer(); + outputBladesBufferInfo.offset = 0; + outputBladesBufferInfo.range = NUM_BLADES * sizeof(Blade); + + VkDescriptorBufferInfo numBladesBufferInfo = {}; + numBladesBufferInfo.buffer = curBlades->GetNumBladesBuffer(); + numBladesBufferInfo.offset = 0; + numBladesBufferInfo.range = sizeof(BladeDrawIndirect); + + descriptorWrites[3 * i + 0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + descriptorWrites[3 * i + 0].dstSet = computeDescriptorSets[i]; + descriptorWrites[3 * i + 0].dstBinding = 0; + descriptorWrites[3 * i + 0].dstArrayElement = 0; + descriptorWrites[3 * i + 0].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + descriptorWrites[3 * i + 0].descriptorCount = 1; + descriptorWrites[3 * i + 0].pBufferInfo = &inputBladesBufferInfo; + descriptorWrites[3 * i + 0].pImageInfo = nullptr; + descriptorWrites[3 * i + 0].pTexelBufferView = nullptr; + + descriptorWrites[3 * i + 1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + descriptorWrites[3 * i + 1].dstSet = computeDescriptorSets[i]; + descriptorWrites[3 * i + 1].dstBinding = 1; + descriptorWrites[3 * i + 1].dstArrayElement = 0; + descriptorWrites[3 * i + 1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + descriptorWrites[3 * i + 1].descriptorCount = 1; + descriptorWrites[3 * i + 1].pBufferInfo = &outputBladesBufferInfo; + descriptorWrites[3 * i + 1].pImageInfo = nullptr; + descriptorWrites[3 * i + 1].pTexelBufferView = nullptr; + + descriptorWrites[3 * i + 2].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + descriptorWrites[3 * i + 2].dstSet = computeDescriptorSets[i]; + descriptorWrites[3 * i + 2].dstBinding = 2; + descriptorWrites[3 * i + 2].dstArrayElement = 0; + descriptorWrites[3 * i + 2].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + descriptorWrites[3 * i + 2].descriptorCount = 1; + descriptorWrites[3 * i + 2].pBufferInfo = &numBladesBufferInfo; + descriptorWrites[3 * i + 2].pImageInfo = nullptr; + descriptorWrites[3 * i + 2].pTexelBufferView = nullptr; + } + + // Update descriptor sets + vkUpdateDescriptorSets(logicalDevice, static_cast(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr); } void Renderer::CreateGraphicsPipeline() { @@ -402,24 +506,13 @@ void Renderer::CreateGraphicsPipeline() { inputAssembly.primitiveRestartEnable = VK_FALSE; // Viewports and Scissors (rectangles that define in which regions pixels are stored) - VkViewport viewport = {}; - viewport.x = 0.0f; - viewport.y = 0.0f; - viewport.width = static_cast(swapChain->GetVkExtent().width); - viewport.height = static_cast(swapChain->GetVkExtent().height); - viewport.minDepth = 0.0f; - viewport.maxDepth = 1.0f; - - VkRect2D scissor = {}; - scissor.offset = { 0, 0 }; - scissor.extent = swapChain->GetVkExtent(); - + // Note: Using dynamic viewport/scissor, so we don't set actual values here VkPipelineViewportStateCreateInfo viewportState = {}; viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; viewportState.viewportCount = 1; - viewportState.pViewports = &viewport; + viewportState.pViewports = nullptr; // Dynamic viewportState.scissorCount = 1; - viewportState.pScissors = &scissor; + viewportState.pScissors = nullptr; // Dynamic // Rasterizer VkPipelineRasterizationStateCreateInfo rasterizer = {}; @@ -494,6 +587,17 @@ void Renderer::CreateGraphicsPipeline() { throw std::runtime_error("Failed to create pipeline layout"); } + // Dynamic state + std::vector dynamicStates = { + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR + }; + + VkPipelineDynamicStateCreateInfo dynamicState = {}; + dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; + dynamicState.dynamicStateCount = static_cast(dynamicStates.size()); + dynamicState.pDynamicStates = dynamicStates.data(); + // --- Create graphics pipeline --- VkGraphicsPipelineCreateInfo pipelineInfo = {}; pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; @@ -506,7 +610,7 @@ void Renderer::CreateGraphicsPipeline() { pipelineInfo.pMultisampleState = &multisampling; pipelineInfo.pDepthStencilState = &depthStencil; pipelineInfo.pColorBlendState = &colorBlending; - pipelineInfo.pDynamicState = nullptr; + pipelineInfo.pDynamicState = &dynamicState; // Add this line pipelineInfo.layout = graphicsPipelineLayout; pipelineInfo.renderPass = renderPass; pipelineInfo.subpass = 0; @@ -576,24 +680,13 @@ void Renderer::CreateGrassPipeline() { inputAssembly.primitiveRestartEnable = VK_FALSE; // Viewports and Scissors (rectangles that define in which regions pixels are stored) - VkViewport viewport = {}; - viewport.x = 0.0f; - viewport.y = 0.0f; - viewport.width = static_cast(swapChain->GetVkExtent().width); - viewport.height = static_cast(swapChain->GetVkExtent().height); - viewport.minDepth = 0.0f; - viewport.maxDepth = 1.0f; - - VkRect2D scissor = {}; - scissor.offset = { 0, 0 }; - scissor.extent = swapChain->GetVkExtent(); - + // Note: Using dynamic viewport/scissor, so we don't set actual values here VkPipelineViewportStateCreateInfo viewportState = {}; viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; viewportState.viewportCount = 1; - viewportState.pViewports = &viewport; + viewportState.pViewports = nullptr; // Dynamic viewportState.scissorCount = 1; - viewportState.pScissors = &scissor; + viewportState.pScissors = nullptr; // Dynamic // Rasterizer VkPipelineRasterizationStateCreateInfo rasterizer = {}; @@ -675,6 +768,17 @@ void Renderer::CreateGrassPipeline() { tessellationInfo.flags = 0; tessellationInfo.patchControlPoints = 1; + // Dynamic state + std::vector dynamicStates = { + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR + }; + + VkPipelineDynamicStateCreateInfo dynamicState = {}; + dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; + dynamicState.dynamicStateCount = static_cast(dynamicStates.size()); + dynamicState.pDynamicStates = dynamicStates.data(); + // --- Create graphics pipeline --- VkGraphicsPipelineCreateInfo pipelineInfo = {}; pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; @@ -688,7 +792,7 @@ void Renderer::CreateGrassPipeline() { pipelineInfo.pDepthStencilState = &depthStencil; pipelineInfo.pColorBlendState = &colorBlending; pipelineInfo.pTessellationState = &tessellationInfo; - pipelineInfo.pDynamicState = nullptr; + pipelineInfo.pDynamicState = &dynamicState; pipelineInfo.layout = grassPipelineLayout; pipelineInfo.renderPass = renderPass; pipelineInfo.subpass = 0; @@ -717,7 +821,7 @@ void Renderer::CreateComputePipeline() { computeShaderStageInfo.pName = "main"; // TODO: Add the compute dsecriptor set layout you create to this list - std::vector descriptorSetLayouts = { cameraDescriptorSetLayout, timeDescriptorSetLayout }; + std::vector descriptorSetLayouts = { cameraDescriptorSetLayout, timeDescriptorSetLayout, computeDescriptorSetLayout }; // Create pipeline layout VkPipelineLayoutCreateInfo pipelineLayoutInfo = {}; @@ -826,24 +930,62 @@ void Renderer::CreateFrameResources() { void Renderer::DestroyFrameResources() { for (size_t i = 0; i < imageViews.size(); i++) { - vkDestroyImageView(logicalDevice, imageViews[i], nullptr); + if (imageViews[i] != VK_NULL_HANDLE) { + vkDestroyImageView(logicalDevice, imageViews[i], nullptr); + imageViews[i] = VK_NULL_HANDLE; + } } + imageViews.clear(); - vkDestroyImageView(logicalDevice, depthImageView, nullptr); - vkFreeMemory(logicalDevice, depthImageMemory, nullptr); - vkDestroyImage(logicalDevice, depthImage, nullptr); + if (depthImageView != VK_NULL_HANDLE) { + vkDestroyImageView(logicalDevice, depthImageView, nullptr); + depthImageView = VK_NULL_HANDLE; + } + if (depthImageMemory != VK_NULL_HANDLE) { + vkFreeMemory(logicalDevice, depthImageMemory, nullptr); + depthImageMemory = VK_NULL_HANDLE; + } + if (depthImage != VK_NULL_HANDLE) { + vkDestroyImage(logicalDevice, depthImage, nullptr); + depthImage = VK_NULL_HANDLE; + } for (size_t i = 0; i < framebuffers.size(); i++) { - vkDestroyFramebuffer(logicalDevice, framebuffers[i], nullptr); + if (framebuffers[i] != VK_NULL_HANDLE) { + vkDestroyFramebuffer(logicalDevice, framebuffers[i], nullptr); + framebuffers[i] = VK_NULL_HANDLE; + } } + framebuffers.clear(); } void Renderer::RecreateFrameResources() { - vkDestroyPipeline(logicalDevice, graphicsPipeline, nullptr); - vkDestroyPipeline(logicalDevice, grassPipeline, nullptr); - vkDestroyPipelineLayout(logicalDevice, graphicsPipelineLayout, nullptr); - vkDestroyPipelineLayout(logicalDevice, grassPipelineLayout, nullptr); - vkFreeCommandBuffers(logicalDevice, graphicsCommandPool, static_cast(commandBuffers.size()), commandBuffers.data()); + // Wait for all operations to complete before recreating + vkDeviceWaitIdle(logicalDevice); + + // Destroy pipelines (safe to call with VK_NULL_HANDLE) + if (graphicsPipeline != VK_NULL_HANDLE) { + vkDestroyPipeline(logicalDevice, graphicsPipeline, nullptr); + graphicsPipeline = VK_NULL_HANDLE; + } + if (grassPipeline != VK_NULL_HANDLE) { + vkDestroyPipeline(logicalDevice, grassPipeline, nullptr); + grassPipeline = VK_NULL_HANDLE; + } + if (graphicsPipelineLayout != VK_NULL_HANDLE) { + vkDestroyPipelineLayout(logicalDevice, graphicsPipelineLayout, nullptr); + graphicsPipelineLayout = VK_NULL_HANDLE; + } + if (grassPipelineLayout != VK_NULL_HANDLE) { + vkDestroyPipelineLayout(logicalDevice, grassPipelineLayout, nullptr); + grassPipelineLayout = VK_NULL_HANDLE; + } + + // Free command buffers if they exist + if (!commandBuffers.empty()) { + vkFreeCommandBuffers(logicalDevice, graphicsCommandPool, static_cast(commandBuffers.size()), commandBuffers.data()); + commandBuffers.clear(); + } DestroyFrameResources(); CreateFrameResources(); @@ -884,6 +1026,10 @@ void Renderer::RecordComputeCommandBuffer() { vkCmdBindDescriptorSets(computeCommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, computePipelineLayout, 1, 1, &timeDescriptorSet, 0, nullptr); // TODO: For each group of blades bind its descriptor set and dispatch + for (uint32_t i = 0; i < scene->GetBlades().size(); ++i) { + vkCmdBindDescriptorSets(computeCommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, computePipelineLayout, 2, 1, &computeDescriptorSets[i], 0, nullptr); + vkCmdDispatch(computeCommandBuffer, (NUM_BLADES + WORKGROUP_SIZE - 1) / WORKGROUP_SIZE, 1, 1); + } // ~ End recording ~ if (vkEndCommandBuffer(computeCommandBuffer) != VK_SUCCESS) { @@ -892,7 +1038,18 @@ void Renderer::RecordComputeCommandBuffer() { } void Renderer::RecordCommandBuffers() { - commandBuffers.resize(swapChain->GetCount()); + // Free existing command buffers if any + if (!commandBuffers.empty()) { + vkFreeCommandBuffers(logicalDevice, graphicsCommandPool, static_cast(commandBuffers.size()), commandBuffers.data()); + commandBuffers.clear(); + } + + uint32_t swapChainImageCount = swapChain->GetCount(); + if (swapChainImageCount == 0) { + throw std::runtime_error("Swap chain has no images"); + } + + commandBuffers.resize(swapChainImageCount); // Specify the command pool and number of buffers to allocate VkCommandBufferAllocateInfo allocInfo = {}; @@ -917,6 +1074,22 @@ void Renderer::RecordCommandBuffers() { throw std::runtime_error("Failed to begin recording command buffer"); } + // Set dynamic viewport and scissor + VkViewport viewport = {}; + viewport.x = 0.0f; + viewport.y = 0.0f; + viewport.width = static_cast(swapChain->GetVkExtent().width); + viewport.height = static_cast(swapChain->GetVkExtent().height); + viewport.minDepth = 0.0f; + viewport.maxDepth = 1.0f; + + VkRect2D scissor = {}; + scissor.offset = { 0, 0 }; + scissor.extent = swapChain->GetVkExtent(); + + vkCmdSetViewport(commandBuffers[i], 0, 1, &viewport); + vkCmdSetScissor(commandBuffers[i], 0, 1, &scissor); + // Begin the render pass VkRenderPassBeginInfo renderPassInfo = {}; renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; @@ -976,13 +1149,11 @@ void Renderer::RecordCommandBuffers() { VkBuffer vertexBuffers[] = { scene->GetBlades()[j]->GetCulledBladesBuffer() }; VkDeviceSize offsets[] = { 0 }; // TODO: Uncomment this when the buffers are populated - // vkCmdBindVertexBuffers(commandBuffers[i], 0, 1, vertexBuffers, offsets); - - // TODO: Bind the descriptor set for each grass blades model + vkCmdBindVertexBuffers(commandBuffers[i], 0, 1, vertexBuffers, offsets); // Draw // TODO: Uncomment this when the buffers are populated - // vkCmdDrawIndirect(commandBuffers[i], scene->GetBlades()[j]->GetNumBladesBuffer(), 0, 1, sizeof(BladeDrawIndirect)); + vkCmdDrawIndirect(commandBuffers[i], scene->GetBlades()[j]->GetNumBladesBuffer(), 0, 1, sizeof(BladeDrawIndirect)); } // End render pass @@ -1012,6 +1183,13 @@ void Renderer::Frame() { return; } + // Ensure we have valid command buffers and the index is valid + uint32_t imageIndex = swapChain->GetIndex(); + if (imageIndex >= commandBuffers.size() || commandBuffers.empty()) { + RecreateFrameResources(); + return; + } + // Submit the command buffer VkSubmitInfo submitInfo = {}; submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; @@ -1023,7 +1201,7 @@ void Renderer::Frame() { submitInfo.pWaitDstStageMask = waitStages; submitInfo.commandBufferCount = 1; - submitInfo.pCommandBuffers = &commandBuffers[swapChain->GetIndex()]; + submitInfo.pCommandBuffers = &commandBuffers[imageIndex]; VkSemaphore signalSemaphores[] = { swapChain->GetRenderFinishedVkSemaphore() }; submitInfo.signalSemaphoreCount = 1; @@ -1057,6 +1235,7 @@ Renderer::~Renderer() { vkDestroyDescriptorSetLayout(logicalDevice, cameraDescriptorSetLayout, nullptr); vkDestroyDescriptorSetLayout(logicalDevice, modelDescriptorSetLayout, nullptr); vkDestroyDescriptorSetLayout(logicalDevice, timeDescriptorSetLayout, nullptr); + vkDestroyDescriptorSetLayout(logicalDevice, computeDescriptorSetLayout, nullptr); vkDestroyDescriptorPool(logicalDevice, descriptorPool, nullptr); diff --git a/src/Renderer.h b/src/Renderer.h index 95e025f..98ed79d 100644 --- a/src/Renderer.h +++ b/src/Renderer.h @@ -1,5 +1,6 @@ #pragma once +#include #include "Device.h" #include "SwapChain.h" #include "Scene.h" @@ -24,7 +25,6 @@ class Renderer { void CreateCameraDescriptorSet(); void CreateModelDescriptorSets(); - void CreateGrassDescriptorSets(); void CreateTimeDescriptorSet(); void CreateComputeDescriptorSets(); @@ -56,12 +56,14 @@ class Renderer { VkDescriptorSetLayout cameraDescriptorSetLayout; VkDescriptorSetLayout modelDescriptorSetLayout; VkDescriptorSetLayout timeDescriptorSetLayout; + VkDescriptorSetLayout computeDescriptorSetLayout; VkDescriptorPool descriptorPool; VkDescriptorSet cameraDescriptorSet; std::vector modelDescriptorSets; VkDescriptorSet timeDescriptorSet; + std::vector computeDescriptorSets; VkPipelineLayout graphicsPipelineLayout; VkPipelineLayout grassPipelineLayout; diff --git a/src/SwapChain.cpp b/src/SwapChain.cpp index 711fec0..d978905 100644 --- a/src/SwapChain.cpp +++ b/src/SwapChain.cpp @@ -45,18 +45,32 @@ namespace { // Specify the swap extent (resolution) of the swap chain VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities, GLFWwindow* window) { + // If currentExtent is valid (not max and not zero), use it if (capabilities.currentExtent.width != std::numeric_limits::max()) { - return capabilities.currentExtent; - } else { - int width, height; - glfwGetWindowSize(window, &width, &height); - VkExtent2D actualExtent = { static_cast(width), static_cast(height) }; + if (capabilities.currentExtent.width > 0 && capabilities.currentExtent.height > 0) { + return capabilities.currentExtent; + } + } + + // Fallback: query window size directly + int width, height; + glfwGetWindowSize(window, &width, &height); + + // If window size is invalid, we cannot proceed (should have been caught earlier) + if (width <= 0 || height <= 0) { + // Return zero to indicate failure - caller should check + return { 0, 0 }; + } + + VkExtent2D actualExtent = { static_cast(width), static_cast(height) }; + // Clamp to valid range - but only if the range is valid + if (capabilities.maxImageExtent.width > 0 && capabilities.maxImageExtent.height > 0) { actualExtent.width = std::max(capabilities.minImageExtent.width, std::min(capabilities.maxImageExtent.width, actualExtent.width)); actualExtent.height = std::max(capabilities.minImageExtent.height, std::min(capabilities.maxImageExtent.height, actualExtent.height)); - - return actualExtent; } + + return actualExtent; } } @@ -74,15 +88,40 @@ SwapChain::SwapChain(Device* device, VkSurfaceKHR vkSurface, unsigned int numBuf } } -void SwapChain::Create() { +void SwapChain::Create(VkSwapchainKHR oldSwapChain) { auto* instance = device->GetInstance(); - const auto& surfaceCapabilities = instance->GetSurfaceCapabilities(); + // Refresh surface capabilities to get current window size (they change on resize) + VkSurfaceCapabilitiesKHR surfaceCapabilities; + vkGetPhysicalDeviceSurfaceCapabilitiesKHR(instance->GetPhysicalDevice(), vkSurface, &surfaceCapabilities); + + // Check if window is minimized - when minimized, all extents are zero + // We cannot create a swap chain in this state, so just return early + // The old swap chain (if any) will remain in use + if (surfaceCapabilities.currentExtent.width == 0 && surfaceCapabilities.currentExtent.height == 0 && + surfaceCapabilities.minImageExtent.width == 0 && surfaceCapabilities.minImageExtent.height == 0 && + surfaceCapabilities.maxImageExtent.width == 0 && surfaceCapabilities.maxImageExtent.height == 0) { + // Window is minimized - cannot create swap chain, keep using old one + return; + } VkSurfaceFormatKHR surfaceFormat = chooseSwapSurfaceFormat(instance->GetSurfaceFormats()); VkPresentModeKHR presentMode = chooseSwapPresentMode(instance->GetPresentModes()); VkExtent2D extent = chooseSwapExtent(surfaceCapabilities, GetGLFWWindow()); + // Validate extent before proceeding - must not be zero and must be within bounds + if (extent.width == 0 || extent.height == 0) { + throw std::runtime_error("Cannot create swap chain with zero extent"); + } + + // Ensure extent is within the valid range + if (extent.width < surfaceCapabilities.minImageExtent.width || + extent.width > surfaceCapabilities.maxImageExtent.width || + extent.height < surfaceCapabilities.minImageExtent.height || + extent.height > surfaceCapabilities.maxImageExtent.height) { + throw std::runtime_error("Swap chain extent is outside valid bounds (window may be minimized)"); + } + uint32_t imageCount = surfaceCapabilities.minImageCount + 1; imageCount = numBuffers > imageCount ? numBuffers : imageCount; if (surfaceCapabilities.maxImageCount > 0 && imageCount > surfaceCapabilities.maxImageCount) { @@ -135,7 +174,7 @@ void SwapChain::Create() { createInfo.clipped = VK_TRUE; // Reference to old swap chain in case current one becomes invalid - createInfo.oldSwapchain = VK_NULL_HANDLE; + createInfo.oldSwapchain = oldSwapChain; // Use the parameter instead of VK_NULL_HANDLE // Create swap chain if (vkCreateSwapchainKHR(device->GetVkDevice(), &createInfo, nullptr, &vkSwapChain) != VK_SUCCESS) { @@ -144,11 +183,20 @@ void SwapChain::Create() { // --- Retrieve swap chain images --- vkGetSwapchainImagesKHR(device->GetVkDevice(), vkSwapChain, &imageCount, nullptr); + vkSwapChainImages.clear(); // Clear old images first vkSwapChainImages.resize(imageCount); vkGetSwapchainImagesKHR(device->GetVkDevice(), vkSwapChain, &imageCount, vkSwapChainImages.data()); vkSwapChainImageFormat = surfaceFormat.format; vkSwapChainExtent = extent; + + // Reset image index after recreation + imageIndex = 0; + + // Destroy old swap chain after creating new one (if it exists) + if (oldSwapChain != VK_NULL_HANDLE) { + vkDestroySwapchainKHR(device->GetVkDevice(), oldSwapChain, nullptr); + } } void SwapChain::Destroy() { @@ -189,8 +237,10 @@ VkSemaphore SwapChain::GetRenderFinishedVkSemaphore() const { } void SwapChain::Recreate() { - Destroy(); - Create(); + VkSwapchainKHR oldSwapChain = vkSwapChain; // Save the old swap chain handle + // Don't set vkSwapChain to null - let Create() update it atomically after successful creation + // If window is minimized, Create() will return early and we'll keep using the old swap chain + Create(oldSwapChain); // Create will update vkSwapChain and destroy oldSwapChain if successful } bool SwapChain::Acquire() { @@ -198,20 +248,34 @@ bool SwapChain::Acquire() { // the validation layer implementation expects the application to explicitly synchronize with the GPU vkQueueWaitIdle(device->GetQueue(QueueFlags::Present)); } - VkResult result = vkAcquireNextImageKHR(device->GetVkDevice(), vkSwapChain, std::numeric_limits::max(), imageAvailableSemaphore, VK_NULL_HANDLE, &imageIndex); - if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) { - throw std::runtime_error("Failed to acquire swap chain image"); + + // Ensure swap chain is valid + if (vkSwapChain == VK_NULL_HANDLE) { + return false; } - + + VkResult result = vkAcquireNextImageKHR(device->GetVkDevice(), vkSwapChain, std::numeric_limits::max(), imageAvailableSemaphore, VK_NULL_HANDLE, &imageIndex); + if (result == VK_ERROR_OUT_OF_DATE_KHR) { + // Wait for all operations to complete before recreating + vkDeviceWaitIdle(device->GetVkDevice()); Recreate(); return false; } + + if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) { + throw std::runtime_error("Failed to acquire swap chain image"); + } return true; } bool SwapChain::Present() { + // Ensure swap chain is valid + if (vkSwapChain == VK_NULL_HANDLE) { + return false; + } + VkSemaphore signalSemaphores[] = { renderFinishedSemaphore }; // Submit result back to swap chain for presentation @@ -228,15 +292,17 @@ bool SwapChain::Present() { VkResult result = vkQueuePresentKHR(device->GetQueue(QueueFlags::Present), &presentInfo); - if (result != VK_SUCCESS) { - throw std::runtime_error("Failed to present swap chain image"); - } - if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) { + // Wait for all operations to complete before recreating + vkDeviceWaitIdle(device->GetVkDevice()); Recreate(); return false; } + if (result != VK_SUCCESS) { + throw std::runtime_error("Failed to present swap chain image"); + } + return true; } diff --git a/src/SwapChain.h b/src/SwapChain.h index dbafcf0..d3902a7 100644 --- a/src/SwapChain.h +++ b/src/SwapChain.h @@ -24,7 +24,7 @@ class SwapChain { private: SwapChain(Device* device, VkSurfaceKHR vkSurface, unsigned int numBuffers); - void Create(); + void Create(VkSwapchainKHR oldSwapChain = VK_NULL_HANDLE); void Destroy(); Device* device; diff --git a/src/main.cpp b/src/main.cpp index 8bf822b..8ad7fa1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,4 +1,7 @@ #include +#include +#include +#include #include "Instance.h" #include "Window.h" #include "Renderer.h" @@ -16,8 +19,36 @@ namespace { if (width == 0 || height == 0) return; vkDeviceWaitIdle(device->GetVkDevice()); - swapChain->Recreate(); - renderer->RecreateFrameResources(); + + float aspectRatio = static_cast(width) / static_cast(height); + camera->UpdateAspectRatio(aspectRatio); + + // Check if window is actually valid before proceeding + // Sometimes GLFW reports non-zero but surface capabilities return zero + int fbWidth, fbHeight; + glfwGetFramebufferSize(window, &fbWidth, &fbHeight); + if (fbWidth == 0 || fbHeight == 0) { + // Window is minimized or invalid - skip recreation + return; + } + + // Destroy frame resources first (image views and framebuffers that reference old swap chain images) + // This must be done BEFORE recreating the swap chain + renderer->DestroyFrameResources(); + + // Now recreate the swap chain (old image views are already destroyed, so it's safe) + try { + swapChain->Recreate(); + + // Recreate all frame resources with the new swap chain + // Note: RecreateFrameResources will call DestroyFrameResources again, but it's safe (handles null checks) + renderer->RecreateFrameResources(); + } catch (const std::exception& e) { + // If swap chain recreation fails (e.g., window minimized), + // we need to recreate frame resources with the old swap chain + // RecreateFrameResources will handle this + renderer->RecreateFrameResources(); + } } bool leftMouseDown = false; @@ -143,10 +174,42 @@ int main() { glfwSetMouseButtonCallback(GetGLFWWindow(), mouseDownCallback); glfwSetCursorPosCallback(GetGLFWWindow(), mouseMoveCallback); + // Frametime tracking + auto lastTime = std::chrono::high_resolution_clock::now(); + auto frameTimeUpdate = lastTime; + float averageFrametime = 16.67f; // Initialize with 60 FPS + const float updateInterval = 0.25f; // Update every 0.25 seconds for smoother display + std::string currentTitle = "Vulkan Grass Rendering - FPS: 60.0 | Frametime: 16.67 ms"; + while (!ShouldQuit()) { + auto currentTime = std::chrono::high_resolution_clock::now(); + auto frameDuration = std::chrono::duration(currentTime - lastTime); + float frametimeMs = frameDuration.count(); + lastTime = currentTime; + glfwPollEvents(); scene->UpdateTime(); renderer->Frame(); + + // Update window title with FPS and frametime at regular intervals + auto timeSinceUpdate = std::chrono::duration(currentTime - frameTimeUpdate).count(); + if (timeSinceUpdate >= updateInterval) { + // Use exponential moving average for smoother frametime display + averageFrametime = averageFrametime * 0.7f + frametimeMs * 0.3f; + float fps = 1000.0f / averageFrametime; + + // Always build the complete title string + std::stringstream title; + title << "Vulkan Grass Rendering - FPS: " << std::fixed << std::setprecision(1) << fps + << " | Frametime: " << std::setprecision(2) << averageFrametime << " ms"; + currentTitle = title.str(); + glfwSetWindowTitle(GetGLFWWindow(), currentTitle.c_str()); + + frameTimeUpdate = currentTime; + } else { + // Ensure title is always set, even between updates + glfwSetWindowTitle(GetGLFWWindow(), currentTitle.c_str()); + } } vkDeviceWaitIdle(device->GetVkDevice()); diff --git a/src/shaders/compute.comp b/src/shaders/compute.comp index 0fd0224..69268d3 100644 --- a/src/shaders/compute.comp +++ b/src/shaders/compute.comp @@ -2,6 +2,20 @@ #extension GL_ARB_separate_shader_objects : enable #define WORKGROUP_SIZE 32 +#define USE_FORCES 1 +#define USE_CULLING 1 +// The derectives below are only meaningful when USE_CULLING is 1 +#define USE_ORIENTATION_CULLING 1 +#define USE_VIEW_FRUSTUM_CULLING 1 +#define USE_DISTANCE_CULLING 1 + +// Parameters for the grass algorithm +#define WIND_STRENGTH 5.0f +#define WIND_FREQUENCY 1.0f +#define WIND_TURBULENCE 6.5f +#define CULLING_DISTANCE 30.0f +#define CULLING_BINS 10 + layout(local_size_x = WORKGROUP_SIZE, local_size_y = 1, local_size_z = 1) in; layout(set = 0, binding = 0) uniform CameraBufferObject { @@ -12,7 +26,7 @@ layout(set = 0, binding = 0) uniform CameraBufferObject { layout(set = 1, binding = 0) uniform Time { float deltaTime; float totalTime; -}; +} time; struct Blade { vec4 v0; @@ -21,36 +35,165 @@ struct Blade { vec4 up; }; +// The project is using vkCmdDrawIndirect to use a buffer as the arguments for a draw call +// This is sort of an advanced feature so we've showed you what this buffer should look like + // TODO: Add bindings to: // 1. Store the input blades +layout(set = 2, binding = 0) buffer InputBlades { + Blade blades[]; +} inputBlades; + // 2. Write out the culled blades -// 3. Write the total number of blades remaining +layout(set = 2, binding = 1) buffer CulledBlades { + Blade blades[]; +} outputBlades; -// The project is using vkCmdDrawIndirect to use a buffer as the arguments for a draw call -// This is sort of an advanced feature so we've showed you what this buffer should look like -// -// layout(set = ???, binding = ???) buffer NumBlades { -// uint vertexCount; // Write the number of blades remaining here -// uint instanceCount; // = 1 -// uint firstVertex; // = 0 -// uint firstInstance; // = 0 -// } numBlades; +// 3. Write the total number of blades remaining +layout(set = 2, binding = 2) buffer NumBlades { + uint vertexCount; // Write the number of blades remaining here + uint instanceCount; // = 1 + uint firstVertex; // = 0 + uint firstInstance; // = 0 +} numBlades; bool inBounds(float value, float bounds) { return (value >= -bounds) && (value <= bounds); } +vec3 getWindVector(vec3 v) { + // Time-based oscillation for smooth wind variation + float windX = WIND_STRENGTH * sin(time.totalTime * WIND_FREQUENCY); + + // Turbulence using position-based noise to make wind vary per blade + float windZ = WIND_TURBULENCE * sin(dot(v.xz, vec2(12.9898, 78.233)) * 43758.5453 + time.totalTime * WIND_FREQUENCY); + + // Fixed Y component for consistent vertical influence + float windY = 0.2; + + return vec3(windX, windY, windZ); +} + void main() { // Reset the number of blades to 0 if (gl_GlobalInvocationID.x == 0) { - // numBlades.vertexCount = 0; + numBlades.vertexCount = 0; } barrier(); // Wait till all threads reach this point + uint bladeIdx = gl_GlobalInvocationID.x; + Blade curBlade = inputBlades.blades[bladeIdx]; + vec3 v0 = curBlade.v0.xyz; + vec3 v1 = curBlade.v1.xyz; + vec3 v2 = curBlade.v2.xyz; + vec3 up = curBlade.up.xyz; + float orientation = curBlade.v0.w; + float height = curBlade.v1.w; + float width = curBlade.v2.w; + float stiffness = curBlade.up.w; + + vec3 s = vec3(cos(orientation), 0.0, sin(orientation)); + vec3 f = normalize(cross(up, s)); + // TODO: Apply forces on every blade and update the vertices in the buffer + #if USE_FORCES + // Gravity + const vec4 D = vec4(0.0, -1.0, 0.0, 9.8); + vec3 gE = normalize(D.xyz) * D.w; + vec3 gF = 0.25 * length(gE) * f; + vec3 g = gE + gF; + + // Recovery + vec3 iv2 = v0 + up * height; + vec3 r = (iv2 - v2) * stiffness; + + // Wind + vec3 wi = getWindVector(v0); + vec3 diff = v2 - v0; + float fd = 1.0f - abs(dot(normalize(wi), normalize(diff))); + float fr = dot(diff, up) / height; + vec3 w = wi * fd * fr; + + // Move the blade + vec3 translation = (g + r + w) * time.deltaTime; + v2 += translation; + + // Validation + v2 -= up * min(0.0f, dot(v2 - v0, up)); // ensure v2 is always above the ground + vec3 v2_minus_v0 = v2 - v0; + float l_proj = length(v2_minus_v0 - up * dot(up, v2_minus_v0)); + float l_proj_div_height = l_proj / height; + vec3 v1_tmp = v0 + height * up * max(1.0f - l_proj_div_height, 0.05 * max(l_proj_div_height, 1.0f)); // ensure the valid position for v1 + float L0 = distance(v0, v2); + float L1 = distance(v0, v1_tmp) + distance(v1_tmp, v2); + float L = (2.0f * L0 + L1) / 3.0f; + float ratio = height / max(L, 0.0001f); + // ensure the length of the blade is always height + v1 = v0 + ratio * (v1_tmp - v0); + v2 = v1 + ratio * (v2 - v1_tmp); + + Blade updatedBlade = Blade(curBlade.v0, vec4(v1, height), vec4(v2, width), curBlade.up); + inputBlades.blades[bladeIdx] = updatedBlade; + #else + Blade updatedBlade = curBlade; + #endif // TODO: Cull blades that are too far away or not in the camera frustum and write them // to the culled blades buffer // Note: to do this, you will need to use an atomic operation to read and update numBlades.vertexCount // You want to write the visible blades to the buffer without write conflicts between threads -} + bool culled = false; + + #if USE_CULLING + #if USE_ORIENTATION_CULLING + // Orientation Culling + vec4 side_vec = vec4(s, 0.0); + vec3 dir_b = normalize((camera.view * side_vec).xyz); + vec3 dir_c = normalize((camera.view * vec4(v0, 1.0)).xyz); + bool is_orientation_culled = abs(dot(dir_b, dir_c)) > 0.9f; + culled = culled || is_orientation_culled; + #endif + + #if USE_VIEW_FRUSTUM_CULLING + // View Frustum Culling + mat4 viewProj = camera.proj * camera.view; + vec3 m = 0.25 * v0 + 0.5 * v1 + 0.25 * v2; + vec4 v0_clip = (viewProj * vec4(v0, 1.0)); + vec4 v2_clip = (viewProj * vec4(v2, 1.0)); + vec4 m_clip = (viewProj * vec4(m, 1.0)); + float t = 0.01; + float v0_tolerance = v0_clip.w + t; + float v2_tolerance = v2_clip.w + t; + float m_tolerance = m_clip.w + t; + bool in_frustum = inBounds(v0_clip.x, v0_tolerance) && inBounds(v0_clip.y, v0_tolerance) && inBounds(v0_clip.z, v0_tolerance) || + inBounds(v2_clip.x, v2_tolerance) && inBounds(v2_clip.y, v2_tolerance) && inBounds(v2_clip.z, v2_tolerance) || + inBounds(m_clip.x, m_tolerance) && inBounds(m_clip.y, m_tolerance) && inBounds(m_clip.z, m_tolerance); + culled = culled || !in_frustum; + #endif + + #if USE_DISTANCE_CULLING + // Distance Culling + // Extract the rotation part (upper 3x3 matrix) + mat3 rotationMatrix = mat3(camera.view); + // Extract the translation part (the last row of the view matrix) + vec3 cam_translation = vec3(camera.view[3][0], camera.view[3][1], camera.view[3][2]); + // Calculate the camera position by undoing the rotation and translation + vec3 c = -transpose(rotationMatrix) * cam_translation; + vec3 camera_to_blade = v0 - c; + vec3 projected_up = dot(camera_to_blade, up) * up; + float d_proj = length(camera_to_blade - projected_up); + d_proj = clamp(d_proj, 0.0f, CULLING_DISTANCE); + bool is_too_far = bladeIdx % CULLING_BINS > floor(CULLING_BINS * (1.0f - d_proj / CULLING_DISTANCE)); + culled = culled || is_too_far; + #endif + + // Write to the output buffer + if (!culled) { + uint idx = atomicAdd(numBlades.vertexCount, 1); + outputBlades.blades[idx] = updatedBlade; + } + #else + uint idx = atomicAdd(numBlades.vertexCount, 1); + outputBlades.blades[idx] = updatedBlade; + #endif +} diff --git a/src/shaders/grass.frag b/src/shaders/grass.frag index c7df157..ff26ce1 100644 --- a/src/shaders/grass.frag +++ b/src/shaders/grass.frag @@ -1,17 +1,43 @@ #version 450 #extension GL_ARB_separate_shader_objects : enable +// For debugging +#define USE_TESS_LEVEL_AS_COLOR 0 + layout(set = 0, binding = 0) uniform CameraBufferObject { mat4 view; mat4 proj; } camera; // TODO: Declare fragment shader inputs +layout(location = 0) in vec2 inUV; +/** For Rendering Tessellation **/ +layout(location = 1) in float inTessLevel; +layout(location = 2) in float inMinTessLevel; +layout(location = 3) in float inMaxTessLevel; layout(location = 0) out vec4 outColor; void main() { // TODO: Compute fragment color + // Define base colors for the grass + /** Green **/ + // vec3 grassLightColor = vec3(0.4, 0.7, 0.3); + // vec3 grassDarkColor = vec3(0.2, 0.6, 0.2); + /** Salmon **/ + vec3 grassLightColor = vec3(251, 196, 171) / 255.0; + vec3 grassDarkColor = vec3(240, 128, 128) / 255.0; + + // Apply a vertical gradient to simulate lighting (slightly dark at the base) + float gradient = smoothstep(0.2, 0.8, inUV.y); + + // Blend the base color with noise for subtle variation + vec3 grassColor = mix(grassDarkColor, grassLightColor, gradient); - outColor = vec4(1.0); + #if USE_TESS_LEVEL_AS_COLOR + float normalised_tess_level = (inTessLevel - inMinTessLevel) / (inMaxTessLevel - inMinTessLevel); + outColor = vec4(normalised_tess_level); + #else + outColor = vec4(grassColor, 1.0); + #endif } diff --git a/src/shaders/grass.tesc b/src/shaders/grass.tesc index f9ffd07..00ae694 100644 --- a/src/shaders/grass.tesc +++ b/src/shaders/grass.tesc @@ -1,6 +1,10 @@ #version 450 #extension GL_ARB_separate_shader_objects : enable +#define MAX_TESS_LEVEL 10.0 +#define MIN_TESS_LEVEL 2.0 +#define DISTANCE_FALL_OFF 50.0 + layout(vertices = 1) out; layout(set = 0, binding = 0) uniform CameraBufferObject { @@ -9,18 +13,43 @@ layout(set = 0, binding = 0) uniform CameraBufferObject { } camera; // TODO: Declare tessellation control shader inputs and outputs +layout(location = 0) in vec3 inV0[]; +layout(location = 1) in vec3 inV1[]; +layout(location = 2) in vec3 inV2[]; +layout(location = 3) in vec3 inParams[]; + +layout(location = 0) out vec3 outV0[]; +layout(location = 1) out vec3 outV1[]; +layout(location = 2) out vec3 outV2[]; +layout(location = 3) out vec3 outParams[]; +layout(location = 4) out float outTessLevel[]; // For debugging +layout(location = 5) out float outMinTessLevel[]; // For debugging +layout(location = 6) out float outMaxTessLevel[]; // For debugging void main() { // Don't move the origin location of the patch - gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position; - - // TODO: Write any shader outputs + // TODO: Write any shader outputs + outV0[gl_InvocationID] = inV0[gl_InvocationID]; + outV1[gl_InvocationID] = inV1[gl_InvocationID]; + outV2[gl_InvocationID] = inV2[gl_InvocationID]; + outParams[gl_InvocationID] = inParams[gl_InvocationID]; + /** For Rendering Tessellation **/ + outMinTessLevel[gl_InvocationID] = MIN_TESS_LEVEL; + outMaxTessLevel[gl_InvocationID] = MAX_TESS_LEVEL; // TODO: Set level of tesselation - // gl_TessLevelInner[0] = ??? - // gl_TessLevelInner[1] = ??? - // gl_TessLevelOuter[0] = ??? - // gl_TessLevelOuter[1] = ??? - // gl_TessLevelOuter[2] = ??? - // gl_TessLevelOuter[3] = ??? + // Calculate the distance from the blade to the camera + vec3 bladePosition = inV0[gl_InvocationID]; // Use inV0 as the base position of the blade + vec3 cameraPosition = vec3(inverse(camera.view)[3]); // Extract camera position from view matrix + float distanceToCamera = distance(bladePosition, cameraPosition); + float tessellationFactor = mix(MAX_TESS_LEVEL, MIN_TESS_LEVEL, clamp(distanceToCamera / DISTANCE_FALL_OFF, 0.0, 1.0)); + + gl_TessLevelInner[0] = tessellationFactor; + gl_TessLevelInner[1] = tessellationFactor; + gl_TessLevelOuter[0] = tessellationFactor; + gl_TessLevelOuter[1] = tessellationFactor; + gl_TessLevelOuter[2] = tessellationFactor; + gl_TessLevelOuter[3] = tessellationFactor; + /** For Rendering Tessellation **/ + outTessLevel[gl_InvocationID] = tessellationFactor; } diff --git a/src/shaders/grass.tese b/src/shaders/grass.tese index 751fff6..82b6c46 100644 --- a/src/shaders/grass.tese +++ b/src/shaders/grass.tese @@ -9,10 +9,50 @@ layout(set = 0, binding = 0) uniform CameraBufferObject { } camera; // TODO: Declare tessellation evaluation shader inputs and outputs +layout(location = 0) in vec3 inV0[]; +layout(location = 1) in vec3 inV1[]; +layout(location = 2) in vec3 inV2[]; +layout(location = 3) in vec3 inParams[]; +/** For Rendering Tessellation **/ +layout(location = 4) in float inTessLevel[]; +layout(location = 5) in float inMinTessLevel[]; +layout(location = 6) in float inMaxTessLevel[]; + +layout(location = 0) out vec2 outUV; +/** For Rendering Tessellation **/ +layout(location = 1) out float outTessLevel; +layout(location = 2) out float outMinTessLevel; +layout(location = 3) out float outMaxTessLevel; void main() { float u = gl_TessCoord.x; float v = gl_TessCoord.y; // TODO: Use u and v to parameterize along the grass blade and output positions for each vertex of the grass blade + vec3 v0 = inV0[0]; + vec3 v1 = inV1[0]; + vec3 v2 = inV2[0]; + float o = inParams[0].x; + vec3 t1 = vec3(cos(o), 0.0f, sin(o)); + float w = inParams[0].z; + + vec3 a = v0 + v * (v1 - v0); + vec3 b = v1 + v * (v2 - v1); + vec3 c = a + v * (b - a); + vec3 c0 = c - w * t1; + vec3 c1 = c + w * t1; + vec3 t0 = normalize(b - a); + vec3 n = normalize(cross(t0, t1)); + + // for quad t = u + // below is for triangle: + float t = u + 0.5 * v - u * v; + vec3 p = (1.0f - t) * c0 + t * c1; + gl_Position = camera.proj * camera.view * vec4(p, 1.0); + + outUV = vec2(u, v); + /** For Rendering Tessellation **/ + outTessLevel = inTessLevel[0]; + outMinTessLevel = inMinTessLevel[0]; + outMaxTessLevel = inMaxTessLevel[0]; } diff --git a/src/shaders/grass.vert b/src/shaders/grass.vert index db9dfe9..f7151c9 100644 --- a/src/shaders/grass.vert +++ b/src/shaders/grass.vert @@ -1,4 +1,3 @@ - #version 450 #extension GL_ARB_separate_shader_objects : enable @@ -7,11 +6,24 @@ layout(set = 1, binding = 0) uniform ModelBufferObject { }; // TODO: Declare vertex shader inputs and outputs +layout(location = 0) in vec4 v0; +layout(location = 1) in vec4 v1; +layout(location = 2) in vec4 v2; +layout(location = 3) in vec4 up; -out gl_PerVertex { - vec4 gl_Position; -}; +layout(location = 0) out vec3 outV0; +layout(location = 1) out vec3 outV1; +layout(location = 2) out vec3 outV2; +layout(location = 3) out vec3 outParams; void main() { // TODO: Write gl_Position and any other shader outputs + vec4 worldV0 = model * vec4(v0.xyz, 1.0); + vec4 worldV1 = model * vec4(v1.xyz, 1.0); + vec4 worldV2 = model * vec4(v2.xyz, 1.0); + + outV0 = worldV0.xyz; + outV1 = worldV1.xyz; + outV2 = worldV2.xyz; + outParams = vec3(v0.w, v1.w, v2.w); }