diff --git a/README.md b/README.md index 20ee451..3c69432 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,144 @@ -Vulkan Grass Rendering -================================== +

Vulkan Grass Rendering

-**University of Pennsylvania, CIS 565: GPU Programming and Architecture, Project 5** +

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) +--- -### (TODO: Your README) -*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. +

+

+ + + + + + + + + + + +## Introduction +This project is my first exploration of the Vulkan API. Vulkan is a graphics and compute API that allows for high-efficiency, cross-platform access to GPUs. Vulkan is currently the industry's only open standard modern GPU API, allowing developers to write applications that are portable to a wide variety of platforms, and it includes the latest graphics technologies such as ray tracing. + +In this project, I set out to implement a grass simulator and renderer using Vulkan. The basic idea is to use compute shaders to perform physics calculations on Bezier curves, which are used to represent the individual grass blades of the final scene. This project is an implementation of 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) by Klemens Jahrmann and Michael Wimmer. Culling and tessellation techniques are employed to optimize the performance of the simulation. Culling is performed on grass blades which are not visible to the camera and thus would be a waste of resources to render. The rest of the blades are rendered by the graphics pipeline, complete with vertex, fragment and tessellation shaders. The vertex shader transforms Bezier control points, the tessellation shaders dynamically create the grass geometry from the Bezier curves, and the fragment shader is used to shade the grass blades. + +## Build Instructions +
+Click here for details on how to build and run this project. + +

+ +This project was developed using Vulkan SDK version 1.3.296 and Visual Studio 2019, and was tested on a laptop with the following specs: + +* **Machine:** ASUS ROG Zephyrus M16 +* **OS:** Windows 11 +* **Processor:** 12th Gen Intel(R) Core(TM) i9-12900H, 2500 Mhz, 14 Core(s), 20 Logical Processor(s) +* **GPU:** NVIDIA GeForce RTX 3070 Ti Laptop GPU + +The project can be built using CMake 3.30.3. +1. Set the source code path to the root folder of the project, "Project5-Vulkan-Grass-Rendering". +2. Then, create a new folder in the root folder of the project called "build". +3. Set the build path to the folder you've just created, "Project5-Vulkan-Grass-Rendering/build" +

+

+4. Press "Configure". Select "Visual Studio 16 2019" as the project generator. Then, press "Finish": +

+

+ +5. Press "Generate". If CMake was successful, you should now see a Visual Studio project, "cis565_project5_vulkan_grass_rendering.sln" in the build folder you created. +

+

+6. Open the project in Visual Studio 2019. +7. In the Solution Explorer, set "vulkan_grass_rendering" as the startup project by right-clicking the project and selecting "Set as Startup Project", as shown below: +

+

+8. Set the build mode to "Release" and press the green arrow to run the project. +

+

+ + +
+ + +## Grass Rendering using Bezier Curves +

+ +Grass blades are modeled as Bezier curves, defined by three control points: v0, v1, and v2. v0 anchors the base of the blade to the ground, v1 influences the blade's curvature above v0, and v2 determines the tip, enabling physics-based transformations like bending from forces such as wind or gravity. Each blade also has attributes like orientation, height, width, up vector, and stiffness, which are stored in four vec4 values. These attributes control the blade’s size, direction, structural integrity, and responsiveness to external forces. + +For rendering, each blade is treated as a 2D object in 3D space. The blade's shape is determined by interpolating the control points along a Bezier curve, with vertices calculated using De Casteljau’s algorithm (Jahrmann & Wimmer). This ensures the blade is smoothly aligned to the curve, allowing for realistic movement and behavior in response to dynamic forces. + +## Force Simulation +* Gravity + * Gravity combines environmental gravity (scene-wide downward pull) and front-facing gravity (blade-specific). + * Effect: Blades are pulled downward, causing them to squash towards the ground. +* Recovery + * Recovery, based on Hooke's Law, counteracts deformation and restores blades to their initial position. + * Effect: Blades return to their original shape, maintaining structural integrity. +* Wind + * Wind is calculated using a heuristic based on blade position and time, creating a swaying effect. + * Effect: Blades sway depending on wind strength, direction, and alignment. Straighter blades are more affected. + +## Culling +Although forces are simulated on every grass blade each frame, many blades don’t need to be rendered due to various factors. Culling optimizes performance by removing non-contributing blades from the render pipeline. Three main culling techniques are implemented in the compute shader: + +* #### Orientation culling + * Blades perpendicular to the view vector are culled, as they would appear too thin and create visual artifacts. + * Effect: The thinnest blades are culled based on the camera's perspective. + +* #### View-frustum culling + * Blades entirely outside the camera’s view are discarded, based on the visibility of control points (v0, v2) and the midpoint (m). + * Effect: Blades at the edges of the screen space are culled. This threshold is adjustable, ensuring that only visible blades are rendered. + +* #### Distance culling + * Blades far from the camera are culled to prevent rendering details that are indistinguishable at a distance. + * Effect: Blades are culled based on their distance from the camera, with adjustable, discrete levels to control which blades are rendered. + +## Tessellation +In this project, I also implemented distance-based LOD tessellation. Based on how close a blade of grass is to the camera (within the specified thresholds), the blades are rendered at different levels of detail. Blades which are closer to the camera are rendered with a higher level of detail, while those that are farther away are rendered with a lower level of detail. + +## Performance Analysis + +### Number of Blades vs. FPS +

