diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0eddcd1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "files.associations": { + "vector": "cpp", + "chrono": "cpp", + "list": "cpp", + "forward_list": "cpp", + "xstring": "cpp", + "array": "cpp" + } +} \ No newline at end of file diff --git a/README.md b/README.md index 20ee451..2fe3455 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,151 @@ -Vulkan Grass Rendering -================================== +## Vulkan Grass Rendering -**University of Pennsylvania, CIS 565: GPU Programming and Architecture, Project 5** +[![Watch the video](https://img.youtube.com/vi/JXlazz3fjsE/maxresdefault.jpg)](https://youtu.be/JXlazz3fjsE) +(same video also at writeup/grass.mov) -* (TODO) YOUR NAME HERE -* Tested on: (TODO) Windows 22, i7-2222 @ 2.22GHz 22GB, GTX 222 222MB (Moore 2222 Lab) +Author: Alan Lee ([LinkedIn](https://www.linkedin.com/in/soohyun-alan-lee/)) -### (TODO: Your README) +This project is a Vulkan grass renderer showcasing physically based simulation of grass blade movements under various forces and collisions. -*DO NOT* leave the README to the last minute! It is a crucial part of the -project, and we will not be able to grade you without a good README. +The algorithm is based on a real-time grass rendering paper by Klemens Jahrmann and Michael Wimmer and accounts for gravity, wind, and recovery forces as well as dynamic culling for maximum performance. + +## Contents + +* `src/` C++/Vulkan source files. + * `shaders/` glsl shader source files + * `images/` images used as textures within graphics pipelines +* `external/` Includes and static libraries for 3rd party libraries. + +## Running the code + +The codebase requires the [Vulkan SDK](https://vulkan.lunarg.com/) and a [Vulkan driver](https://developer.nvidia.com/vulkan-driver) appropriate to your graphics card to have been installed to be ran. + +Configure and generate build files using provided cmakelists. + +Vulkan validation layer is turned on by default for `debug` mode, but it is not enabled in `release` mode. + +## Analysis + +### Grass Representation + +![](img/blade_model.jpg) + +In this project, grass blades will be represented as Bezier curves while performing physics calculations and culling operations. + +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 +* `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 can 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. + +### Simulating Forces + +We separate our rendering pipeline into two passes: first pass to render current form of grass blades and second pass to compute updated paramters for the next frame. Physical force simulation is performed in the second pass as a compute shader. This compute shader pass alters the control points of each grass blade according to forces being applied on them. + +#### Gravity + +Given a gravity direction, `D.xyz`, and the magnitude of acceleration, `D.w`, we can compute the environmental gravity in our scene as `gE = normalize(D.xyz) * D.w`. + +We then determine the contribution of the gravity with respect to the front facing direction of the blade, `f`, as a term called the "front gravity". Front gravity is computed as `gF = (1/4) * ||gE|| * f`. + +We can then determine the total gravity on the grass blade as `g = gE + gF`. + +#### Recovery + +Recovery corresponds to the counter-force that brings our grass blade back into equilibrium. This is derived in the paper using Hooke's law. In order to determine the recovery force, we need to compare the current position of `v2` to its original position before simulation started, `iv2`. At the beginning of our simulation, `v1` and `v2` are initialized to be a distance of the blade height along the `up` vector. + +Once we have `iv2`, we can compute the recovery forces as `r = (iv2 - v2) * stiffness`. + +#### Wind + +![](writeup/wind_dir.png) + +We represent wind as an analytic function `w_i(v_0)` that outputs direction and strength of the wind influence at the position of a blade of grass. The analytic functions can be modeled heuristically using multiple sine and cosine functions with different frequencies. This can simulate wind coming from some direction or a specific source, like a helicopter or a fan. + +The strength by which our blades are affected by wind should also be affected by the alignment of the grass blades to the direction of wind. That is, a grass blade facing perpendicular to the wind direction should be affected more strongly by the wind. Similarly, a grass blade standing more straight up (so greater surface area exposed) should be affected more strongly by the wind as well. These two ideas are captured by the following equations where `f_d` is the directional alignment and `f_r(h)` represents the straightnes of the blade with respect to the up vector. + +![](writeup/wind_eq.png) + +Once we have a wind direction and a wind alignment term, we can compute total wind force (`w`) as `w_i(v_0) * windAlignment`. + +### Culling tests + +Although we need to simulate forces on every grass blade at every frame, there are many blades that we won't need to render due to a variety of reasons. Here are some heuristics we implement in this project to cull blades that won't contribute positively to a given frame. + +#### Orientation culling + +Consider the scenario in which the front face direction of the grass blade is perpendicular to the view vector. Since our grass blades won't have width, we will end up trying to render parts of the grass that are actually smaller than the size of a pixel. This could lead to aliasing artifacts. In order to remedy this, we cull these blades by performing a dot product test to see if the view vector and front face direction of the blade are perpendicular. + +#### View-frustum culling + +We also want to cull blades that are outside of the view-frustum, considering they won't show up in the frame anyway. To determine if a grass blade is in the view-frustum, we want to compare the visibility of three points: `v0, v2, and m`, where `m = (1/4)v0 * (1/2)v1 * (1/4)v2`. + +If all three points are outside of the view-frustum, we cull the grass blade. The paper uses a tolerance value for this test so that we are culling blades a little more conservatively. This can help with cases in which the Bezier curve is technically not visible, but we might be able to see the blade if we consider its width. The default tolerance for this project is `0.3`. + +#### Distance culling + +Similarly to orientation culling, we can end up with grass blades that at large distances are smaller than the size of a pixel. This could lead to additional artifacts in our renders. In this case, we can cull grass blades as a function of their distance from the camera. + +We define an arbitrary max distance from camera and divide the range from camera do this max distance plane into 10 buckets. We firstly find the bucket index `d` of each grass blade based on its distance from camera, and then for every 10 grass blades, we cull all blades such that `bladeIndex % 10 < d`. + +### Performance + +* Tested on: Windows 10, AMD Ryzen 5 5600X 6-Core Processor @ 3.70GHz, 32GB RAM, NVIDIA GeForce RTX 3070 Ti (Personal Computer) +* Render resolution 640 x 480 +* Number of grass blades 2^16 unless otherwise noted +* Default camera position and view + +![](writeup/default_camera_view.jpg) + +We use `VK_LAYER_LUNARG_monitor` layer to measure FPS. For each tested method, 20 FPS measurements were made and averaged to produce a single performance metric. Raw data can be found at `writeup/rawdata.xlsx`. +> ```cpp +> Instance::Instance(const char* applicationName, unsigned int additionalExtensionCount, > const char** additionalExtensions) { +> ... +> const char* instance_layers[] = { "VK_LAYER_LUNARG_monitor" }; +> createInfo.enabledLayerCount = 1; +> createInfo.ppEnabledLayerNames = instance_layers; +> ... +> } +> ``` + +#### Varying Number of Grass Blades + +![](writeup/average_fps_num_grass.png) + +We can immediately see that for high number of grass blades (2^12 and above), our algorithm approximately scales linearly FPS-wise with the number of grass blades. That is, as the number of grass blades increase by 4x each time, the average FPS is decreased by approximately 4x as well. This shows the scalability and effectiveness of our grass representation and culling approach. + +Another interesting observation to be made is not having this trend with small number of grass blades (2^8 and 2^10). This may be due to the fact that these number of grass blades is so low that we are reaching physical limitations of memory bandwidth and graphics pipeline state changes. Recall that each frame as reported by the monitoring layer includes both compute shader and rendering pipelines, so a reported average of 8000+ FPS means more than 8000 iterations of the command queue executions and relevant memory transfers. This limitation is currently a speculation, but future works involving more detailed profiling with tools such as Nvidia Insight may prove or disprove this conjecture. + +#### Varying Culling Options + +![](writeup/average_fps_culling.png) + +We can observe here the effectiveness of each culling method for our specific testing setup. We see that the orientation culling improve performance by **5.2%**, the view-frustum culling by **17.8%**, the distance calling by **95.2%**, and all culling methods combined by **153.9%**. + +The orientation culling not getting much performance boost is somewhat expected as all grass blades are initialized to a random direction vector. This means that there is a 10% chance (as our threshold is 0.9 for degree of orientation alignment) in a purely random grass generation algorithm that a grass will fit the culling criteria. Performance gain of 5.2% in that sense is a reasonable result. + +The default camera view most definitely does not include the entirety of the scene in its view frustum. The exact proportion of the scene that is outside of the tolerance of our view-frustum culling heuristic is hard to compute, but this culled scene structure is most likely no less than 15%. For this, performance gain of 17.8% is also a reasonable result. + +The distance culling provides the most dramatic increase in performance. Undoubtedly, not drawing at all is the cheapest drawing operation we can have. Assuming completely even distribution of grass blades across our distance clamping range, we know that in expectation we will cull 50% of all of our grass blades, as the percentage of blades culled is inversely proportional to the bucket approximation of the distance from camera. Therefore, 50% culling of grass blades should result in approximately double the performance, which is practically equivalent to our 95.2% performance gain. + +It is interesting to note that the overall performance gain of all culling methods combined is greater than compounding of each culling method. We speculate that this may be due to the fact that the blades culled by each methodology are distinct so we get full benefits of compounding culling as well as such compounding improvements being transferred to rendering pipeline efficiencies as well. However, more investigation should follow to better identify why this may be the case. + +#### Varying Tessellation Level based on Distance + +For a more dynamic and effective rendering of grass blade, we differ the level of tessellation based on the distance from camera. We convert the input grass blade position `v0` into camera view space, clamp its distance from camera to arbitrary near and far planes, and linearly interpolate the level of detail for tessellation between 20 (closest) and 4 (farthest). This interpolated level of detail is then truncated to integer and used as our actual inner and outer tessellation levels in the tessellation control shader. + +![](writeup/average_fps_lod.png) + +Since we do not want to compromise on the quality of the closest grass blades as their appearance is the most visible to us and determines our impression of the effectiveness of our rendering, we compared dynamic LoD computation scheme described above to statically setting tessellation level to 20 for all grass blades. We can observe **75.1%** performance gain with dynamic distance-based LoD scheme. This improvement matches our expectation as the scene is constructed to have grass blades evenly distributed throughout the entire depth range, so a lot of grass blades are guaranteed to be cheaper to tessellate and render. + +## Credits + +- [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) diff --git a/bin/Release/vulkan_grass_rendering.exe b/bin/Release/vulkan_grass_rendering.exe index f68db3a..8503329 100644 Binary files a/bin/Release/vulkan_grass_rendering.exe and b/bin/Release/vulkan_grass_rendering.exe 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..fc23ba7 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 << 17; 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..e3957e1 100644 --- a/src/Renderer.cpp +++ b/src/Renderer.cpp @@ -198,6 +198,39 @@ 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 + // Describe the binding of the descriptor set layout + VkDescriptorSetLayoutBinding bladesInputLayoutBinding = {}; + bladesInputLayoutBinding.binding = 0; + bladesInputLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + bladesInputLayoutBinding.descriptorCount = 1; + bladesInputLayoutBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + bladesInputLayoutBinding.pImmutableSamplers = nullptr; + + VkDescriptorSetLayoutBinding bladesCulledLayoutBinding = {}; + bladesCulledLayoutBinding.binding = 1; + bladesCulledLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + bladesCulledLayoutBinding.descriptorCount = 1; + bladesCulledLayoutBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + bladesCulledLayoutBinding.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 = { bladesInputLayoutBinding, bladesCulledLayoutBinding, numBladesLayoutBinding }; + + // Create the descriptor set layout + VkDescriptorSetLayoutCreateInfo layoutInfo = {}; + layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + layoutInfo.bindingCount = static_cast(bindings.size()); + layoutInfo.pBindings = bindings.data(); + + if (vkCreateDescriptorSetLayout(logicalDevice, &layoutInfo, nullptr, &computeDescriptorSetLayout) != VK_SUCCESS) { + throw std::runtime_error("Failed to create descriptor set layout"); + } } void Renderer::CreateDescriptorPool() { @@ -216,6 +249,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, 3 }, }; VkDescriptorPoolCreateInfo poolInfo = {}; @@ -360,6 +394,68 @@ 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 + // Describe the desciptor set + VkDescriptorSetLayout layouts[] = { computeDescriptorSetLayout }; + VkDescriptorSetAllocateInfo allocInfo = {}; + allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + allocInfo.descriptorPool = descriptorPool; + allocInfo.descriptorSetCount = 1; + allocInfo.pSetLayouts = layouts; + + // Allocate descriptor sets + if (vkAllocateDescriptorSets(logicalDevice, &allocInfo, &computeDescriptorSet) != VK_SUCCESS) { + throw std::runtime_error("Failed to allocate descriptor set"); + } + + std::array descriptorWrites = {}; + + VkDescriptorBufferInfo bladesInputBufferInfo = {}; + bladesInputBufferInfo.buffer = scene->GetBlades()[0]->GetBladesBuffer(); + bladesInputBufferInfo.offset = 0; + bladesInputBufferInfo.range = NUM_BLADES * sizeof(Blade); + + descriptorWrites[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + descriptorWrites[0].dstSet = computeDescriptorSet; + descriptorWrites[0].dstBinding = 0; + descriptorWrites[0].dstArrayElement = 0; + descriptorWrites[0].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + descriptorWrites[0].descriptorCount = 1; + descriptorWrites[0].pBufferInfo = &bladesInputBufferInfo; + descriptorWrites[0].pImageInfo = nullptr; + descriptorWrites[0].pTexelBufferView = nullptr; + + VkDescriptorBufferInfo bladesCulledBufferInfo = {}; + bladesCulledBufferInfo.buffer = scene->GetBlades()[0]->GetCulledBladesBuffer(); + bladesCulledBufferInfo.offset = 0; + bladesCulledBufferInfo.range = NUM_BLADES * sizeof(Blade); + + descriptorWrites[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + descriptorWrites[1].dstSet = computeDescriptorSet; + descriptorWrites[1].dstBinding = 1; + descriptorWrites[1].dstArrayElement = 0; + descriptorWrites[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + descriptorWrites[1].descriptorCount = 1; + descriptorWrites[1].pBufferInfo = &bladesCulledBufferInfo; + descriptorWrites[1].pImageInfo = nullptr; + descriptorWrites[1].pTexelBufferView = nullptr; + + VkDescriptorBufferInfo numBladesBufferInfo = {}; + numBladesBufferInfo.buffer = scene->GetBlades()[0]->GetNumBladesBuffer(); + numBladesBufferInfo.offset = 0; + numBladesBufferInfo.range = sizeof(BladeDrawIndirect); + + descriptorWrites[2].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + descriptorWrites[2].dstSet = computeDescriptorSet; + descriptorWrites[2].dstBinding = 2; + descriptorWrites[2].dstArrayElement = 0; + descriptorWrites[2].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + descriptorWrites[2].descriptorCount = 1; + descriptorWrites[2].pBufferInfo = &numBladesBufferInfo; + descriptorWrites[2].pImageInfo = nullptr; + descriptorWrites[2].pTexelBufferView = nullptr; + + // Update descriptor sets + vkUpdateDescriptorSets(logicalDevice, static_cast(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr); } void Renderer::CreateGraphicsPipeline() { @@ -716,8 +812,8 @@ 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 }; + // TODO: Add the compute descriptor set layout you create to this list + std::vector descriptorSetLayouts = { cameraDescriptorSetLayout, timeDescriptorSetLayout, computeDescriptorSetLayout }; // Create pipeline layout VkPipelineLayoutCreateInfo pipelineLayoutInfo = {}; @@ -884,6 +980,8 @@ 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 + vkCmdBindDescriptorSets(computeCommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, computePipelineLayout, 2, 1, &computeDescriptorSet, 0, nullptr); + vkCmdDispatch(computeCommandBuffer, (NUM_BLADES + WORKGROUP_SIZE - 1) / WORKGROUP_SIZE, 1, 1); // ~ End recording ~ if (vkEndCommandBuffer(computeCommandBuffer) != VK_SUCCESS) { @@ -976,13 +1074,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 // 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 +1155,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..a893392 100644 --- a/src/Renderer.h +++ b/src/Renderer.h @@ -56,12 +56,14 @@ class Renderer { VkDescriptorSetLayout cameraDescriptorSetLayout; VkDescriptorSetLayout modelDescriptorSetLayout; VkDescriptorSetLayout timeDescriptorSetLayout; + VkDescriptorSetLayout computeDescriptorSetLayout; VkDescriptorPool descriptorPool; VkDescriptorSet cameraDescriptorSet; std::vector modelDescriptorSets; VkDescriptorSet timeDescriptorSet; + VkDescriptorSet computeDescriptorSet; VkPipelineLayout graphicsPipelineLayout; VkPipelineLayout grassPipelineLayout; 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..2783046 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -16,7 +16,7 @@ namespace { if (width == 0 || height == 0) return; vkDeviceWaitIdle(device->GetVkDevice()); - swapChain->Recreate(); + swapChain->Recreate(width, height); renderer->RecreateFrameResources(); } diff --git a/src/shaders/compute.comp b/src/shaders/compute.comp index 0fd0224..2e977d1 100644 --- a/src/shaders/compute.comp +++ b/src/shaders/compute.comp @@ -26,31 +26,168 @@ struct Blade { // 2. Write out the culled blades // 3. Write the total number of blades remaining +layout (set = 2, binding = 0) buffer BladesInput { + Blade[] bladesInput; +}; +layout (set = 2, binding = 1) buffer BladesCulled { + Blade[] bladesCulled; +}; + // 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 = 2) buffer NumBlades { + uint vertexCount; // Write the number of blades remaining here + uint instanceCount; // = 1 + uint firstVertex; // = 0 + uint firstInstance; // = 0 +} numBlades; bool inBounds(float value, float bounds) { return (value >= -bounds) && (value <= bounds); } +#define M_TWO_PI 6.28318530718 +#define M_PI 3.1415926535897932384626433832795 +#define M_PI_HALF 1.57079632679 + +#define ANIM_SPEED 0.15 +#define TOLERANCE 0.3 + +struct Wind { + vec3 w_direction; + float amplitude; +}; + +Wind windDirectional(vec3 v0) { + Wind wind; + + wind.w_direction = vec3(1, 0, 1); + wind.amplitude = 10.0f * abs(sin(M_TWO_PI * totalTime * 1.0f + - wind.w_direction.x * sin(v0.x * ANIM_SPEED) + - wind.w_direction.y * sin(v0.y * ANIM_SPEED) + - wind.w_direction.z * sin(v0.z * ANIM_SPEED))); + return wind; +} + +Wind windHelicopter(vec3 v0) { + Wind wind; + + vec3 w_origin = vec3(3.0f * sin(M_TWO_PI * totalTime * ANIM_SPEED), + 0.0, + 3.0f * cos(M_TWO_PI * totalTime * ANIM_SPEED)); + + wind.w_direction = v0 - w_origin; + + float dist_amp = 100.0f / length(wind.w_direction); + float wind_amp = 1.0f; + float wavelength = 1.0f; + float wavespeed = 1.0f; + + wind.amplitude = dist_amp + + wind_amp * sin(M_TWO_PI * sqrt(wind.w_direction.x * wind.w_direction.x + + wind.w_direction.y * wind.w_direction.y + + wind.w_direction.z * wind.w_direction.z) / wavelength + - wavespeed / wavelength * totalTime); + return wind; +} + +bool viewFrustumTest(vec3 p) { + vec4 p_prime = camera.proj * camera.view * vec4(p, 1.0); + float h = p_prime.w + TOLERANCE; + return p_prime.x >= -h && p_prime.x <= h + && p_prime.y >= -h && p_prime.y <= h + && p_prime.z >= -h && p_prime.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 // TODO: Apply forces on every blade and update the vertices in the buffer + Blade currBlade = bladesInput[gl_GlobalInvocationID.x]; + + vec3 v0 = currBlade.v0.xyz; + vec3 v1 = currBlade.v1.xyz; + vec3 v2 = currBlade.v2.xyz; + vec3 up = currBlade.up.xyz; + + // Gravity, assume t = 0 + vec3 g_D = vec3(0, -1, 0); + float g_Mag = 9.83f; + vec3 g_E = normalize(g_D) * g_Mag; + vec3 g_F = 0.25 * length(g_E) * vec3(cos(currBlade.v0.w + M_PI_HALF), 0, sin(currBlade.v0.w + M_PI_HALF)); + vec3 g = g_E + g_F; + + // Recovery + float eta = 0.0f; + vec3 I_v2 = v0 + up * currBlade.v1.w; + vec3 r = (I_v2 - v2) * currBlade.up.w * max(1.0f - eta, 0.1f); + + // Wind + Wind wind; + + // Directional wind + // wind = windDirectional(v0); + // Helicopter wind + wind = windHelicopter(v0); + + vec3 w_i = normalize(wind.w_direction) * wind.amplitude; + + // theta = f_d(w_i(v_0)) * f_r(h) + float theta = (1.0f - abs(dot(normalize(w_i), normalize(v2 - v0)))) * dot(v2 - v0, up) / currBlade.v1.w; + vec3 w = w_i * theta; + + // Total Force + v2 += (g + r + w) * deltaTime; + v2 = v2 - up * min(dot(up, v2 - v0), 0); + + float l_proj = length(v2 - v0 - up * dot(v2 - v0, up)); + v1 = v0 + currBlade.v1.w * up * max(1.0f - l_proj / currBlade.v1.w, 0.05f * max(l_proj / currBlade.v1.w, 1.0f)); + + // n = 3 + float L = (2.0f * length(v2 - v0) + 2.0f * (length(v2 - v1) + length(v1 - v0))) / 4.0f; + + currBlade.v1.xyz = v0 + currBlade.v1.w / L * (v1 - v0); + currBlade.v2.xyz = currBlade.v1.xyz + currBlade.v1.w / L * (v2 - v1); + bladesInput[gl_GlobalInvocationID.x] = currBlade; // 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 + + // Use updated v1 v values + v1 = currBlade.v1.xyz; + v2 = currBlade.v2.xyz; + + // Orientation culling + vec3 dir_c = (camera.view * vec4(0.0, 0.0, -1.0, 1.0)).xyz; + vec3 dir_b = vec3(cos(currBlade.v0.w), 0, sin(currBlade.v0.w)); + if (0.9 > abs(dot(dir_c, dir_b))) { + return; + } + + // View-frustum culling + vec3 m = 0.25f * v0 + 0.5f * v1 + 0.25f * v2; + if (!(viewFrustumTest(v0) || viewFrustumTest(m) || viewFrustumTest(v2))) { + return; + } + + // Distance culling + vec3 v0View = (camera.view * vec4(v0, 1.0)).xyz; + vec3 upView = (camera.view * vec4(up, 0.0)).xyz; + float d_proj = length(v0View - upView * dot(v0View, upView)); // c = origin in view space + + float d_max = 20.0f; + int n = 10; + if (gl_GlobalInvocationID.x % n < floor(float(n) * d_proj / d_max)) { + return; + } + + uint idx = atomicAdd(numBlades.vertexCount, 1); + bladesCulled[idx] = currBlade; } diff --git a/src/shaders/grass.frag b/src/shaders/grass.frag index c7df157..aec4f18 100644 --- a/src/shaders/grass.frag +++ b/src/shaders/grass.frag @@ -7,11 +7,20 @@ layout(set = 0, binding = 0) uniform CameraBufferObject { } camera; // TODO: Declare fragment shader inputs +layout (location = 0) in vec3 pos; +layout (location = 1) in vec3 nor; +layout (location = 2) in vec2 uv; layout(location = 0) out vec4 outColor; +const vec3 directionalLight = normalize(vec3(1, -3, 1)); + void main() { // TODO: Compute fragment color + vec3 grassAlbedo = vec3(0.0, 1.0, 0.0); + + // Directional lighting + vec3 grassLit = grassAlbedo * max(0.2, abs(dot(directionalLight, nor))); - outColor = vec4(1.0); + outColor = vec4(grassLit, 1.0); } diff --git a/src/shaders/grass.tesc b/src/shaders/grass.tesc index f9ffd07..dd26323 100644 --- a/src/shaders/grass.tesc +++ b/src/shaders/grass.tesc @@ -9,18 +9,42 @@ layout(set = 0, binding = 0) uniform CameraBufferObject { } camera; // TODO: Declare tessellation control shader inputs and outputs +layout (location = 0) in vec4 iv0[]; +layout (location = 1) in vec4 iv1[]; +layout (location = 2) in vec4 iv2[]; +layout (location = 3) in vec4 iv3[]; + +layout (location = 0) out vec4 ov0[]; +layout (location = 1) out vec4 ov1[]; +layout (location = 2) out vec4 ov2[]; +layout (location = 3) out vec4 ov3[]; 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 + ov0[gl_InvocationID] = iv0[gl_InvocationID]; + ov1[gl_InvocationID] = iv1[gl_InvocationID]; + ov2[gl_InvocationID] = iv2[gl_InvocationID]; + ov3[gl_InvocationID] = iv3[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] = ??? + // Convert point into view space, compute distance from camera (origin) + float nearPlane = 0.1f; + float farPlane = 20.0f; + float dist = length((camera.view * vec4(iv0[gl_InvocationID].xyz, 1.0f)).xyz); + + // Linearly interpolate in view space + float u = clamp((dist - nearPlane) / (farPlane - nearPlane), 0.0, 1.0); + float closeLevel = 20.0f; + float farLevel = 4.0f; + int level = int(closeLevel * (1.0f - u) + farLevel * u); + + gl_TessLevelInner[0] = level; + gl_TessLevelInner[1] = level; + gl_TessLevelOuter[0] = level; + gl_TessLevelOuter[1] = level; + gl_TessLevelOuter[2] = level; + gl_TessLevelOuter[3] = level; } diff --git a/src/shaders/grass.tese b/src/shaders/grass.tese index 751fff6..a9da0d1 100644 --- a/src/shaders/grass.tese +++ b/src/shaders/grass.tese @@ -9,10 +9,76 @@ layout(set = 0, binding = 0) uniform CameraBufferObject { } camera; // TODO: Declare tessellation evaluation shader inputs and outputs +layout (location = 0) in vec4 iv0[]; +layout (location = 1) in vec4 iv1[]; +layout (location = 2) in vec4 iv2[]; +layout (location = 3) in vec4 iv3[]; + +layout (location = 0) out vec3 pos; +layout (location = 1) out vec3 nor; +layout (location = 2) out vec2 uv; + +float interpQuad(float u, float v) { + return u; +} + +float interpTri(float u, float v) { + return u + 0.5f * v - u * v; +} + +float interpQuadratic(float u, float v) { + return u - u * v * v; +} + +float interpTriTip(float u, float v, float tau) { + return 0.5f + (u - 0.5f) * (1.0f - max(v - tau, 0) / (1.0f - tau)); +} 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 + /* From Blades.h + // Position and direction + glm::vec4 v0; + // Bezier point and height + glm::vec4 v1; + // Physical model guide and width + glm::vec4 v2; + // Up vector and stiffness coefficient + glm::vec4 up; + */ + // Blade geometry as in Section 6.3 + vec3 v0 = iv0[0].xyz; + vec3 v1 = iv1[0].xyz; + vec3 v2 = iv2[0].xyz; + + vec3 a = v0 + v * (v1 - v0); + vec3 b = v1 + v * (v2 - v1); + vec3 c = a + v * (b - a); + + // direction vector does not have vertical component + float direction = iv0[0].w; + vec3 t1 = vec3(cos(direction), 0.0, sin(direction)); + + float width = iv2[0].w; + vec3 c0 = c - width * t1; + vec3 c1 = c + width * t1; + + vec3 t0 = (b - a) / length(b - a); + vec3 n = cross(t0, t1) / length(cross(t0, t1)); + + // float t = interpQuad(u, v); + // float t = interpTri(u, v); + float t = interpQuadratic(u, v); + // float t = interpTriTip(u, v, 0.5f); + + vec3 p = (1.0f - t) * c0 + t * c1; + + gl_Position = camera.proj * camera.view * vec4(p, 1.0); + + pos = p; + nor = n; + uv = vec2(u, v); } diff --git a/src/shaders/grass.vert b/src/shaders/grass.vert index db9dfe9..0020c81 100644 --- a/src/shaders/grass.vert +++ b/src/shaders/grass.vert @@ -7,11 +7,35 @@ layout(set = 1, binding = 0) uniform ModelBufferObject { }; // TODO: Declare vertex shader inputs and outputs +/* From Blades.h + // Position and direction + glm::vec4 v0; + // Bezier point and height + glm::vec4 v1; + // Physical model guide and width + glm::vec4 v2; + // Up vector and stiffness coefficient + glm::vec4 up; +*/ +layout(location = 0) in vec4 v0; +layout(location = 1) in vec4 v1; +layout(location = 2) in vec4 v2; +layout(location = 3) in vec4 up; -out gl_PerVertex { - vec4 gl_Position; -}; +layout (location = 0) out vec4 ov0; +layout (location = 1) out vec4 ov1; +layout (location = 2) out vec4 ov2; +layout (location = 3) out vec4 ov3; void main() { // TODO: Write gl_Position and any other shader outputs + vec4 v0Trans = model * vec4(v0.xyz, 1.0); + vec4 v1Trans = model * vec4(v1.xyz, 1.0); + vec4 v2Trans = model * vec4(v2.xyz, 1.0); + vec4 upTrans = model * vec4(up.xyz, 0.0); + + ov0 = vec4(v0Trans.xyz / v0Trans.w, v0.w); + ov1 = vec4(v1Trans.xyz / v1Trans.w, v1.w); + ov2 = vec4(v2Trans.xyz / v2Trans.w, v2.w); + ov3 = vec4(upTrans.xyz / upTrans.w, up.w); } diff --git a/writeup/average_fps_culling.png b/writeup/average_fps_culling.png new file mode 100644 index 0000000..2ec30bb Binary files /dev/null and b/writeup/average_fps_culling.png differ diff --git a/writeup/average_fps_lod.png b/writeup/average_fps_lod.png new file mode 100644 index 0000000..e08cc77 Binary files /dev/null and b/writeup/average_fps_lod.png differ diff --git a/writeup/average_fps_num_grass.png b/writeup/average_fps_num_grass.png new file mode 100644 index 0000000..337fd09 Binary files /dev/null and b/writeup/average_fps_num_grass.png differ diff --git a/writeup/default_camera_view.jpg b/writeup/default_camera_view.jpg new file mode 100644 index 0000000..842a745 Binary files /dev/null and b/writeup/default_camera_view.jpg differ diff --git a/writeup/grass.mov b/writeup/grass.mov new file mode 100644 index 0000000..d0ee778 Binary files /dev/null and b/writeup/grass.mov differ diff --git a/writeup/rawdata.xlsx b/writeup/rawdata.xlsx new file mode 100644 index 0000000..31c2ff6 Binary files /dev/null and b/writeup/rawdata.xlsx differ diff --git a/writeup/wind_dir.png b/writeup/wind_dir.png new file mode 100644 index 0000000..0b65c69 Binary files /dev/null and b/writeup/wind_dir.png differ diff --git a/writeup/wind_eq.png b/writeup/wind_eq.png new file mode 100644 index 0000000..eb7b43c Binary files /dev/null and b/writeup/wind_eq.png differ