diff --git a/README.md b/README.md index 20ee451..6c95a58 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,124 @@ 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) +* Joanna Fisch + * [LinkedIn](https://www.linkedin.com/in/joanna-fisch-bb2979186/), [Website](https://sites.google.com/view/joannafischsportfolio/home) +* Tested on: Windows 11, i7-12700H @ 2.30GHz 16GB, NVIDIA GeForce RTX 3060 (Laptop) -### (TODO: Your README) +### Introduction -*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. +This project implements a grass simulation and renderer using Vulkan, based on the paper [Responsive Real-Time Grass Rendering for General 3D Scenes](https://www.cg.tuwien.ac.at/research/publications/2017/JAHRMANN-2017-RRTG/JAHRMANN-2017-RRTG-draft.pdf). It uses compute shaders to simulate forces on Bezier curves, which represent individual grass blades, and graphics shaders to dynamically tessellate and shade the grass. Key features include physics-based blade movement, culling techniques to optimize performance, and tessellation shaders to shape the grass blades. + +### Features Implemented +* **Compute Shader for Physics Calculations:** Forces are applied to simulate gravity, wind, and recovery on each grass blade. Adjustments ensure blades retain realistic movement. +* **Vertex Shader:** Applies transformations to the Bezier control points, preparing the blades for tessellation. +* **Tessellation Control and Evaluation Shaders:** Dynamically generate detailed grass geometry, creating realistic curvature in blades. The evaluation shader converts tessellated patches into clip space for rendering. +* **Fragment Shader:** Handles shading based on environmental lighting, producing a natural grass appearance. +* **Resource Binding:** Storage buffers store grass blade data across frames, and descriptors manage compute pipeline resources. +* **Culling Techniques:** + * _Orientation Culling:_ Avoids rendering blades angled perpendicular to the camera. + * _View-Frustum Culling:_ Skips blades outside the camera view. + * _Distance Culling:_ Limits rendering for distant blades to improve performance. +* **Extra Credit:** Variable tessellation levels based on camera distance for optimized detail. + +### Project Stages +1. Initial Blade Display - Lighting + * The grass in the rendering is lit using Lambertian shading, where ambient and diffuse lighting are combined to produce a natural effect. This shader code adjusts the grass color based on height, producing a darker green near the base and lighter green at the top, to simulate natural lighting and height effects. + + + + + + + + + + + + + + + + +
Diffuse Lighting Position Normal Full Lighting
Grass color based on height and ambient and diffuse lighting
+ +2. Physics Simulation + + + + + + + + + + + + +
No forces With Forces
Gravity, wind, and recovery forces affecting blade movement
+ +3. Culling Optimization + + + + + + + + + + + + + + +
Orientation Culling View-Frustum Culling Distance Culling
Culling based on orientation, distance, and frustum tests
+ +4. Tesselation + * The variable tessellation levels refine the geometry of each grass blade based on camera distance, creating a more detailed appearance without an excessive increase in the vertex count, which is critical for efficiently rendering complex scenes like grass fields. + + + + + + + + + +
Tesselation Max Distance 50 Tesselation Max Distance 30
+ +### Performance Analysis +#### Varying Blade Counts +Testing different numbers of grass blades to analyze frame rate and memory usage, as shown in the table below: + +To calculate the memory usage as a percentage of the memory budget for each heap, we use the following formula: + +**Memory Usage Percentage** = (Usage / Budget) * 100 + + where: +- **Usage** is the memory currently in use (in MB). +- **Budget** is the maximum available memory for that heap (in MB). + +For each performance scenario, the total memory usage is calculated by summing the memory usage across all heaps and computing the overall usage percentage relative to the total budget. + +| Blade Count | FPS | Memory Usage (MB) | % of Budget | +|-------|-------------|-------------|-------------| +| 2^5 | 1958 | 57.8867 | 0.46% | +| 2^9 | 1678 | 57.9141 | 0.46% | +| 2^13 | 1552 | 58.8203 | 0.47% | +| 2^17 | 329 | 73.8203 | 0.59% | + +> **Note:** Frame rate values are in frames per second (FPS). + +#### Culling Performance + +| Culling Technique | FPS | +|-------|-------------| +| No Culling | 1162 | +| Orientation Culling | 1288 | +| View-Frustum Culling | 1293 | +| Distance Culling | 1385 | +| Full Culling | 1535 | diff --git a/img/distance_culling.gif b/img/distance_culling.gif new file mode 100644 index 0000000..0b94fbe Binary files /dev/null and b/img/distance_culling.gif differ diff --git a/img/distance_culling_2.gif b/img/distance_culling_2.gif new file mode 100644 index 0000000..d6b2144 Binary files /dev/null and b/img/distance_culling_2.gif differ diff --git a/img/frustrum_culling.gif b/img/frustrum_culling.gif new file mode 100644 index 0000000..f30ecab Binary files /dev/null and b/img/frustrum_culling.gif differ diff --git a/img/frustrum_culling_2.gif b/img/frustrum_culling_2.gif new file mode 100644 index 0000000..ea1037d Binary files /dev/null and b/img/frustrum_culling_2.gif differ diff --git a/img/full_demo.gif b/img/full_demo.gif new file mode 100644 index 0000000..c8b3bb9 Binary files /dev/null and b/img/full_demo.gif differ diff --git a/img/grass_diffuse.png b/img/grass_diffuse.png new file mode 100644 index 0000000..3addad5 Binary files /dev/null and b/img/grass_diffuse.png differ diff --git a/img/grass_full.png b/img/grass_full.png new file mode 100644 index 0000000..51ce1b2 Binary files /dev/null and b/img/grass_full.png differ diff --git a/img/grass_noForce.png b/img/grass_noForce.png new file mode 100644 index 0000000..78d10e8 Binary files /dev/null and b/img/grass_noForce.png differ diff --git a/img/grass_nor.png b/img/grass_nor.png new file mode 100644 index 0000000..de989cc Binary files /dev/null and b/img/grass_nor.png differ diff --git a/img/grass_pos.png b/img/grass_pos.png new file mode 100644 index 0000000..26500dd Binary files /dev/null and b/img/grass_pos.png differ diff --git a/img/grass_tesselationLevel.png b/img/grass_tesselationLevel.png new file mode 100644 index 0000000..b4b01de Binary files /dev/null and b/img/grass_tesselationLevel.png differ diff --git a/img/grass_tesselationLevel_2.png b/img/grass_tesselationLevel_2.png new file mode 100644 index 0000000..71ef707 Binary files /dev/null and b/img/grass_tesselationLevel_2.png differ diff --git a/img/orientation_culling.gif b/img/orientation_culling.gif new file mode 100644 index 0000000..0d8a3c9 Binary files /dev/null and b/img/orientation_culling.gif differ diff --git a/img/orientation_culling_2.gif b/img/orientation_culling_2.gif new file mode 100644 index 0000000..0334777 Binary files /dev/null and b/img/orientation_culling_2.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/Instance.cpp b/src/Instance.cpp index 7f6b01c..4775e66 100644 --- a/src/Instance.cpp +++ b/src/Instance.cpp @@ -49,7 +49,7 @@ Instance::Instance(const char* applicationName, unsigned int additionalExtension appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0); appInfo.pEngineName = "No Engine"; appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0); - appInfo.apiVersion = VK_API_VERSION_1_0; + appInfo.apiVersion = VK_API_VERSION_1_1; // --- Create Vulkan instance --- VkInstanceCreateInfo createInfo = {}; diff --git a/src/Renderer.cpp b/src/Renderer.cpp index b445d04..f28748c 100644 --- a/src/Renderer.cpp +++ b/src/Renderer.cpp @@ -198,6 +198,38 @@ 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 // will be stored at each binding + VkDescriptorSetLayoutBinding storeBladesBinding = {}; + storeBladesBinding.binding = 0; + storeBladesBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + storeBladesBinding.descriptorCount = 1; + storeBladesBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + storeBladesBinding.pImmutableSamplers = nullptr; + + VkDescriptorSetLayoutBinding culledBladesBinding = {}; + culledBladesBinding.binding = 1; + culledBladesBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + culledBladesBinding.descriptorCount = 1; + culledBladesBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + culledBladesBinding.pImmutableSamplers = nullptr; + + VkDescriptorSetLayoutBinding numBladesBinding = {}; + numBladesBinding.binding = 2; + numBladesBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + numBladesBinding.descriptorCount = 1; + numBladesBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + numBladesBinding.pImmutableSamplers = nullptr; + + std::vector bindings = { storeBladesBinding, culledBladesBinding, numBladesBinding }; + + // 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 compute descriptor set layout"); + } } void Renderer::CreateDescriptorPool() { @@ -216,6 +248,7 @@ void Renderer::CreateDescriptorPool() { { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER , 1 }, // TODO: Add any additional types and counts of descriptors you will need to allocate + { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER , static_cast(3 * scene->GetBlades().size()) }, }; VkDescriptorPoolCreateInfo poolInfo = {}; @@ -320,6 +353,42 @@ void Renderer::CreateModelDescriptorSets() { 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 + grassDescriptorSets.resize(scene->GetBlades().size()); + + // Describe the desciptor set + VkDescriptorSetLayout layouts[] = { modelDescriptorSetLayout }; + VkDescriptorSetAllocateInfo allocInfo = {}; + allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + allocInfo.descriptorPool = descriptorPool; + allocInfo.descriptorSetCount = static_cast(grassDescriptorSets.size()); + allocInfo.pSetLayouts = layouts; + + // Allocate descriptor sets + if (vkAllocateDescriptorSets(logicalDevice, &allocInfo, grassDescriptorSets.data()) != VK_SUCCESS) { + throw std::runtime_error("Failed to allocate grass descriptor set"); + } + + std::vector descriptorWrites(grassDescriptorSets.size()); + + for (uint32_t i = 0; i < scene->GetBlades().size(); ++i) { + VkDescriptorBufferInfo modelBufferInfo = {}; + modelBufferInfo.buffer = scene->GetBlades()[i]->GetModelBuffer(); + modelBufferInfo.offset = 0; + modelBufferInfo.range = sizeof(ModelBufferObject); + + descriptorWrites[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + descriptorWrites[i].dstSet = grassDescriptorSets[i]; + descriptorWrites[i].dstBinding = 0; + descriptorWrites[i].dstArrayElement = 0; + descriptorWrites[i].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + descriptorWrites[i].descriptorCount = 1; + descriptorWrites[i].pBufferInfo = &modelBufferInfo; + descriptorWrites[i].pImageInfo = nullptr; + descriptorWrites[i].pTexelBufferView = nullptr; + } + + // Update descriptor sets + vkUpdateDescriptorSets(logicalDevice, static_cast(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr); } void Renderer::CreateTimeDescriptorSet() { @@ -360,6 +429,69 @@ 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 set"); + } + + std::vector descriptorWrites(3 * computeDescriptorSets.size()); + + for (uint32_t i = 0; i < scene->GetBlades().size(); ++i) { + VkDescriptorBufferInfo storedBladesBufferInfo = {}; + storedBladesBufferInfo.buffer = scene->GetBlades()[i]->GetBladesBuffer(); + storedBladesBufferInfo.offset = 0; + storedBladesBufferInfo.range = NUM_BLADES * sizeof(Blade); + 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 = &storedBladesBufferInfo; + descriptorWrites[3 * i + 0].pImageInfo = nullptr; + descriptorWrites[3 * i + 0].pTexelBufferView = nullptr; + + VkDescriptorBufferInfo culledBladesBufferInfo = {}; + culledBladesBufferInfo.buffer = scene->GetBlades()[i]->GetCulledBladesBuffer(); + culledBladesBufferInfo.offset = 0; + culledBladesBufferInfo.range = NUM_BLADES * sizeof(Blade); + 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 = &culledBladesBufferInfo; + descriptorWrites[3 * i + 1].pImageInfo = nullptr; + descriptorWrites[3 * i + 1].pTexelBufferView = nullptr; + + VkDescriptorBufferInfo numBladesBufferInfo = {}; + numBladesBufferInfo.buffer = scene->GetBlades()[i]->GetNumBladesBuffer(); + numBladesBufferInfo.offset = 0; + numBladesBufferInfo.range = sizeof(BladeDrawIndirect); + 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() { @@ -717,7 +849,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 = {}; @@ -884,6 +1016,14 @@ 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 j = 0; j < scene->GetBlades().size(); ++j) { + + // Bind the descriptor set for each group of blades + vkCmdBindDescriptorSets(computeCommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, computePipelineLayout, 2, 1, &computeDescriptorSets[j], 0, nullptr); + + // Draw + vkCmdDispatch(computeCommandBuffer, (NUM_BLADES) / WORKGROUP_SIZE, 1, 1); + } // ~ End recording ~ if (vkEndCommandBuffer(computeCommandBuffer) != VK_SUCCESS) { @@ -973,16 +1113,17 @@ void Renderer::RecordCommandBuffers() { vkCmdBindPipeline(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, grassPipeline); for (uint32_t j = 0; j < scene->GetBlades().size(); ++j) { - VkBuffer vertexBuffers[] = { scene->GetBlades()[j]->GetCulledBladesBuffer() }; + VkBuffer vertexBuffers[] = { scene->GetBlades()[j]->GetCulledBladesBuffer() }; // GetCulledBladesBuffer VkDeviceSize offsets[] = { 0 }; // TODO: Uncomment this when the buffers are populated - // vkCmdBindVertexBuffers(commandBuffers[i], 0, 1, vertexBuffers, offsets); + vkCmdBindVertexBuffers(commandBuffers[i], 0, 1, vertexBuffers, offsets); // TODO: Bind the descriptor set for each grass blades model + vkCmdBindDescriptorSets(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, grassPipelineLayout, 1, 1, &grassDescriptorSets[j], 0, nullptr); // 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 @@ -1057,6 +1198,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..a2a47f7 100644 --- a/src/Renderer.h +++ b/src/Renderer.h @@ -55,12 +55,15 @@ class Renderer { VkDescriptorSetLayout cameraDescriptorSetLayout; VkDescriptorSetLayout modelDescriptorSetLayout; + VkDescriptorSetLayout computeDescriptorSetLayout; VkDescriptorSetLayout timeDescriptorSetLayout; VkDescriptorPool descriptorPool; VkDescriptorSet cameraDescriptorSet; std::vector modelDescriptorSets; + std::vector grassDescriptorSets; + std::vector computeDescriptorSets; VkDescriptorSet timeDescriptorSet; VkPipelineLayout graphicsPipelineLayout; diff --git a/src/SwapChain.cpp b/src/SwapChain.cpp index 711fec0..6ab6ed0 100644 --- a/src/SwapChain.cpp +++ b/src/SwapChain.cpp @@ -74,14 +74,17 @@ SwapChain::SwapChain(Device* device, VkSurfaceKHR vkSurface, unsigned int numBuf } } -void SwapChain::Create() { +void SwapChain::Create(int w, int h) { auto* instance = device->GetInstance(); const auto& surfaceCapabilities = instance->GetSurfaceCapabilities(); VkSurfaceFormatKHR surfaceFormat = chooseSwapSurfaceFormat(instance->GetSurfaceFormats()); VkPresentModeKHR presentMode = chooseSwapPresentMode(instance->GetPresentModes()); - VkExtent2D extent = chooseSwapExtent(surfaceCapabilities, GetGLFWWindow()); + VkExtent2D extent{ w, h }; + if (w == 0 || h == 0) { + extent = chooseSwapExtent(surfaceCapabilities, GetGLFWWindow()); + } uint32_t imageCount = surfaceCapabilities.minImageCount + 1; imageCount = numBuffers > imageCount ? numBuffers : imageCount; @@ -188,9 +191,9 @@ VkSemaphore SwapChain::GetRenderFinishedVkSemaphore() const { return renderFinishedSemaphore; } -void SwapChain::Recreate() { +void SwapChain::Recreate(int w, int h) { Destroy(); - Create(); + Create(w, h); } bool SwapChain::Acquire() { diff --git a/src/SwapChain.h b/src/SwapChain.h index dbafcf0..318b41b 100644 --- a/src/SwapChain.h +++ b/src/SwapChain.h @@ -17,14 +17,14 @@ class SwapChain { VkSemaphore GetImageAvailableVkSemaphore() const; VkSemaphore GetRenderFinishedVkSemaphore() const; - void Recreate(); + void Recreate(int w = 0, int h = 0); bool Acquire(); bool Present(); ~SwapChain(); private: SwapChain(Device* device, VkSurfaceKHR vkSurface, unsigned int numBuffers); - void Create(); + void Create(int w = 0, int h = 0); void Destroy(); Device* device; diff --git a/src/main.cpp b/src/main.cpp index 8bf822b..03ed161 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,18 +5,65 @@ #include "Camera.h" #include "Scene.h" #include "Image.h" +#include +#include Device* device; SwapChain* swapChain; Renderer* renderer; Camera* camera; +// For frame rate calculation +std::chrono::time_point lastTime = std::chrono::high_resolution_clock::now(); +int frameCount = 0; + +void calculateFPS() { + frameCount++; + auto currentTime = std::chrono::high_resolution_clock::now(); + std::chrono::duration elapsed = currentTime - lastTime; + + if (elapsed.count() >= 1.0f) { + std::cout << "FPS: " << frameCount << std::endl; + frameCount = 0; + lastTime = currentTime; + } +} + +// Function to display memory usage if VK_EXT_memory_budget is available +void printMemoryUsage() { + VkPhysicalDeviceMemoryProperties2 memoryProperties = {}; + memoryProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2; + + VkPhysicalDeviceMemoryBudgetPropertiesEXT memoryBudgetProperties = {}; + memoryBudgetProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT; + memoryProperties.pNext = &memoryBudgetProperties; + + vkGetPhysicalDeviceMemoryProperties2(device->GetInstance()->GetPhysicalDevice(), &memoryProperties); + + double totalUsageMB = 0; + double totalBudgetMB = 0; + + for (uint32_t i = 0; i < memoryProperties.memoryProperties.memoryHeapCount; i++) { + double heapUsageMB = memoryBudgetProperties.heapUsage[i] / (1024.0 * 1024.0); + double heapBudgetMB = memoryBudgetProperties.heapBudget[i] / (1024.0 * 1024.0); + + totalUsageMB += heapUsageMB; + totalBudgetMB += heapBudgetMB; + + // Logging each heap's usage and budget for debugging + std::cout << "Heap " << i << " - Budget: " << heapBudgetMB << " MB, Usage: " << heapUsageMB << " MB\n"; + } + + double memoryUsagePercentage = (totalUsageMB / totalBudgetMB) * 100; + std::cout << "| Grass Rendering - Medium Density | " << totalUsageMB << " MB (" << memoryUsagePercentage << "% of Budget) |\n"; +} + namespace { void resizeCallback(GLFWwindow* window, int width, int height) { if (width == 0 || height == 0) return; vkDeviceWaitIdle(device->GetVkDevice()); - swapChain->Recreate(); + swapChain->Recreate(width, height); renderer->RecreateFrameResources(); } @@ -143,10 +190,19 @@ int main() { glfwSetMouseButtonCallback(GetGLFWWindow(), mouseDownCallback); glfwSetCursorPosCallback(GetGLFWWindow(), mouseMoveCallback); + int count = 0; while (!ShouldQuit()) { glfwPollEvents(); scene->UpdateTime(); renderer->Frame(); + + // Output FPS and memory usage + /*if (count < 2) { + printMemoryUsage(); + count++; + }*/ + // calculateFPS(); + } vkDeviceWaitIdle(device->GetVkDevice()); diff --git a/src/shaders/compute.comp b/src/shaders/compute.comp index 0fd0224..7c3b507 100644 --- a/src/shaders/compute.comp +++ b/src/shaders/compute.comp @@ -4,6 +4,15 @@ #define WORKGROUP_SIZE 32 layout(local_size_x = WORKGROUP_SIZE, local_size_y = 1, local_size_z = 1) in; +#define RECOVERY 1 +#define GRAVITY 1 +#define WIND 1 +#define DISTANCE 30.0f +#define TOLERANCE 0.2f +#define C_ORIENTATION 1 +#define C_FRUSTRUM 1 +#define C_DISTANCE 1 + layout(set = 0, binding = 0) uniform CameraBufferObject { mat4 view; mat4 proj; @@ -27,30 +36,147 @@ struct Blade { // 3. Write the total number of blades remaining // 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; + +layout(set = 2, binding = 0) buffer StoreBlades { + Blade inBlades[]; +}; + +layout(set = 2, binding = 1) buffer CulledBlades { + Blade culledBlades[]; +}; + +layout(set = 2, binding = 2) buffer NumBlades { + uint vertexCount; //used atomic add to write number of blades remaining + uint instanceCount; // = 1 + uint firstVertex; // = 0 + uint firstInstance; // = 0 +} numBlades; bool inBounds(float value, float bounds) { return (value >= -bounds) && (value <= bounds); } +bool inBoundsZ(float value, float bounds) { + return (value >= 0.0f) && (value <= bounds); +} + +bool inFrustum(vec4 p_proj) { + float h = p_proj.w + TOLERANCE; + return inBounds(p_proj.x, h) && inBounds(p_proj.y, h) && inBoundsZ(p_proj.z, h); +} + 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 + Blade curr = inBlades[gl_GlobalInvocationID.x]; + vec3 v0 = curr.v0.xyz; + vec3 v1 = curr.v1.xyz; + vec3 v2 = curr.v2.xyz; + vec3 up = curr.up.xyz; + float orientation = curr.v0.w; + float height = curr.v1.w; + float width = curr.v2.w; + float stiffness = curr.up.w; + // TODO: Apply forces on every blade and update the vertices in the buffer + vec3 side = vec3(cos(orientation), 0.0f, sin(orientation)); + vec3 front = normalize(cross(up, side)); + + // Recovery Force +#if RECOVERY + vec3 iv2 = v0 + height * up; + vec3 recovery = (iv2 - v2) * stiffness; +#else + vec3 recovery = vec3(0.0f); +#endif + + // Gravity Force +#if GRAVITY + vec3 gE = vec3(0.0f, -9.8f, 0.0f); + vec3 gF = 0.25f * length(gE) * front; + vec3 gravity = gE + gF; +#else + vec3 gravity = vec3(0.0f); +#endif + + // Wind Force +#if WIND + vec3 wind = vec3(5.f, 0.f, 3.f) * sin(totalTime); +#else + vec3 wind = vec3(0.0f); +#endif + + // Apply forces to v2 + vec3 delta = (recovery + gravity + wind) * deltaTime; + + v2 += delta; + v2 = v2 - up * min(dot(up, (v2 - v0)), 0); + + float l_proj = length(v2 - v0 - up * dot((v2 - v0), up)); + v1 = v0 + height * up * max(1.0f - l_proj / height, 0.05 * max(l_proj / height, 1.0f)); + + float l0 = distance(v0, v2); + float l1 = distance(v0, v1) + distance(v1, v2); + float l = (l0 + l1) * 0.5f; + + float r = height / l; + + vec3 corr_v1 = v0 + r * (v1 - v0); + vec3 corr_v2 = corr_v1 + r * (v2 - v1); + + inBlades[gl_GlobalInvocationID.x].v1.xyz = corr_v1; + inBlades[gl_GlobalInvocationID.x].v2.xyz = corr_v2; // TODO: Cull blades that are too far away or not in the camera frustum and write them // to the culled blades buffer + + // cull blades whose side is parallel with camera forward +#if C_ORIENTATION + vec3 grass_Forward = normalize(vec3(camera.view * vec4(side, 0.0f))); + vec3 cam_Forward = normalize(vec3(camera.view * vec4(v0, 1.0f))); + + if (abs(dot(cam_Forward, grass_Forward)) > 0.9f) { + return; + } +#endif + + // Frustum culling for the blade using v0, midpoint m, and v2 +#if C_FRUSTRUM + mat4 vp = camera.proj * camera.view; + + // Calculate midpoint + // vec3 midpoint = (v0 + v2) * 0.5; + vec3 midpoint = 0.25 * v0 + 0.5 * v1 + 0.25 * v2; + + // Transform points to NDC space + vec4 v0_proj = vp * vec4(v0, 1.0); + vec4 m_proj = vp * vec4(midpoint, 1.0); + vec4 v2_proj = vp * vec4(v2, 1.0); + + // Test if any of the points are within the view frustum + if (!(inFrustum(v0_proj)) && !(inFrustum(m_proj)) && !(inFrustum(v2_proj))) { + return; + } +#endif + + // cull blades whose distance +#if C_DISTANCE + vec3 cam_pos = vec3(camera.view * vec4(v0, 1.0f)); + vec3 cam_up = vec3(camera.view * vec4(up, 0.0f)); + + float d_proj = length(cam_pos - cam_up * dot(cam_pos, cam_up)); + //if (d_proj > DISTANCE) return; + int n = 20; + if (gl_GlobalInvocationID.x % n < floor(float(n) * d_proj / DISTANCE)) { + return; + } +#endif + // 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 + culledBlades[atomicAdd(numBlades.vertexCount, 1)] = curr; } diff --git a/src/shaders/grass.frag b/src/shaders/grass.frag index c7df157..64adc83 100644 --- a/src/shaders/grass.frag +++ b/src/shaders/grass.frag @@ -7,11 +7,37 @@ layout(set = 0, binding = 0) uniform CameraBufferObject { } camera; // TODO: Declare fragment shader inputs +layout(location = 0) in vec3 fs_pos; +layout(location = 1) in vec3 fs_nor; +layout(location = 2) in vec2 fs_uv; +layout(location = 3) in float fs_teslevel; layout(location = 0) out vec4 outColor; +vec3 pos_light = vec3(20, 20, 5); + void main() { - // TODO: Compute fragment color + // Compute lighting (Lambertian shading) + vec3 light_Dir = normalize(fs_pos - pos_light); + float diffuseTerm = clamp(dot(light_Dir, fs_nor), 0.0f, 1.0f); + float ambientTerm = 0.2; + float lightIntensity = diffuseTerm + ambientTerm; + + // Adjust color based on height + float minHeight = 0.0; // Minimum expected height of the grass + float maxHeight = 2.0; // Maximum expected height of the grass + float heightFactor = clamp((fs_pos.y - minHeight) / (maxHeight - minHeight), 0.0, 1.0); + + // Set base green color and adjust brightness by heightFactor + vec3 baseColor = vec3(0.0, 0.6, 0.0); // Base green color for grass + vec3 heightAdjustedColor = mix(baseColor * 0.8, baseColor * 1.2, heightFactor); // Darker at low height, lighter at high + + //outColor = vec4(fs_pos, 1.0f); + //outColor = vec4(vec3(diffuseTerm), 1.0f); + //outColor = vec4(fs_nor, 1.0f); + //outColor = vec4((fs_teslevel - 1.f) / 7.f); + //outColor = vec4(0.0, lightIntensity, 0.0, 1.0); - outColor = vec4(1.0); + // Final output color with lighting applied + outColor = vec4(heightAdjustedColor * lightIntensity, 1.0f); } diff --git a/src/shaders/grass.tesc b/src/shaders/grass.tesc index f9ffd07..9f6d7c3 100644 --- a/src/shaders/grass.tesc +++ b/src/shaders/grass.tesc @@ -9,18 +9,51 @@ layout(set = 0, binding = 0) uniform CameraBufferObject { } camera; // TODO: Declare tessellation control shader inputs and outputs +in gl_PerVertex +{ + vec4 gl_Position; +} gl_in[gl_MaxPatchVertices]; + +layout(location = 0) in vec4 tcs_v1[]; +layout(location = 1) in vec4 tcs_v2[]; +layout(location = 2) in vec4 tcs_up[]; + +layout(location = 0) out vec4 tes_v1[]; +layout(location = 1) out vec4 tes_v2[]; +layout(location = 2) out vec4 tes_up[]; +layout(location = 3) out float tes_teslevel[]; 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 + tes_v1[gl_InvocationID] = tcs_v1[gl_InvocationID]; + tes_v2[gl_InvocationID] = tcs_v2[gl_InvocationID]; + tes_up[gl_InvocationID] = tcs_up[gl_InvocationID]; + + // Extract camera position from view matrix (assuming the camera's position is in the fourth column) + vec3 cam_pos = vec3(camera.view * vec4(gl_out[gl_InvocationID].gl_Position.xyz, 1.0f)); + + // Calculate the distance between the patch and the camera + float distance = length(cam_pos); + + // Set tessellation level based on distance + float maxTessLevel = 8.0; + float minTessLevel = 1.0; + float thresholdNear = 5.0; // Distance at which tessellation level is highest + float thresholdFar = 40.0; // Distance at which tessellation level is lowest - // TODO: Set level of tesselation - // gl_TessLevelInner[0] = ??? - // gl_TessLevelInner[1] = ??? - // gl_TessLevelOuter[0] = ??? - // gl_TessLevelOuter[1] = ??? - // gl_TessLevelOuter[2] = ??? - // gl_TessLevelOuter[3] = ??? + // Interpolate tessellation level based on distance + float tessLevel = mix(maxTessLevel, minTessLevel, clamp((distance - thresholdNear) / (thresholdFar - thresholdNear), 0.0, 1.0)); + + tes_teslevel[gl_InvocationID] = tessLevel; + + // Assign calculated tessellation levels + gl_TessLevelInner[0] = tessLevel; + gl_TessLevelInner[1] = tessLevel; + gl_TessLevelOuter[0] = tessLevel; + gl_TessLevelOuter[1] = tessLevel; + gl_TessLevelOuter[2] = tessLevel; + gl_TessLevelOuter[3] = tessLevel; } diff --git a/src/shaders/grass.tese b/src/shaders/grass.tese index 751fff6..c118ec2 100644 --- a/src/shaders/grass.tese +++ b/src/shaders/grass.tese @@ -9,10 +9,49 @@ layout(set = 0, binding = 0) uniform CameraBufferObject { } camera; // TODO: Declare tessellation evaluation shader inputs and outputs +layout(location = 0) in vec4 tes_v1[]; +layout(location = 1) in vec4 tes_v2[]; +layout(location = 2) in vec4 tes_up[]; +layout(location = 3) in float tes_teslevel[]; + +layout(location = 0) out vec3 fs_pos; +layout(location = 1) out vec3 fs_nor; +layout(location = 2) out vec2 fs_uv; +layout(location = 3) out float fs_teslevel; 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 = gl_in[0].gl_Position.xyz; + vec3 v1 = tes_v1[0].xyz; + vec3 v2 = tes_v2[0].xyz; + + float angle = gl_in[0].gl_Position.w; + float height = tes_v1[0].w; + float width = tes_v2[0].w; + float stiffness = tes_up[0].w; + + vec3 a = v0 + v * (v1 - v0); + vec3 b = v1 + v * (v2 - v1); + vec3 c = a + v * (b - a); + vec3 t1 = vec3(cos(angle), 0.0f, sin(angle)); // bitangent + vec3 c0 = c - width * t1; + vec3 c1 = c + width * t1; + vec3 t0 = normalize(b - a); + vec3 n = normalize(cross(t0,t1)); + + // triangle + // float t = u + 0.5f * v - u * v; + + // quadratic + float t = u - u * v * v; + + fs_pos = (1 - t) * c0 + t * c1; + fs_nor = n; + fs_uv = vec2(u,v); + fs_teslevel = tes_teslevel[0]; + + gl_Position = camera.proj * camera.view * vec4(fs_pos, 1.f); } diff --git a/src/shaders/grass.vert b/src/shaders/grass.vert index db9dfe9..3fb5e80 100644 --- a/src/shaders/grass.vert +++ b/src/shaders/grass.vert @@ -7,6 +7,15 @@ layout(set = 1, binding = 0) uniform ModelBufferObject { }; // TODO: Declare vertex shader inputs and outputs +layout(location = 0) in vec4 vs_v0; +layout(location = 1) in vec4 vs_v1; +layout(location = 2) in vec4 vs_v2; +layout(location = 3) in vec4 vs_up; + +// v0 is gl_Position +layout(location = 0) out vec4 tcs_v1; +layout(location = 1) out vec4 tcs_v2; +layout(location = 2) out vec4 tcs_up; out gl_PerVertex { vec4 gl_Position; @@ -14,4 +23,8 @@ out gl_PerVertex { void main() { // TODO: Write gl_Position and any other shader outputs + gl_Position = vec4(vec3(model * vec4(vs_v0.xyz, 1.0f)), vs_v0.w); + tcs_v1 = vec4(vec3(model * vec4(vs_v1.xyz, 1.0f)), vs_v1.w); + tcs_v2 = vec4(vec3(model * vec4(vs_v2.xyz, 1.0f)), vs_v2.w); + tcs_up = vec4(vec3(model * vec4(vs_up.xyz, 1.0f)), vs_up.w); }