+ +The graph above shows the relationship between the number of grass blades in the scene and the frame rate (frames per second). As expected, as the grass blade count increases, the FPS drops. The effect is most significant as the number of blades becomes very large. Starting out with a blade count 2^6, the FPS is consistently in the 3000s, dropping slightly lower as the blade count is increased to 2^8. The performance hit becomes more drastic as the blade count increases to 2^14 and beyond. At the least, this demonstrates the need for additional techniques to boost the performance of our simulator so that it can handle higher blade counts with ease. + +### Culling vs. FPS +

+ +The graph above showcases the performance boosts granted by the inclusion of grass blade culling in my implementation. For each of the tests, a blade count of 2^15 was used. + +As we can see, without any culling (or tessellation), the FPS was consistently around 110. +With only frustum culling, the FPS increased to 180. +Then with distance-based culling, the performance displays a significant boost, with an FPS of 240. +Finally, with all culling options enabled, we can see that the FPS increases to around 700. + +This graph demonstrates the significance of the performance boost awarded by the inclusion of various culling techniques. + + + +## Bloopers, Extras, & Final Thoughts +Overall, I really enjoyed implementing this project! I think it was a good way to get my feet wet in terms of working with the Vulkan API for the first time. It was also nice to have the paper as a guide for the implementation. + +## Meet the Dev! :wave: +

+ + +

Hi, I'm Nadine! :)
+Questions? Comments? Just want to say hi back?
+Contact me here:
+Email | + LinkedIn +

