diff --git a/.gitignore b/.gitignore index 6c57396..6e8a02a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ *.sln *.vcxproj *.xcodeproj +*.spv build # Created by https://www.gitignore.io/api/linux,osx,sublimetext,windows,jetbrains,vim,emacs,cmake,c++,cuda,visualstudio,webstorm,eclipse,xcode diff --git a/README.md b/README.md index 20ee451..d5b37f5 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,145 @@ Vulkan Grass Rendering ================================== -**University of Pennsylvania, CIS 565: GPU Programming and Architecture, Project 5** +* Jichu Mao + * [LinkedIn](https://www.linkedin.com/in/jichu-mao-a3a980226/) + * [Personal Website](https://jichu.art/) +* Tested on: Windows 11, i7-13700K @ 3.40 GHz, 32GB, RTX 4090 24GB -* (TODO) YOUR NAME HERE -* Tested on: (TODO) Windows 22, i7-2222 @ 2.22GHz 22GB, GTX 222 222MB (Moore 2222 Lab) +## Overview -### (TODO: Your README) +In this project, a grass simulator and renderer is implemented using the Vulkan API, leveraging advanced shader techniques for both physics simulation and rendering optimization. Each blade of grass is represented by a Bézier curve, with tessellation shaders utilized to simulate realistic physics. Physics calculations incorporate various natural forces — environmental gravity, wind, and self-recovery — applied through a compute shader to create an organic, lifelike appearance. -*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. +To enhance rendering efficiency, a compute shader is employed to cull grass blades that are not visible or do not contribute significantly to the current frame, reducing the workload on the GPU. The remaining visible blades are processed through a graphics pipeline, where the vertex shader manipulates Bézier control points, the tessellation shaders dynamically generate grass geometry, and the fragment shader applies Lambertian shading to realistically color the grass blades. + +This implementation is inspired by the techniques outlined in 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 Jahrmann and Wimmer. The base code for this project is derived from a GPU course project at the University of Pennsylvania + + +## Showcase + +| ![](img/1.gif) | +|:--:| +| ***213 Grass Blades with dynamic tesselation levels*** | + +## Pipeline Overview +This project leverages a Vulkan-based pipeline to simulate and render realistic grass. The diagram below provides a detailed breakdown of the various components built within the pipeline to achieve this. + +| ![](img/pipe.jpeg) | +|:--:| +| *Figure 2. Vulkan Pipeline Diagram* | + +In this project, we are provided with a foundational Vulkan setup, which includes a compute pipeline for running compute shaders and two distinct graphics pipelines: one for rendering the terrain geometry and another specifically for grass rendering. Our primary focus is to develop the shaders required for the grass rendering pipeline (`grass.vert`, `grass.tesc`, `grass.tese`, and `grass.frag`) and the compute pipeline (`compute.comp`). Additionally, necessary resources (descriptors) are bound to facilitate these tasks within the pipeline. + +The following diagram shows clearly the workflow at different stages of the Vulkan pipeline in order to render and simulate realistic grass. *(Diagram credits from [Rhuta Joshi](https://github.com/rcj9719/gpu-vulkan-grass-rendering/))* +| ![](img/pipe2.png) | +|:--:| +| *Figure 3. Project Pipeline Diagram* | + +## The Physical Grass Model - Blade +In this project, grass blades will be represented as Bezier curves while performing physics calculations and culling operations (Figure 4). + +Each Bezier curve has three control points. +* `v0`: the position of the grass blade on the geomtry +* `v1`: a Bezier curve guide that is always "above" `v0` with respect to the grass blade's up vector (explained soon) +* `v2`: a physical guide for which we simulate forces on + +We also need to store per-blade characteristics that will help us simulate and tessellate our grass blades correctly. +* `up`: the blade's up vector, which corresponds to the normal of the geometry that the grass blade resides on at `v0` +* Orientation: the orientation of the grass blade's face +* Height: the height of the grass blade +* Width: the width of the grass blade's face +* Stiffness coefficient: the stiffness of our grass blade, which will affect the force computations on our blade + +We pack all this data into four `vec4`s, such that `v0.w` holds orientation, `v1.w` holds height, `v2.w` holds width, and `up.w` holds the stiffness coefficient. + +| ![](img/blade_model.jpg) | +|:--:| +| *Figure 4. Illustration of the definition of a blade of grass* | + +More than that, we also apply **Gravity**, **Recovery** and **Wind Force** onto the blades in order to creating an nartural movement. + +The compute shader `compute.comp` operates on each blade individually, calculating the influence of various forces on each blade and updating the positions of control points v0, v1, and v2 every frame. Additionally, it handles culling operations, which are detailed in a later section. + +## Tessellation + +As previously mentioned in pipleline overview, we will implement the tessellation control shader in `grass.tesc` and the tessellation evaluation shader in `grass.tese`. + +### Geometry Generation Using Tessellation Shaders +With each grass blade represented by a Bézier curve, we use tessellation shaders to generate a triangulated mesh, optimizing for both detail and performance within the graphics pipeline. + +Tessellation involves subdividing vertex data patches into finer primitives. The pipeline comprises the Tessellation Control Shader (TCS), a fixed-function Primitive Generator (PG), and the Tessellation Evaluation Shader (TES). Our TCS (`grass.tesc`) processes patches of three control points for each blade and sets tessellation levels to control subdivision density. We define quads as our patch domain in the TES (`grass.tese`), which determines the final vertex positions using De Casteljau’s algorithm. The tessellated quads are then remapped into triangular shapes to better resemble grass blades, as illustrated in Figure 5. + +| ![](img/q1.jpg) | +|:--:| +| *Figure 5. Triangles remapped from quads* | + +### Level of Detail (LOD) Control +To manage Level of Detail (LOD), tessellation levels are adjusted based on each blade’s distance from the camera, allowing for finer detail in closer blades while conserving resources for those farther away. This dynamic approach effectively balances visual quality and performance, highlighting the versatility of tessellation shaders in real-time rendering, as illustrated in Figure 6. + +| ![](img/lod.gif) | +|:--:| +| *Figure 6. Tessellation with LOD control* | + +## Optimization - Culling Techniques +To optimize rendering performance, we implement three distinct types of culling as described in the reference paper. These methods selectively discard grass blades that do not contribute significantly to the scene, thus enhancing efficiency: + +### Orientation Culling + Grass blades with a forward vector that is nearly perpendicular to the camera’s view vector (resulting in minimal visible width) are culled. This feature can be enabled with the `ORIENTATION_CULLING` preprocessor directive in `compute.comp`. + +| ![](img/c1.gif) | +|:--:| +| *Figure 7. Orientation Culling* | + +### View-Frustum Culling + Grass blades positioned outside the viewing frustum are discarded. This feature is controlled by the `FRUSTUM_CULLING` directive in compute.comp. The tolerance for this culling method can be adjusted via the tolerance parameter within the compute shader, allowing for fine-tuning. + +| ![](img/c2.gif) | +|:--:| +| *Figure 8. View-Frustum Culling* | + +### Distance Culling +Grass blades beyond a certain threshold distance are culled to reduce rendering costs, particularly for small, distant blades that contribute minimally to visual quality. This feature can be activated using the `DISTANCE_CULLING` preprocessor directive in `compute.comp`. + +| ![](img/c3.gif) | +|:--:| +| *Figure 9. Distance Culling* | + +Each of these culling techniques is selectively applied to achieve an efficient rendering pipeline, optimizing the balance between visual fidelity and performance. + +## Performance Analysis + +In this analysis, we assess the effectiveness of various culling techniques in optimizing frame rate (FPS) and evaluate performance under different numbers of grass blades. All tests use a tessellation level of 8, with consistent configuration parameters for each culling method. + +### Impact of Culling Techniques +The first chart illustrates the FPS impact of each individual culling method and their combined effect. Testing with 2^15 grass blades, we find that: + +- **Orientation Culling** and **Distance Culling** both deliver a noticeable FPS improvement, achieving frame rates of 97.5 FPS and 120.1 FPS, respectively, compared to the baseline of 70.2 FPS with no culling. This demonstrates that blades with limited visibility or those far from the camera can be efficiently removed from processing, reducing computational load. + +- **View-Frustum Culling**, in this scenario, offers minimal or even negligible benefits, as most grass blades remain within the camera’s view frustum, leading to minimal culling. As a result, the extra computation required for frustum checks can even reduce efficiency slightly. + +- **Combined Culling (All Three Techniques)** achieves the highest performance gain, reaching 228.2 FPS, a substantial improvement over any single method. This indicates that integrating multiple culling strategies is highly effective, maximizing the number of blades culled across diverse conditions. + +![](img/a2.jpg) + +### Effect of Blade Count on Rendering Performance +The second chart examines FPS variation across different grass blade counts, comparing scenarios with no culling and with all three culling techniques enabled. + +- **Performance Decrease with Increased Blade Count**: As expected, the FPS consistently decreases with an increasing number of blades. Rendering all blades without any culling shows a steep decline in performance, from 2,822.1 FPS at 2^12 blades to just 74.2 FPS at 2^20 blades. + +- **Performance Gains with Culling Enabled**: When all three culling methods are applied, significant performance improvements are observed, particularly as blade count rises. For example, at 2^16 blades, FPS increases from 914.5 to 2,278.5 with culling, highlighting the effectiveness of culling in reducing GPU load as scene complexity increases. + +- **Performance Plateau at High Blade Counts**: As the number of blades reaches higher levels (e.g., 2^20), the culling methods still offer a performance boost (219.5 FPS compared to 74.2 FPS without culling), but the absolute gains diminish. This plateau effect suggests that beyond a certain threshold, the system’s ability to handle additional grass blades is limited by other bottlenecks, possibly memory bandwidth or CPU-GPU synchronization overhead. + +![](img/a1.jpg) + + +In summary, the integration of orientation, frustum, and distance culling significantly enhances rendering performance, especially in complex scenes with high blade counts. The combined approach yields the best results, with distance and orientation culling providing the most consistent gains individually. + +## References + +* [Responsive Real-Time Grass Grass Rendering for General 3D Scenes](https://www.cg.tuwien.ac.at/research/publications/2017/JAHRMANN-2017-RRTG/JAHRMANN-2017-RRTG-draft.pdf) +* [CIS565 Vulkan samples](https://github.com/CIS565-Fall-2017/Vulkan-Samples/tree/master/samples/5_helloTessellation) +* [Official Vulkan documentation](https://www.khronos.org/registry/vulkan/) +* [Vulkan tutorial](https://vulkan-tutorial.com/) +* [RenderDoc blog on Vulkan](https://renderdoc.org/vulkan-in-30-minutes.html) +* [Tessellation tutorial](https://ogldev.org/www/tutorial30/tutorial30.html) diff --git a/bin/Release/vulkan_grass_rendering.exe b/bin/Release/vulkan_grass_rendering.exe index f68db3a..fcc7b76 100644 Binary files a/bin/Release/vulkan_grass_rendering.exe and b/bin/Release/vulkan_grass_rendering.exe differ diff --git a/img/1.gif b/img/1.gif new file mode 100644 index 0000000..9c983c0 Binary files /dev/null and b/img/1.gif differ diff --git a/img/a1.jpg b/img/a1.jpg new file mode 100644 index 0000000..ed6fe25 Binary files /dev/null and b/img/a1.jpg differ diff --git a/img/a2.jpg b/img/a2.jpg new file mode 100644 index 0000000..799e372 Binary files /dev/null and b/img/a2.jpg differ diff --git a/img/blade_model.jpg b/img/blade_model.jpg index b7a0325..bdabcca 100644 Binary files a/img/blade_model.jpg and b/img/blade_model.jpg differ diff --git a/img/c1.gif b/img/c1.gif new file mode 100644 index 0000000..1203159 Binary files /dev/null and b/img/c1.gif differ diff --git a/img/c2.gif b/img/c2.gif new file mode 100644 index 0000000..e18fa59 Binary files /dev/null and b/img/c2.gif differ diff --git a/img/c3.gif b/img/c3.gif new file mode 100644 index 0000000..43ef292 Binary files /dev/null and b/img/c3.gif differ diff --git a/img/lod.gif b/img/lod.gif new file mode 100644 index 0000000..78fc90f Binary files /dev/null and b/img/lod.gif differ diff --git a/img/pipe.jpeg b/img/pipe.jpeg new file mode 100644 index 0000000..4debb8d Binary files /dev/null and b/img/pipe.jpeg differ diff --git a/img/pipe2.png b/img/pipe2.png new file mode 100644 index 0000000..87b7d47 Binary files /dev/null and b/img/pipe2.png differ diff --git a/img/q1.jpg b/img/q1.jpg new file mode 100644 index 0000000..9b6504b Binary files /dev/null and b/img/q1.jpg differ diff --git a/src/Blades.cpp b/src/Blades.cpp index 80e3d76..624c4fe 100644 --- a/src/Blades.cpp +++ b/src/Blades.cpp @@ -45,7 +45,8 @@ 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_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..9856a17 100644 --- a/src/Blades.h +++ b/src/Blades.h @@ -4,6 +4,7 @@ #include #include "Model.h" +//constexpr static unsigned int NUM_BLADES = 1 << 13; constexpr static unsigned int NUM_BLADES = 1 << 13; constexpr static float MIN_HEIGHT = 1.3f; constexpr static float MAX_HEIGHT = 2.5f; diff --git a/src/Renderer.cpp b/src/Renderer.cpp index b445d04..fba1346 100644 --- a/src/Renderer.cpp +++ b/src/Renderer.cpp @@ -198,6 +198,28 @@ 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 + std::vector bindings = {}; + + // Create the descriptor set layout + for (int i = 0; i < 3; i++) { + VkDescriptorSetLayoutBinding layoutBinding = {}; + layoutBinding.binding = i; + layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + layoutBinding.descriptorCount = 1; + layoutBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + layoutBinding.pImmutableSamplers = nullptr; + bindings.push_back(layoutBinding); + } + + 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 computeDescriptorSetLayout"); + } + } void Renderer::CreateDescriptorPool() { @@ -216,6 +238,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 +343,39 @@ 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()); + + 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; + + if (vkAllocateDescriptorSets(logicalDevice, &allocInfo, grassDescriptorSets.data()) != VK_SUCCESS) { + throw std::runtime_error("Failed to allocate grassDescriptorSets"); + } + + 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; + } + + vkUpdateDescriptorSets(logicalDevice, static_cast(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr); } void Renderer::CreateTimeDescriptorSet() { @@ -360,6 +416,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()); + + 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; + + if (vkAllocateDescriptorSets(logicalDevice, &allocInfo, computeDescriptorSets.data()) != VK_SUCCESS) { + throw std::runtime_error("Failed to allocate computeDescriptorSets"); + } + + std::vector descriptorWrites(computeDescriptorSets.size() * 3); + + for (uint32_t i = 0; i < scene->GetBlades().size(); i++) { + VkDescriptorBufferInfo bladesBufferInfo = {}; + bladesBufferInfo.buffer = scene->GetBlades()[i]->GetBladesBuffer(); + bladesBufferInfo.offset = 0; + bladesBufferInfo.range = NUM_BLADES * sizeof(Blade); + + VkDescriptorBufferInfo culledBladesBufferInfo = {}; + culledBladesBufferInfo.buffer = scene->GetBlades()[i]->GetCulledBladesBuffer(); + culledBladesBufferInfo.offset = 0; + culledBladesBufferInfo.range = NUM_BLADES * sizeof(Blade); + + VkDescriptorBufferInfo numBladesBufferInfo = {}; + numBladesBufferInfo.buffer = scene->GetBlades()[i]->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 = &bladesBufferInfo; + 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 = &culledBladesBufferInfo; + 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; + } + + vkUpdateDescriptorSets(logicalDevice, static_cast(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr); } void Renderer::CreateGraphicsPipeline() { @@ -480,7 +599,7 @@ void Renderer::CreateGraphicsPipeline() { colorBlending.blendConstants[2] = 0.0f; colorBlending.blendConstants[3] = 0.0f; - std::vector descriptorSetLayouts = { cameraDescriptorSetLayout, modelDescriptorSetLayout }; + std::vector descriptorSetLayouts = { cameraDescriptorSetLayout, modelDescriptorSetLayout}; // Pipeline layout: used to specify uniform values VkPipelineLayoutCreateInfo pipelineLayoutInfo = {}; @@ -601,6 +720,7 @@ void Renderer::CreateGrassPipeline() { rasterizer.depthClampEnable = VK_FALSE; rasterizer.rasterizerDiscardEnable = VK_FALSE; rasterizer.polygonMode = VK_POLYGON_MODE_FILL; + //rasterizer.polygonMode = VK_POLYGON_MODE_LINE; rasterizer.lineWidth = 1.0f; rasterizer.cullMode = VK_CULL_MODE_NONE; rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; @@ -717,7 +837,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,7 +1004,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, 1); + } // ~ End recording ~ if (vkEndCommandBuffer(computeCommandBuffer) != VK_SUCCESS) { throw std::runtime_error("Failed to record compute command buffer"); @@ -976,13 +1099,13 @@ 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); + 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 +1180,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..e7fe29a 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 grassDescriptorSets; + std::vector computeDescriptorSets; VkPipelineLayout graphicsPipelineLayout; VkPipelineLayout grassPipelineLayout; diff --git a/src/SwapChain.cpp b/src/SwapChain.cpp index 711fec0..7eab854 100644 --- a/src/SwapChain.cpp +++ b/src/SwapChain.cpp @@ -74,14 +74,18 @@ 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 = 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 +192,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() { @@ -199,14 +203,15 @@ bool SwapChain::Acquire() { 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"); - } + if (result == VK_ERROR_OUT_OF_DATE_KHR) { Recreate(); return false; } + else if (result != VK_SUCCESS) { + throw std::runtime_error("Failed to acquire swap chain image"); + } return true; } 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..0b67db1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,6 +5,7 @@ #include "Camera.h" #include "Scene.h" #include "Image.h" +#include Device* device; SwapChain* swapChain; @@ -16,7 +17,7 @@ namespace { if (width == 0 || height == 0) return; vkDeviceWaitIdle(device->GetVkDevice()); - swapChain->Recreate(); + swapChain->Recreate(width, height); renderer->RecreateFrameResources(); } @@ -143,7 +144,24 @@ int main() { glfwSetMouseButtonCallback(GetGLFWWindow(), mouseDownCallback); glfwSetCursorPosCallback(GetGLFWWindow(), mouseMoveCallback); + int frames = 0; + float fps = 0.0; + float lastT = 0.0f; + std::string windowTitle; + while (!ShouldQuit()) { + frames++; + double curT = glfwGetTime(); + if (curT - lastT >= 1.0) + { + fps = static_cast(frames) / (curT - lastT); + lastT = curT; + frames = 0; + } + + windowTitle = ("Vulkan Grass Renderer | FPS: ") + std::to_string(fps); + glfwSetWindowTitle(GetGLFWWindow(), windowTitle.c_str()); + glfwPollEvents(); scene->UpdateTime(); renderer->Frame(); diff --git a/src/shaders/compute.comp b/src/shaders/compute.comp index 0fd0224..e6fb3c6 100644 --- a/src/shaders/compute.comp +++ b/src/shaders/compute.comp @@ -21,36 +21,143 @@ struct Blade { vec4 up; }; -// TODO: Add bindings to: -// 1. Store the input blades -// 2. Write out the culled blades -// 3. Write the total number of blades remaining +layout(set = 2, binding = 0) buffer InBlades { + Blade inBlades[]; +}; + +layout(set = 2, binding = 1) buffer CulledBlades { + Blade culledBlades[]; +}; + +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; + -// 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; +#define GRAVITY_ACCELERATION 5.0 +#define WIND_MAGNITUDE 1.0 +#define WIND_FREQUENCY 0.7 +#define TOLERANCE_FRUSTUM -1.0 +#define DISTANCE_LEVEL 10 + +#define ORIENTATION_CULLING 0 +#define VIEW_FRUSTUM_CULLING 0 +#define DISTANCE_CULLING 0 bool inBounds(float value, float bounds) { return (value >= -bounds) && (value <= bounds); } +bool inFrustum(vec3 pos) { + vec4 ndcPos = camera.proj * camera.view * vec4(pos, 1.f); + float homogeneous_coord = ndcPos.w + TOLERANCE_FRUSTUM; + return inBounds(ndcPos.x, homogeneous_coord) && inBounds(ndcPos.y, homogeneous_coord); +} + +// TODO: Add bindings to: +// 1. Store the input blades +// 2. Write out the culled blades +// 3. Write the total number of blades remaining 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 // TODO: Apply forces on every blade and update the vertices in the buffer + // Apply forces on every blade and update the vertices in the buffer + Blade thisBlade = inBlades[gl_GlobalInvocationID.x]; + vec3 v0 = thisBlade.v0.xyz; + vec3 v1 = thisBlade.v1.xyz; + vec3 v2 = thisBlade.v2.xyz; + vec3 up = thisBlade.up.xyz; + + float orientAngle = thisBlade.v0.w; + float height = thisBlade.v1.w; + float width = thisBlade.v2.w; + float stiffness = thisBlade.up.w; + vec3 oriention = normalize(vec3(-cos(orientAngle), 0.0, sin(orientAngle))); + vec3 f = normalize(cross(oriention, up)); + // Apply forces on every blade and update the vertices in the buffer + + // Gravity + vec4 D = vec4(0.0, -1.0, 0.0, GRAVITY_ACCELERATION); + vec3 gE = normalize(D.xyz) * D.w; + vec3 gF = 0.25 * length(gE) * f; + vec3 gravity = gE + gF; + + // Recovery force + vec3 iv2 = v0 + up * height; + vec3 recovery = (iv2 - v2) * stiffness; + + float windPhaseX = WIND_FREQUENCY * v0.x * totalTime; + float windPhaseZ = WIND_FREQUENCY * v0.z * totalTime; + vec3 wind = WIND_MAGNITUDE * vec3(sin(windPhaseX), 0.0, cos(windPhaseZ)); + + vec3 windDir = normalize(wind); + vec3 dir = normalize(v2 - v0); + + float fd = 1.0 - abs(dot(windDir, dir)); + float fr = dot(v2 - v0, up) / height; + vec3 windForce = wind * fd * fr; + + // Corrected Update + v2 += (gravity + recovery + windForce) * deltaTime; + + float lProj = length(v2 - v0 - up * dot(v2 - v0, up)); + v1 = v0 + height * up * max(1.f - lProj/height, 0.05 * max(lProj/height, 1.f)); + + v2 -= up * min(dot(up, v2 - v0), 0); + float L0 = distance(v0, v2); + float L1 = distance(v0, v1) + distance(v1, v2); + // (2L0 + (n-1)L1)/(n+1)£¬When n = 3, L = (L0 + L1) / 2 + float L = 0.5 * (L0 + L1); + float r = height / L; + v1 = v0 + r * (v1 - v0); + v2 = v1 + r * (v2 - v1); + + thisBlade.v1.xyz = v1; + thisBlade.v2.xyz = v2; + inBlades[gl_GlobalInvocationID.x] = thisBlade; // 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 -} + +// Cull blades that have no thickness + vec3 cameraPos = inverse(camera.view)[3].xyz; + vec3 cameraToBladeDir = v0 - cameraPos; + vec3 viewDir = cameraToBladeDir - up * dot(cameraToBladeDir, up); +#if ORIENTATION_CULLING +// Cull blades that are facing or oppsite the camera + if (abs(dot(normalize(viewDir), oriention)) > 0.9) { + return; + } +#endif + +// Cull blades that are not in the camera frustum +#if VIEW_FRUSTUM_CULLING + vec3 m = 0.25 * v0 + 0.5 * v1 + 0.25 * v2; + if( !inFrustum(v0) && !inFrustum(v2) && !inFrustum(m)) { + return; + } + +#endif + +// Cull blades that are too far away +#if DISTANCE_CULLING + float dProj = length(viewDir); + if (gl_GlobalInvocationID.x % DISTANCE_LEVEL < int(floor(DISTANCE_LEVEL * (1.0 - dProj / 20.0f)))) { + return; + } +#endif + + // 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 + culledBlades[atomicAdd(numBlades.vertexCount, 1)] = inBlades[gl_GlobalInvocationID.x]; +} \ No newline at end of file diff --git a/src/shaders/grass.frag b/src/shaders/grass.frag index c7df157..2ebe65b 100644 --- a/src/shaders/grass.frag +++ b/src/shaders/grass.frag @@ -7,11 +7,23 @@ layout(set = 0, binding = 0) uniform CameraBufferObject { } camera; // TODO: Declare fragment shader inputs +layout(location = 0) in float inHeight; +layout(location = 1) in vec3 inNor; layout(location = 0) out vec4 outColor; void main() { // TODO: Compute fragment color - outColor = vec4(1.0); -} + // Hard coded base color and light direction + vec3 lightDir = vec3(0.0, 5.0, 0.0); + vec3 color1 = vec3(0.2, 0.2, 0.2); + vec3 color2 = vec3(0.1, 0.9, 0.1); + vec3 albedo = mix(color1, color2, inHeight); + + float diffuse = dot(inNor, normalize(lightDir)); + diffuse = clamp(diffuse,0.0, 1.0); + + vec3 color = albedo * (0.5f + diffuse); + outColor = vec4(color, 1.0); +} \ No newline at end of file diff --git a/src/shaders/grass.tesc b/src/shaders/grass.tesc index f9ffd07..644360e 100644 --- a/src/shaders/grass.tesc +++ b/src/shaders/grass.tesc @@ -9,18 +9,59 @@ 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[] inV0; +layout(location = 1)in vec4[] inV1; +layout(location = 2)in vec4[] inV2; +layout(location = 3)in vec4[] inUp; + +layout(location = 0)out vec4[] outV0; +layout(location = 1)out vec4[] outV1; +layout(location = 2)out vec4[] outV2; +layout(location = 3)out vec4[] outUp; + +//#define tessLevel 8 + +float getTessLevelFromLOD() { + vec3 cameraPos = inverse(camera.view)[3].xyz; + vec3 v0 = inV0[gl_InvocationID].xyz; + float tesslevel; + float dist = length(cameraPos - v0); + if (dist < 2.0) { + tesslevel = 16; + }else if (dist < 6.0) { + tesslevel = 8; + }else if (dist < 10.0) { + tesslevel = 4; + }else if (dist < 14.0) { + tesslevel = 2; + } + return tesslevel; +} 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 + outV0[gl_InvocationID] = inV0[gl_InvocationID]; + outV1[gl_InvocationID] = inV1[gl_InvocationID]; + outV2[gl_InvocationID] = inV2[gl_InvocationID]; + outUp[gl_InvocationID] = inUp[gl_InvocationID]; + // TODO: Set level of tesselation - // gl_TessLevelInner[0] = ??? - // gl_TessLevelInner[1] = ??? - // gl_TessLevelOuter[0] = ??? - // gl_TessLevelOuter[1] = ??? - // gl_TessLevelOuter[2] = ??? - // gl_TessLevelOuter[3] = ??? + float tessLevel = getTessLevelFromLOD(); + 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..f289390 100644 --- a/src/shaders/grass.tese +++ b/src/shaders/grass.tese @@ -9,10 +9,40 @@ layout(set = 0, binding = 0) uniform CameraBufferObject { } camera; // TODO: Declare tessellation evaluation shader inputs and outputs +layout(location = 0) in vec4 inV0[]; +layout(location = 1) in vec4 inV1[]; +layout(location = 2) in vec4 inV2[]; +layout(location = 3) in vec4 inUp[]; + +layout(location = 0) out float outHeight; +layout(location = 1) out vec3 outNor; void main() { + // Use u and v to parameterize along the grass blade and output positions for each vertex of the grass blade float u = gl_TessCoord.x; float v = gl_TessCoord.y; + + vec3 v0 = inV0[0].xyz; + vec3 v1 = inV1[0].xyz; + vec3 v2 = inV2[0].xyz; + + float orientAngle = inV0[0].w; + float width = inV2[0].w; + + // De Casteljau + vec3 a = mix(v0, v1, v); + vec3 b = mix(v1, v2, v); + vec3 c = mix(a, b, v); + + vec3 t0 = normalize(b - a); + vec3 t1 = normalize(vec3(-cos(orientAngle), 0.0, sin(orientAngle))); + + vec3 c0 = c - width * t1; + vec3 c1 = c + width * t1; + float t = u + 0.5 * v - u * v; + vec3 pos = mix(c0, c1, t); - // TODO: Use u and v to parameterize along the grass blade and output positions for each vertex of the grass blade -} + outHeight = v; + outNor = normalize(cross(t0, t1)); + gl_Position = camera.proj * camera.view * vec4(pos, 1.0); +} \ No newline at end of file diff --git a/src/shaders/grass.vert b/src/shaders/grass.vert index db9dfe9..8ebeae6 100644 --- a/src/shaders/grass.vert +++ b/src/shaders/grass.vert @@ -7,6 +7,14 @@ layout(set = 1, binding = 0) uniform ModelBufferObject { }; // TODO: Declare vertex shader inputs and outputs +layout(location = 0) in vec4 inV0; +layout(location = 1) in vec4 inV1; +layout(location = 2) in vec4 inV2; +layout(location = 3) in vec4 inUp; +layout(location = 0) out vec4 outV0; +layout(location = 1) out vec4 outV1; +layout(location = 2) out vec4 outV2; +layout(location = 3) out vec4 outUp; out gl_PerVertex { vec4 gl_Position; @@ -14,4 +22,16 @@ out gl_PerVertex { void main() { // TODO: Write gl_Position and any other shader outputs -} + //Note we are NOT transforming into screen space bc we will have to use world space pos for positioning tessellated points later in evaluation shader + outV0.xyz = (model * inV0).xyz; + outV0.w = inV0.w; + + outV1.xyz = (model * inV1).xyz; + outV1.w = inV1.w; + + outV2.xyz = (model * inV2).xyz; + outV2.w = inV2.w; + + outUp = inUp; + gl_Position = outV0; +} \ No newline at end of file