+ +## References +* [Vulkan.org](https://www.vulkan.org/) +* [NVIDIA Developer - Vulkan](https://developer.nvidia.com/vulkan) +* [IBM - Open Standard vs. Open Source](https://www.ibm.com/think/topics/open-standards-vs-open-source-explanation) +* [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) +* [Official Vulkan documentation](https://www.khronos.org/registry/vulkan/) +* [Tessellation tutorial](https://ogldev.org/www/tutorial30/tutorial30.html) \ No newline at end of file diff --git a/img/cmake.png b/img/cmake.png new file mode 100644 index 0000000..b13663d Binary files /dev/null and b/img/cmake.png differ diff --git a/img/cmake2.png b/img/cmake2.png new file mode 100644 index 0000000..dc734b6 Binary files /dev/null and b/img/cmake2.png differ diff --git a/img/cmake3.png b/img/cmake3.png new file mode 100644 index 0000000..928091e Binary files /dev/null and b/img/cmake3.png differ diff --git a/img/graph1.png b/img/graph1.png new file mode 100644 index 0000000..f8dd249 Binary files /dev/null and b/img/graph1.png differ diff --git a/img/graph2.png b/img/graph2.png new file mode 100644 index 0000000..1077755 Binary files /dev/null and b/img/graph2.png differ diff --git a/img/init.png b/img/init.png new file mode 100644 index 0000000..5f22af7 Binary files /dev/null and b/img/init.png differ diff --git a/img/nadine.png b/img/nadine.png new file mode 100644 index 0000000..a1af005 Binary files /dev/null and b/img/nadine.png differ diff --git a/img/test.gif b/img/test.gif new file mode 100644 index 0000000..3f6a10e Binary files /dev/null and b/img/test.gif differ diff --git a/img/visual_studio.png b/img/visual_studio.png new file mode 100644 index 0000000..e5b1af2 Binary files /dev/null and b/img/visual_studio.png differ diff --git a/img/visual_studio2.png b/img/visual_studio2.png new file mode 100644 index 0000000..f52f1ef Binary files /dev/null and b/img/visual_studio2.png 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..b438e24 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 << 15; 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/Renderer.cpp b/src/Renderer.cpp index b445d04..d6b8f79 100644 --- a/src/Renderer.cpp +++ b/src/Renderer.cpp @@ -195,9 +195,53 @@ void Renderer::CreateTimeDescriptorSetLayout() { } void Renderer::CreateComputeDescriptorSetLayout() { - // TODO: Create the descriptor set layout for the compute pipeline + // DONE: 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 + + // Create descriptors for grass blades + VkDescriptorSetLayoutBinding grassBladesLayoutBinding = {}; + grassBladesLayoutBinding.binding = 0; + grassBladesLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + grassBladesLayoutBinding.descriptorCount = 1; + grassBladesLayoutBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + grassBladesLayoutBinding.pImmutableSamplers = nullptr; + + // Create descriptors for culled grass blades + VkDescriptorSetLayoutBinding culledBladesLayoutBinding = {}; + culledBladesLayoutBinding.binding = 1; + culledBladesLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + culledBladesLayoutBinding.descriptorCount = 1; + culledBladesLayoutBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + culledBladesLayoutBinding.pImmutableSamplers = nullptr; + + VkDescriptorSetLayoutBinding numBladesLayoutBinding = {}; + numBladesLayoutBinding.binding = 2; + numBladesLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + numBladesLayoutBinding.descriptorCount = 1; + numBladesLayoutBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + numBladesLayoutBinding.pImmutableSamplers = nullptr; + + std::vector bindings = + { + grassBladesLayoutBinding, + culledBladesLayoutBinding, + 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(); + + VkResult result = vkCreateDescriptorSetLayout(logicalDevice, &layoutInfo, nullptr, &computeDescriptorSetLayout); + + if (result != VK_SUCCESS) + { + throw std::runtime_error("Failed to create compute pipeline descriptor set layout"); + } + } void Renderer::CreateDescriptorPool() { @@ -215,7 +259,8 @@ void Renderer::CreateDescriptorPool() { // Time (compute) { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER , 1 }, - // TODO: Add any additional types and counts of descriptors you will need to allocate + // DONE: 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 = {}; @@ -224,7 +269,9 @@ void Renderer::CreateDescriptorPool() { poolInfo.pPoolSizes = poolSizes.data(); poolInfo.maxSets = 5; - if (vkCreateDescriptorPool(logicalDevice, &poolInfo, nullptr, &descriptorPool) != VK_SUCCESS) { + VkResult result = vkCreateDescriptorPool(logicalDevice, &poolInfo, nullptr, &descriptorPool); + + if (result != VK_SUCCESS) { throw std::runtime_error("Failed to create descriptor pool"); } } @@ -318,8 +365,51 @@ void Renderer::CreateModelDescriptorSets() { } void Renderer::CreateGrassDescriptorSets() { - // TODO: Create Descriptor sets for the grass. + // DONE: 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 descriptor 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 + VkResult result = vkAllocateDescriptorSets(logicalDevice, &allocInfo, grassDescriptorSets.data()); + if (result != VK_SUCCESS) { + throw std::runtime_error("Failed to allocate descriptor set"); + } + std::vector descriptorWrites(grassDescriptorSets.size()); + + for (uint32_t i = 0; i < scene->GetBlades().size(); ++i) { + // Configure the descriptors to refer to buffers + VkDescriptorBufferInfo modelBufferInfo = {}; + modelBufferInfo.buffer = scene->GetBlades()[i]->GetModelBuffer(); + modelBufferInfo.offset = 0; + modelBufferInfo.range = sizeof(ModelBufferObject); + + // Bind image and sampler resources to the descriptor + VkDescriptorImageInfo imageInfo = {}; + imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + imageInfo.imageView = scene->GetModels()[i]->GetTextureView(); + imageInfo.sampler = scene->GetModels()[i]->GetTextureSampler(); + + descriptorWrites[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + descriptorWrites[0].dstSet = grassDescriptorSets[i]; + descriptorWrites[0].dstBinding = 0; + descriptorWrites[0].dstArrayElement = 0; + descriptorWrites[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + descriptorWrites[0].descriptorCount = 1; + descriptorWrites[0].pBufferInfo = &modelBufferInfo; + descriptorWrites[0].pImageInfo = nullptr; + descriptorWrites[0].pTexelBufferView = nullptr; + } + + // Update descriptor sets + vkUpdateDescriptorSets(logicalDevice, static_cast(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr); } void Renderer::CreateTimeDescriptorSet() { @@ -358,8 +448,85 @@ void Renderer::CreateTimeDescriptorSet() { } void Renderer::CreateComputeDescriptorSets() { - // TODO: Create Descriptor sets for the compute pipeline + // DONE: 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->GetModels().size()); + + // Describe the descriptor 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 descriptor set"); + } + + std::vector descriptorWrites(3 * computeDescriptorSets.size()); + + for (uint32_t i = 0; i < scene->GetBlades().size(); ++i) { + + // Grass blade buffer + VkDescriptorBufferInfo bladesBufferInfo = {}; + bladesBufferInfo.buffer = scene->GetBlades()[i]->GetBladesBuffer(); + bladesBufferInfo.offset = 0; + bladesBufferInfo.range = sizeof(Blade) * NUM_BLADES; + + // Bind image and sampler resources to the descriptor + int idx = 3 * i; + descriptorWrites[idx].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + descriptorWrites[idx].dstSet = computeDescriptorSets[i]; + descriptorWrites[idx].dstBinding = 0; + descriptorWrites[idx].dstArrayElement = 0; + descriptorWrites[idx].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + descriptorWrites[idx].descriptorCount = 1; + descriptorWrites[idx].pBufferInfo = &bladesBufferInfo; + descriptorWrites[idx].pImageInfo = nullptr; + descriptorWrites[idx].pTexelBufferView = nullptr; + + // Culled grass blade buffer + VkDescriptorBufferInfo culledBladesBufferInfo = {}; + culledBladesBufferInfo.buffer = scene->GetBlades()[i]->GetCulledBladesBuffer(); + culledBladesBufferInfo.offset = 0; + culledBladesBufferInfo.range = sizeof(Blade) * NUM_BLADES; + + // Bind image and sampler resources to the descriptor + idx++; + descriptorWrites[idx].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + descriptorWrites[idx].dstSet = computeDescriptorSets[i]; + descriptorWrites[idx].dstBinding = 1; + descriptorWrites[idx].dstArrayElement = 0; + descriptorWrites[idx].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + descriptorWrites[idx].descriptorCount = 1; + descriptorWrites[idx].pBufferInfo = &culledBladesBufferInfo; + descriptorWrites[idx].pImageInfo = nullptr; + descriptorWrites[idx].pTexelBufferView = nullptr; + + // Number of grass blades buffer + VkDescriptorBufferInfo numBladesBufferInfo = {}; + numBladesBufferInfo.buffer = scene->GetBlades()[i]->GetNumBladesBuffer(); + numBladesBufferInfo.offset = 0; + numBladesBufferInfo.range = sizeof(BladeDrawIndirect); + + // Bind image and sampler resources to the descriptor + idx++; + descriptorWrites[idx].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + descriptorWrites[idx].dstSet = computeDescriptorSets[i]; + descriptorWrites[idx].dstBinding = 2; + descriptorWrites[idx].dstArrayElement = 0; + descriptorWrites[idx].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + descriptorWrites[idx].descriptorCount = 1; + descriptorWrites[idx].pBufferInfo = &numBladesBufferInfo; + descriptorWrites[idx].pImageInfo = nullptr; + descriptorWrites[idx].pTexelBufferView = nullptr; + } + + // Update descriptor sets + vkUpdateDescriptorSets(logicalDevice, static_cast(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr); } void Renderer::CreateGraphicsPipeline() { @@ -716,8 +883,13 @@ void Renderer::CreateComputePipeline() { computeShaderStageInfo.module = computeShaderModule; computeShaderStageInfo.pName = "main"; - // TODO: Add the compute dsecriptor set layout you create to this list - std::vector descriptorSetLayouts = { cameraDescriptorSetLayout, timeDescriptorSetLayout }; + // DONE: Add the compute dsecriptor set layout you create to this list + std::vector descriptorSetLayouts = + { + cameraDescriptorSetLayout, + timeDescriptorSetLayout, + computeDescriptorSetLayout + }; // Create pipeline layout VkPipelineLayoutCreateInfo pipelineLayoutInfo = {}; @@ -883,7 +1055,11 @@ void Renderer::RecordComputeCommandBuffer() { // Bind descriptor set for time uniforms 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 + // DONE: 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, 1); + } // ~ End recording ~ if (vkEndCommandBuffer(computeCommandBuffer) != VK_SUCCESS) { @@ -926,7 +1102,10 @@ void Renderer::RecordCommandBuffers() { renderPassInfo.renderArea.extent = swapChain->GetVkExtent(); std::array clearValues = {}; - clearValues[0].color = { 0.0f, 0.0f, 0.0f, 1.0f }; + + // Set the background color to a dark blue + clearValues[0].color = { 0.063f, 0.114f, 0.51f, 1.0f }; + clearValues[1].depthStencil = { 1.0f, 0 }; renderPassInfo.clearValueCount = static_cast(clearValues.size()); renderPassInfo.pClearValues = clearValues.data(); @@ -975,14 +1154,15 @@ void Renderer::RecordCommandBuffers() { for (uint32_t j = 0; j < scene->GetBlades().size(); ++j) { VkBuffer vertexBuffers[] = { scene->GetBlades()[j]->GetCulledBladesBuffer() }; VkDeviceSize offsets[] = { 0 }; - // TODO: Uncomment this when the buffers are populated - // vkCmdBindVertexBuffers(commandBuffers[i], 0, 1, vertexBuffers, offsets); + // DONE: Uncomment this when the buffers are populated + vkCmdBindVertexBuffers(commandBuffers[i], 0, 1, vertexBuffers, offsets); - // TODO: Bind the descriptor set for each grass blades model + // DONE: 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)); + // DONE: Uncomment this when the buffers are populated + vkCmdDrawIndirect(commandBuffers[i], scene->GetBlades()[j]->GetNumBladesBuffer(), 0, 1, sizeof(BladeDrawIndirect)); } // End render pass @@ -1041,7 +1221,7 @@ void Renderer::Frame() { Renderer::~Renderer() { vkDeviceWaitIdle(logicalDevice); - // TODO: destroy any resources you created + // DONE: destroy any resources you created vkFreeCommandBuffers(logicalDevice, graphicsCommandPool, static_cast(commandBuffers.size()), commandBuffers.data()); vkFreeCommandBuffers(logicalDevice, computeCommandPool, 1, &computeCommandBuffer); @@ -1050,6 +1230,7 @@ Renderer::~Renderer() { vkDestroyPipeline(logicalDevice, grassPipeline, nullptr); vkDestroyPipeline(logicalDevice, computePipeline, nullptr); + vkDestroyPipelineLayout(logicalDevice, graphicsPipelineLayout, nullptr); vkDestroyPipelineLayout(logicalDevice, graphicsPipelineLayout, nullptr); vkDestroyPipelineLayout(logicalDevice, grassPipelineLayout, nullptr); vkDestroyPipelineLayout(logicalDevice, computePipelineLayout, nullptr); @@ -1057,6 +1238,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..ee37418 100644 --- a/src/Renderer.h +++ b/src/Renderer.h @@ -56,12 +56,15 @@ class Renderer { VkDescriptorSetLayout cameraDescriptorSetLayout; VkDescriptorSetLayout modelDescriptorSetLayout; VkDescriptorSetLayout timeDescriptorSetLayout; + VkDescriptorSetLayout computeDescriptorSetLayout; VkDescriptorPool descriptorPool; VkDescriptorSet cameraDescriptorSet; - std::vector modelDescriptorSets; VkDescriptorSet timeDescriptorSet; + std::vector modelDescriptorSets; + std::vector grassDescriptorSets; + std::vector computeDescriptorSets; VkPipelineLayout graphicsPipelineLayout; VkPipelineLayout grassPipelineLayout; diff --git a/src/SwapChain.cpp b/src/SwapChain.cpp index 711fec0..702d24b 100644 --- a/src/SwapChain.cpp +++ b/src/SwapChain.cpp @@ -74,14 +74,21 @@ SwapChain::SwapChain(Device* device, VkSurfaceKHR vkSurface, unsigned int numBuf } } -void SwapChain::Create() { +void SwapChain::Create(int width, int height) { 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 = chooseSwapExtent(surfaceCapabilities, GetGLFWWindow()); + VkExtent2D extent; + extent.width = width; + extent.height = height; + + if (width == 0 || height == 0) { + extent = chooseSwapExtent(surfaceCapabilities, GetGLFWWindow()); + } uint32_t imageCount = surfaceCapabilities.minImageCount + 1; imageCount = numBuffers > imageCount ? numBuffers : imageCount; @@ -188,9 +195,9 @@ VkSemaphore SwapChain::GetRenderFinishedVkSemaphore() const { return renderFinishedSemaphore; } -void SwapChain::Recreate() { +void SwapChain::Recreate(int width, int height) { Destroy(); - Create(); + Create(width, height); } bool SwapChain::Acquire() { diff --git a/src/SwapChain.h b/src/SwapChain.h index dbafcf0..545c72d 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 width = 0, int height = 0); bool Acquire(); bool Present(); ~SwapChain(); private: SwapChain(Device* device, VkSurfaceKHR vkSurface, unsigned int numBuffers); - void Create(); + void Create(int width = 0, int height = 0); void Destroy(); Device* device; diff --git a/src/main.cpp b/src/main.cpp index 8bf822b..f7ec048 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -16,7 +16,9 @@ namespace { if (width == 0 || height == 0) return; vkDeviceWaitIdle(device->GetVkDevice()); - swapChain->Recreate(); + + // Added fix for window resize bug + swapChain->Recreate(width, height); renderer->RecreateFrameResources(); } diff --git a/src/shaders/compute.comp b/src/shaders/compute.comp index 0fd0224..386fe77 100644 --- a/src/shaders/compute.comp +++ b/src/shaders/compute.comp @@ -2,6 +2,26 @@ #extension GL_ARB_separate_shader_objects : enable #define WORKGROUP_SIZE 32 + +// Implementation details 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 +// by Klemens Jahrmann and Michael Wimmer + +// =============================================== +// SIMULATION PROPERTIES +// =============================================== +#define GRAVITY 1 +#define RECOVERY 1 +#define WIND 1 + +// =============================================== +// CULLING PROPERTIES +// =============================================== +#define ORIENT_CULL 1 +#define FRUSTUM_CULL 1 +#define DIST_CULL 1 + layout(local_size_x = WORKGROUP_SIZE, local_size_y = 1, local_size_z = 1) in; layout(set = 0, binding = 0) uniform CameraBufferObject { @@ -14,43 +34,213 @@ layout(set = 1, binding = 0) uniform Time { float totalTime; }; +// =============================================== +// BLADE STRUCTURE +// =============================================== struct Blade { - vec4 v0; - vec4 v1; - vec4 v2; - vec4 up; + vec4 v0; // Position of first control point + vec4 v1; // Position of second control point + vec4 v2; // Position of third control point + vec4 up; // Up vector }; -// TODO: Add bindings to: +// =============================================== +// BUFFER BINDINGS +// =============================================== + // 1. Store the input blades -// 2. Write out the culled blades -// 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 InputBlades { + Blade inputBlades[]; +}; + +// 2. Store the culled blades +layout(set = 2, binding = 1) buffer CulledBlades { + Blade culledBlades[]; +}; + +// 3. Store the total number of blades remaining +layout(set = 2, binding = 2) buffer NumBlades { + uint vertexCount; // Total 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 inViewFrustum(vec3 p) { + vec4 ndcP = camera.proj * camera.view * vec4(p, 1.0); + float tolerance = -0.05; // Tolerance for frustum culling + float h = ndcP.w + tolerance; + return inBounds(ndcP.x, h) && inBounds(ndcP.y, h) && inBounds(ndcP.z, h); +} + +vec3 calcWindInfluence(vec3 v0, float height) { + + float windPower = 5.0; + + // Wind direction oscillation through the plane + float turbulence = 0.1 * cos(v0.z * 0.2 + totalTime * 1.5); // Noise for irregularity + float sway = sin(0.5 * v0.x + totalTime) * height * 0.2; // Height-dependent sway + + return windPower * vec3(sway, 0.0, 0.0); +} + void main() { - // Reset the number of blades to 0 - if (gl_GlobalInvocationID.x == 0) { - // numBlades.vertexCount = 0; - } - barrier(); // Wait till all threads reach this point + // =============================================== + // RESET NUMBER OF BLADES + // =============================================== + if (gl_GlobalInvocationID.x == 0) { + numBlades.vertexCount = 0; + } + barrier(); // Synchronize all threads + + // =============================================== + // APPLY FORCES AND UPDATE VERTICES + // =============================================== + // DONE: Apply forces on every blade and update the vertices in the buffer + + // DONE: Cull blades that are too far away or not in the camera frustum + // and write them to the culled blades buffer + + // Get blade information + Blade blade = inputBlades[gl_GlobalInvocationID.x]; + vec3 v0 = blade.v0.xyz; + vec3 v1 = blade.v1.xyz; + vec3 v2 = blade.v2.xyz; + vec3 up = blade.up.xyz; + + float direction = blade.v0.w; + float height = blade.v1.w; + float width = blade.v2.w; + float stiffness = blade.up.w * 0.8; + + // =============================================== + // GRAVITY FORCE + // =============================================== + float g = 19.8; // Gravitational acceleration + vec4 gravityDirection = vec4(0.f, -1.f, 0.f, g); + float mass = 1.f; // Mass of the blade + float t = 0.5; + + vec3 environmentGravity = mass * ((normalize(gravityDirection.xyz) * gravityDirection.w * (1 - t)) + (t)); - // TODO: Apply forces on every blade and update the vertices in the buffer + vec3 widthDir = vec3(cos(direction), 0.0, -sin(direction)); // Direction vector for blade width + vec3 gravityForce = cross(widthDir, up); + vec3 frontGravity = 0.25 * length(environmentGravity) * gravityForce; - // TODO: Cull blades that are too far away or not in the camera frustum and write them +#if GRAVITY + vec3 gravity = environmentGravity + frontGravity; +#else + vec3 gravity = vec3(0.f); +#endif + + // =============================================== + // RECOVERY FORCE + // =============================================== + vec3 initialPosition = v0 + height * up; // Initial pose of blade + +#if RECOVERY + vec3 recoveryForce = (initialPosition - v2) * stiffness; // Hooke's law for recovery +#else + vec3 recoveryForce = vec3(0.f); +#endif + + // =============================================== + // WIND FORCE + // =============================================== + vec3 windInfluence = calcWindInfluence(v0, height); // Calculate wind influence + + float directionalAlignment = 1 - abs(dot(normalize(windInfluence), normalize(v2 - v0))); // Directional alignment + float heightRatio = dot(v2 - v0, up) / height; // Straightness of blade + + float windAlignment = directionalAlignment * heightRatio; + +#if WIND + vec3 wind = windInfluence * windAlignment; +#else + vec3 wind = vec3(0.f); +#endif + + // =============================================== + // TOTAL FORCES + // =============================================== + vec3 totalForce = (gravity + recoveryForce + wind) * deltaTime; + v2 += totalForce; + + // Update position of v1 based on v2 position + float lengthProj = length(v2 - v0 - up * dot(v2 - v0, up)); + v1 = v0 + height * up * max(1 - (lengthProj / height), 0.05 * max(lengthProj / height, 1)); + + // Correct blade curve length + float curveLength = distance(v0, v2); // Total distance between first and last control points + float correctedCurveLength = distance(v0, v1) + distance(v1, v2); // Sum of distances between control points + + float curveDegree = 2.f; // Degree for Bezier curve + float bladeCurveLength = (2 * curveLength + (curveDegree - 1) * correctedCurveLength) / (curveDegree + 1); // Final Bezier length + + float ratio = height / bladeCurveLength; // Ratio for blade length adjustment + + vec3 v1_old = v1; + v1 = v0 + ratio * (v1_old - v0); + v2 = v1 + ratio * (v2 - v1_old); + + blade.v1.xyz = v1; + blade.v2.xyz = v2; + inputBlades[gl_GlobalInvocationID.x] = blade; + + // =============================================== + // CULLING + // =============================================== + // DONE: Cull blades that are too far away or not in the camera frustum + + // =============================================== + // ORIENTATION CULLING + // =============================================== + vec3 cameraDirection = normalize(-inverse(camera.view)[2].xyz); + if (abs(dot(cameraDirection, widthDir)) > 0.9) { +#if ORIENT_CULL + return; // Skip the blade if orientation culling applies +#else +#endif + } + + // =============================================== + // VIEW FRUSTUM CULLING + // =============================================== + vec3 middlePoint = (0.25 * v0) + (0.5 * v1) + (0.25 * v2); + if (!inViewFrustum(v0) && !inViewFrustum(v2) && !inViewFrustum(middlePoint)) { +#if FRUSTUM_CULL + return; // Skip the blade if it is outside the view frustum +#else +#endif + } + + // =============================================== + // DISTANCE CULLING + // =============================================== + float maxDistance = 30.f; + vec3 v0ToCamera = v0 - inverse(camera.view)[3].xyz; + float projectedDistance = length(v0ToCamera - up * dot(v0ToCamera, up)); + + int distanceLevel = 20; + + if (projectedDistance > maxDistance || gl_GlobalInvocationID.x % distanceLevel > floor(distanceLevel * (1.0 - (projectedDistance / maxDistance)))) { +#if DIST_CULL + return; // Skip the blade if it is too far from the camera +#else +#endif + } + + // DONE: 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 + + // =============================================== + // UPDATE CULLED BLADES BUFFER + // =============================================== + culledBlades[atomicAdd(numBlades.vertexCount, 1)] = inputBlades[gl_GlobalInvocationID.x]; } diff --git a/src/shaders/grass.frag b/src/shaders/grass.frag index c7df157..29ccf36 100644 --- a/src/shaders/grass.frag +++ b/src/shaders/grass.frag @@ -6,12 +6,27 @@ layout(set = 0, binding = 0) uniform CameraBufferObject { mat4 proj; } camera; -// TODO: Declare fragment shader inputs +// DONE: Declare fragment shader inputs +layout(location = 0) in vec3 fsNor; +layout(location = 1) in float fsPosY; layout(location = 0) out vec4 outColor; void main() { - // TODO: Compute fragment color + // DONE: Compute fragment color + vec3 light = vec3(0.0, 1.0, 0.0); + float diffuseTerm = dot(normalize(fsNor), normalize(light)); - outColor = vec4(1.0); + vec3 green1; + vec3 green2; + + // More blue-green at the blade base + green1 = vec3(0.047f, 0.412f, 0.471f); + + // Lighter green at the blade tip + green2 = vec3(0.302f, 0.851f, 0.451f); + + vec3 green = mix(green1, green2, fsPosY); + + outColor = vec4(green * (1.f + diffuseTerm), 1.0); } diff --git a/src/shaders/grass.tesc b/src/shaders/grass.tesc index f9ffd07..f6b725d 100644 --- a/src/shaders/grass.tesc +++ b/src/shaders/grass.tesc @@ -8,19 +8,53 @@ layout(set = 0, binding = 0) uniform CameraBufferObject { mat4 proj; } camera; -// TODO: Declare tessellation control shader inputs and outputs +// DONE: Declare tessellation control shader inputs and outputs + +layout(location = 0) in vec4[] in_v0; +layout(location = 1) in vec4[] in_v1; +layout(location = 2) in vec4[] in_v2; +layout(location = 3) in vec4[] in_up; + +layout(location = 0) out vec4[] out_v0; +layout(location = 1) out vec4[] out_v1; +layout(location = 2) out vec4[] out_v2; +layout(location = 3) out vec4[] out_up; 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 + // DONE: Write any shader outputs + + out_v0[gl_InvocationID] = in_v0[gl_InvocationID]; + out_v1[gl_InvocationID] = in_v1[gl_InvocationID]; + out_v2[gl_InvocationID] = in_v2[gl_InvocationID]; + out_up[gl_InvocationID] = in_up[gl_InvocationID]; + + // Calculate the distance from the camera to the current grass blade + vec3 bladePos = vec3(gl_in[gl_InvocationID].gl_Position); + vec3 cameraPos = vec3(inverse(camera.view)[3]); + float dist = length(bladePos - cameraPos); + + // Tessellate - determine level of detail depending on distance + float tessLevel; + if (dist < 10.0) { + // Blades closer to the camera have a higher value = smoother curve + tessLevel = 20.0; + } else if (dist < 20.0) { + // Blades slightly farther have a lower tessellation value = rougher curve + tessLevel = 10.0; + } else { + // Blades farthest from the camera have the lowest value = roughest curve + tessLevel = 5.0; + } + + // DONE: Set level of tesselation + gl_TessLevelInner[0] = tessLevel; + gl_TessLevelInner[1] = tessLevel; - // TODO: Set level of tesselation - // gl_TessLevelInner[0] = ??? - // gl_TessLevelInner[1] = ??? - // gl_TessLevelOuter[0] = ??? - // gl_TessLevelOuter[1] = ??? - // gl_TessLevelOuter[2] = ??? - // gl_TessLevelOuter[3] = ??? + 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..81abd7f 100644 --- a/src/shaders/grass.tese +++ b/src/shaders/grass.tese @@ -8,11 +8,43 @@ layout(set = 0, binding = 0) uniform CameraBufferObject { mat4 proj; } camera; -// TODO: Declare tessellation evaluation shader inputs and outputs +// DONE: Declare tessellation evaluation shader inputs and outputs +layout(location = 0) in vec4 in_v0[]; +layout(location = 1) in vec4 in_v1[]; +layout(location = 2) in vec4 in_v2[]; +layout(location = 3) in vec4 in_up[]; + +layout(location = 0) out vec3 fsNor; +layout(location = 1) out float fsPosY; 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 + // DONE: Use u and v to parameterize along the grass blade and output positions for each vertex of the grass blade + vec3 v0 = in_v0[0].xyz; + vec3 v1 = in_v1[0].xyz; + vec3 v2 = in_v2[0].xyz; + + float direction = in_v0[0].w; + float width = in_v2[0].w; + + // Calculate De Casteljau's Algorithm + vec3 t1 = vec3(cos(direction), 0.0, -sin(direction)); + vec3 a = v0 + v * (v1 - v0); + vec3 b = v1 + v * (v2 - v1); + vec3 c = a + v * (b - a); + vec3 c0 = c - width * t1; + vec3 c1 = c + width * t1; + vec3 t0 = normalize(b - a); + + float t; + vec3 pos; + float threshold = 0.0; + t = 0.5 + (u - 0.5) * (1 - (max(v - threshold, 0) / (1 - threshold))); + pos = (1 - t) * c0 + t * c1; + + fsNor = normalize(cross(t0, t1)); + fsPosY = pos.y; + gl_Position = camera.proj * camera.view * vec4(pos, 1.0); } diff --git a/src/shaders/grass.vert b/src/shaders/grass.vert index db9dfe9..a808ebb 100644 --- a/src/shaders/grass.vert +++ b/src/shaders/grass.vert @@ -6,12 +6,28 @@ layout(set = 1, binding = 0) uniform ModelBufferObject { mat4 model; }; -// TODO: Declare vertex shader inputs and outputs +// DONE: Declare vertex shader inputs and outputs +layout(location = 0) in vec4 in_v0; +layout(location = 1) in vec4 in_v1; +layout(location = 2) in vec4 in_v2; +layout(location = 3) in vec4 in_up; + +layout(location = 0) out vec4 out_v0; +layout(location = 1) out vec4 out_v1; +layout(location = 2) out vec4 out_v2; +layout(location = 3) out vec4 out_up; out gl_PerVertex { vec4 gl_Position; }; void main() { - // TODO: Write gl_Position and any other shader outputs + // DONE: Write gl_Position and any other shader outputs + + out_v0 = model * in_v0; + out_v1 = model * in_v1; + out_v2 = model * in_v2; + out_up = model * in_up; + + gl_Position = out_v0; }