diff --git a/README.md b/README.md index a744a2e..9487b5f 100644 --- a/README.md +++ b/README.md @@ -1,297 +1,28 @@ -Instructions - Vulkan Grass Rendering -======================== +Vulkan Grass Rendering +====================== -This is due **Sunday 11/5, evening at midnight**. +**University of Pennsylvania, CIS 565: GPU Programming and Architecture, Project 6** -**Summary:** -In this project, you will use Vulkan to implement a grass simulator and renderer. You will -use compute shaders to perform physics calculations on Bezier curves that represent individual -grass blades in your application. Since rendering every grass blade on every frame will is fairly -inefficient, you will also use compute shaders to cull grass blades that don't contribute to a given frame. -The remaining blades will be passed to a graphics pipeline, in which you will write several shaders. -You will write a vertex shader to transform Bezier control points, tessellation shaders to dynamically create -the grass geometry from the Bezier curves, and a fragment shader to shade the grass blades. +* Joseph Klinger +* Tested on: Windows 10, i5-7300HQ (4 CPUs) @ ~2.50GHz, GTX 1050 6030MB (Personal Machine) -The base code provided includes all of the basic Vulkan setup, including a compute pipeline that will run your compute -shaders and two graphics pipelines, one for rendering the geometry that grass will be placed on and the other for -rendering the grass itself. Your job will be to write the shaders for the grass graphics pipeline and the compute pipeline, -as well as binding any resources (descriptors) you may need to accomplish the tasks described in this assignment. +### Demo Video -![](img/grass.gif) +[Link.](https://vimeo.com/242475364) -You are not required to use this base code if you don't want -to. You may also change any part of the base code as you please. -**This is YOUR project.** The above .gif is just a simple example that you -can use as a reference to compare to. +### README -**Important:** -- If you are not in CGGT/DMD, you may replace this project with a GPU compute -project. You MUST get this pre-approved by Austin Eng before continuing! +This week, I worked on implementing a published paper on the efficient rendering of grass for 3d scenes. The project was also intended to be an introduction to Vulkan + and tesselation and compute shaders in GLSL. As a brief summary, the paper represents a blade of grass as a 3-point, quadratic Bezier curve. Tesselation shaders are used to create + geometry, ultimately creating 2D geometry in a 3D scene. A compute shader is used to cull grass blades that are otherwise unnecessary to render. -### Contents +# Features -* `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. -* `img/` Screenshots and images to use in your READMEs +Compute shader - implemented basic physically based computations to animate the grass blades. Forces included wind, gravity and stiffness/recovery. Additionally, the compute shader is +responsible for culling unnecessary grass blades based on orientation (grass blade is angled ~90 in relation to the camera, which can create artifacts during rasterization), the view frustum itself, +and distance (far away grass blades don't need to be rendered). -### Installing Vulkan +Tesselation shader - based on the three Bezier control points, created renderable geometry in tesselation control/evaluation shaders. Note that there is actually a small, unresolved bug that causes geometry to be shaded +black. -In order to run a Vulkan project, you first need to download and install the [Vulkan SDK](https://vulkan.lunarg.com/). -Make sure to run the downloaded installed as administrator so that the installer can set the appropriate environment -variables for you. - -Once you have done this, you need to make sure your GPU driver supports Vulkan. Download and install a -[Vulkan driver](https://developer.nvidia.com/vulkan-driver) from NVIDIA's website. - -Finally, to check that Vulkan is ready for use, go to your Vulkan SDK directory (`C:/VulkanSDK/` unless otherwise specified) -and run the `cube.exe` example within the `Bin` directory. IF you see a rotating gray cube with the LunarG logo, then you -are all set! - -### Running the code - -While developing your grass renderer, you will want to keep validation layers enabled so that error checking is turned on. -The project is set up such that when you are in `debug` mode, validation layers are enabled, and when you are in `release` mode, -validation layers are disabled. After building the code, you should be able to run the project without any errors. You will see -a plane with a grass texture on it to begin with. - -![](img/cube_demo.png) - -## Requirements - -**Ask on the mailing list for any clarifications.** - -In this project, you are given the following code: - -* The basic setup for a Vulkan project, including the swapchain, physical device, logical device, and the pipelines described above. -* Structs for some of the uniform buffers you will be using. -* Some buffer creation utility functions. -* A simple interactive camera using the mouse. - -You need to implement the following features/pipeline stages: - -* Compute shader (`shaders/compute.comp`) -* Grass pipeline stages - * Vertex shader (`shaders/grass.vert') - * Tessellation control shader (`shaders/grass.tesc`) - * Tessellation evaluation shader (`shaders/grass.tese`) - * Fragment shader (`shaders/grass.frag`) -* Binding of any extra descriptors you may need - -See below for more guidance. - -## Base Code Tour - -Areas that you need to complete are -marked with a `TODO` comment. Functions that are useful -for reference are marked with the comment `CHECKITOUT`. - -* `src/main.cpp` is the entry point of our application. -* `src/Instance.cpp` sets up the application state, initializes the Vulkan library, and contains functions that will create our -physical and logical device handles. -* `src/Device.cpp` manages the logical device and sets up the queues that our command buffers will be submitted to. -* `src/Renderer.cpp` contains most of the rendering implementation, including Vulkan setup and resource creation. You will -likely have to make changes to this file in order to support changes to your pipelines. -* `src/Camera.cpp` manages the camera state. -* `src/Model.cpp` manages the state of the model that grass will be created on. Currently a plane is hardcoded, but feel free to -update this with arbitrary model loading! -* `src/Blades.cpp` creates the control points corresponding to the grass blades. There are many parameters that you can play with -here that will change the behavior of your rendered grass blades. -* `src/Scene.cpp` manages the scene state, including the model, blades, and simualtion time. -* `src/BufferUtils.cpp` provides helper functions for creating buffers to be used as descriptors. - -We left out descriptions for a couple files that you likely won't have to modify. Feel free to investigate them to understand their -importance within the scope of the project. - -## Grass Rendering - -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). -Please make sure to use this paper as a primary resource while implementing your grass renderers. It does a great job of explaining -the key algorithms and math you will be using. Below is a brief description of the different components in chronological order of how your renderer will -execute, but feel free to develop the components in whatever order you prefer. - -### Representing Grass as Bezier Curves - -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 (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 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. - -![](img/blade_model.jpg) - -### Simulating Forces - -In this project, you will be simulating forces on grass blades while they are still Bezier curves. This will be done in a compute -shader using the compute pipeline that has been created for you. Remember that `v2` is our physical guide, so we will be -applying transformations to `v2` initially, then correcting for potential errors. We will finally update `v1` to maintain the appropriate -length of our grass blade. - -#### Binding Resources - -In order to update the state of your grass blades on every frame, you will need to create a storage buffer to maintain the grass data. -You will also need to pass information about how much time has passed in the simulation and the time since the last frame. To do this, -you can extend or create descriptor sets that will be bound to the compute pipeline. - -#### 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 - -In order to simulate wind, you are at liberty to create any wind function you want! In order to have something interesting, -you can make the function depend on the position of `v0` and a function that changes with time. Consider using some combination -of sine or cosine functions. - -Your wind function will determine a wind direction that is affecting the blade, but it is also worth noting that wind has a larger impact on -grass blades whose forward directions are parallel to the wind direction. The paper describes this as a "wind alignment" term. We won't go -over the exact math here, but use the paper as a reference when implementing this. It does a great job of explaining this! - -Once you have a wind direction and a wind alignment term, your total wind force (`w`) will be `windDirection * windAlignment`. - -#### Total force - -We can then determine a translation for `v2` based on the forces as `tv2 = (gravity + recovery + wind) * deltaTime`. However, we can't simply -apply this translation and expect the simulation to be robust. Our forces might push `v2` under the ground! Similarly, moving `v2` but leaving -`v1` in the same position will cause our grass blade to change length, which doesn't make sense. - -Read section 5.2 of the paper in order to learn how to determine the corrected final positions for `v1` and `v2`. - -### 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 can use 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 can cull these blades! Simply do a dot product test to see if the view vector and front face direction of -the blade are perpendicular. The paper uses a threshold value of `0.9` to cull, but feel free to use what you think looks best. - -#### 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`. -Notice that we aren't using `v1` for the visibility test. This is because the `v1` is a Bezier guide that doesn't represent a position on the grass blade. -We instead use `m` to approximate the midpoint of our Bezier curve. - -If all three points are outside of the view-frustum, we will 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. - -#### 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. - -You are free to define two parameters here. -* A max distance afterwhich all grass blades will be culled. -* A number of buckets to place grass blades between the camera and max distance into. - -Define a function such that the grass blades in the bucket closest to the camera are kept while an increasing number of grass blades -are culled with each farther bucket. - -#### Occlusion culling (extra credit) - -This type of culling only makes sense if our scene has additional objects aside from the plane and the grass blades. We want to cull grass blades that -are occluded by other geometry. Think about how you can use a depth map to accomplish this! - -### Tessellating Bezier curves into grass blades - -In this project, you should pass in each Bezier curve as a single patch to be processed by your grass graphics pipeline. You will tessellate this patch into -a quad with a shape of your choosing (as long as it looks sufficiently like grass of course). The paper has some examples of grass shapes you can use as inspiration. - -In the tessellation control shader, specify the amount of tessellation you want to occur. Remember that you need to provide enough detail to create the curvature of a grass blade. - -The generated vertices will be passed to the tessellation evaluation shader, where you will place the vertices in world space, respecting the width, height, and orientation information -of each blade. Once you have determined the world space position of each vector, make sure to set the output `gl_Position` in clip space! - -** Extra Credit**: Tessellate to varying levels of detail as a function of how far the grass blade is from the camera. For example, if the blade is very far, only generate four vertices in the tessellation control shader. - -To build more intuition on how tessellation works, I highly recommend playing with the [helloTessellation sample](https://github.com/CIS565-Fall-2017/Vulkan-Samples/tree/master/samples/5_helloTessellation) -and reading this [tutorial on tessellation](http://in2gpu.com/2014/07/12/tessellation-tutorial-opengl-4-3/). - -## Resources - -### Links - -The following resources may be useful for this project. - -* [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) -* [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](http://in2gpu.com/2014/07/12/tessellation-tutorial-opengl-4-3/) - - -## Third-Party Code Policy - -* Use of any third-party code must be approved by asking on our Google Group. -* If it is approved, all students are welcome to use it. Generally, we approve - use of third-party code that is not a core part of the project. For example, - for the path tracer, we would approve using a third-party library for loading - models, but would not approve copying and pasting a CUDA function for doing - refraction. -* Third-party code **MUST** be credited in README.md. -* Using third-party code without its approval, including using another - student's code, is an academic integrity violation, and will, at minimum, - result in you receiving an F for the semester. - - -## README - -* A brief description of the project and the specific features you implemented. -* At least one screenshot of your project running. -* A performance analysis (described below). - -### Performance Analysis - -The performance analysis is where you will investigate how... -* Your renderer handles varying numbers of grass blades -* The improvement you get by culling using each of the three culling tests - -## Submit - -If you have modified any of the `CMakeLists.txt` files at all (aside from the -list of `SOURCE_FILES`), mentions it explicity. -Beware of any build issues discussed on the Google Group. - -Open a GitHub pull request so that we can see that you have finished. -The title should be "Project 6: YOUR NAME". -The template of the comment section of your pull request is attached below, you can do some copy and paste: - -* [Repo Link](https://link-to-your-repo) -* (Briefly) Mentions features that you've completed. Especially those bells and whistles you want to highlight - * Feature 0 - * Feature 1 - * ... -* Feedback on the project itself, if any. +Vertex/fragment shader - Passes information further down the graphics pipeline and performs basic lambertian shading. \ No newline at end of file diff --git a/bin/Debug/vulkan_grass_rendering.exe b/bin/Debug/vulkan_grass_rendering.exe new file mode 100644 index 0000000..ea763a1 Binary files /dev/null and b/bin/Debug/vulkan_grass_rendering.exe differ diff --git a/bin/Debug/vulkan_grass_rendering.ilk b/bin/Debug/vulkan_grass_rendering.ilk new file mode 100644 index 0000000..0862fd9 Binary files /dev/null and b/bin/Debug/vulkan_grass_rendering.ilk differ diff --git a/bin/Debug/vulkan_grass_rendering.pdb b/bin/Debug/vulkan_grass_rendering.pdb new file mode 100644 index 0000000..0d81f46 Binary files /dev/null and b/bin/Debug/vulkan_grass_rendering.pdb differ diff --git a/bin/Release/vulkan_grass_rendering.exe b/bin/Release/vulkan_grass_rendering.exe new file mode 100644 index 0000000..9098890 Binary files /dev/null and b/bin/Release/vulkan_grass_rendering.exe differ diff --git a/src/Blades.h b/src/Blades.h index 9bd1eed..2628ccf 100644 --- a/src/Blades.h +++ b/src/Blades.h @@ -12,6 +12,10 @@ constexpr static float MAX_WIDTH = 0.14f; constexpr static float MIN_BEND = 7.0f; constexpr static float MAX_BEND = 13.0f; +struct BladeBufferObject { + glm::mat4 modelMatrix; +}; + struct Blade { // Position and direction glm::vec4 v0; diff --git a/src/Renderer.cpp b/src/Renderer.cpp index b445d04..690ec18 100644 --- a/src/Renderer.cpp +++ b/src/Renderer.cpp @@ -19,12 +19,13 @@ Renderer::Renderer(Device* device, SwapChain* swapChain, Scene* scene, Camera* c CreateRenderPass(); CreateCameraDescriptorSetLayout(); CreateModelDescriptorSetLayout(); + CreateGrassDescriptorSetLayout(); CreateTimeDescriptorSetLayout(); CreateComputeDescriptorSetLayout(); CreateDescriptorPool(); CreateCameraDescriptorSet(); CreateModelDescriptorSets(); - CreateGrassDescriptorSets(); + //CreateGrassDescriptorSets(); CreateTimeDescriptorSet(); CreateComputeDescriptorSets(); CreateFrameResources(); @@ -172,6 +173,27 @@ void Renderer::CreateModelDescriptorSetLayout() { } } +void Renderer::CreateGrassDescriptorSetLayout() { + VkDescriptorSetLayoutBinding uboLayoutBinding = {}; + uboLayoutBinding.binding = 0; + uboLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + uboLayoutBinding.descriptorCount = 1; + uboLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; + uboLayoutBinding.pImmutableSamplers = nullptr; + + std::vector bindings = { uboLayoutBinding }; + + // 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, &grassDescriptorSetLayout) != VK_SUCCESS) { + throw std::runtime_error("Failed to create descriptor set layout"); + } +} + void Renderer::CreateTimeDescriptorSetLayout() { // Describe the binding of the descriptor set layout VkDescriptorSetLayoutBinding uboLayoutBinding = {}; @@ -198,6 +220,44 @@ 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 + // bindings: num_Blades blades - before + // allocate num_blades space after culling - after + // num_blades after culling - after + + // Input blades, read only + VkDescriptorSetLayoutBinding sboLayoutBinding0 = {}; + sboLayoutBinding0.binding = 0; + sboLayoutBinding0.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + sboLayoutBinding0.descriptorCount = 1; + sboLayoutBinding0.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + sboLayoutBinding0.pImmutableSamplers = nullptr; + + // Write the result of the compute shader in a storage buffer + VkDescriptorSetLayoutBinding sboLayoutBinding1 = {}; + sboLayoutBinding1.binding = 1; + sboLayoutBinding1.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + sboLayoutBinding1.descriptorCount = 1; + sboLayoutBinding1.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + sboLayoutBinding1.pImmutableSamplers = nullptr; + + VkDescriptorSetLayoutBinding sboLayoutBinding2 = {}; + sboLayoutBinding2.binding = 2; + sboLayoutBinding2.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + sboLayoutBinding2.descriptorCount = 1; + sboLayoutBinding2.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + sboLayoutBinding2.pImmutableSamplers = nullptr; + + std::vector bindings = { sboLayoutBinding0, sboLayoutBinding1, sboLayoutBinding2 }; + + // 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() { @@ -215,7 +275,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 + // Add any additional types and counts of descriptors you will need to allocate + { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 3 * static_cast(scene->GetBlades().size()) }, }; VkDescriptorPoolCreateInfo poolInfo = {}; @@ -318,8 +379,45 @@ void Renderer::CreateModelDescriptorSets() { } void Renderer::CreateGrassDescriptorSets() { - // TODO: Create Descriptor sets for the grass. + // Create Descriptor sets for the grass. // This should involve creating descriptor sets which point to the model matrix of each group of grass blades + + grassDescriptorSets.resize(scene->GetBlades().size()); + + // Describe the desciptor set + VkDescriptorSetLayout layouts[] = { grassDescriptorSetLayout }; + VkDescriptorSetAllocateInfo allocInfo = {}; + allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + allocInfo.descriptorPool = descriptorPool; + allocInfo.descriptorSetCount = static_cast(grassDescriptorSets.size()); + allocInfo.pSetLayouts = layouts; + + // Allocate descriptor sets + if (vkAllocateDescriptorSets(logicalDevice, &allocInfo, grassDescriptorSets.data()) != VK_SUCCESS) { + throw std::runtime_error("Failed to allocate descriptor set"); + } + + std::vector descriptorWrites(grassDescriptorSets.size()); + + for (uint32_t i = 0; i < scene->GetBlades().size(); ++i) { + VkDescriptorBufferInfo bladesBufferInfo = {}; + bladesBufferInfo.buffer = scene->GetBlades()[i]->GetCulledBladesBuffer(); + bladesBufferInfo.offset = 0; + bladesBufferInfo.range = sizeof(BladeBufferObject) * NUM_BLADES; // how to get number of culled blades? + + 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 = &bladesBufferInfo; + descriptorWrites[i].pImageInfo = nullptr; + descriptorWrites[i].pTexelBufferView = nullptr; + } + + // Update descriptor sets + vkUpdateDescriptorSets(logicalDevice, static_cast(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr); } void Renderer::CreateTimeDescriptorSet() { @@ -359,7 +457,74 @@ 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 + // The descriptors should point to Storage buffers which will hold the grass blades, the culled grass blades, and the output number of grass blades + + computeDescriptorSets.resize(scene->GetBlades().size()); + + // Describe the desciptor set + VkDescriptorSetLayout layouts[] = { computeDescriptorSetLayout }; + VkDescriptorSetAllocateInfo allocInfo = {}; + allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + allocInfo.descriptorPool = descriptorPool; + allocInfo.descriptorSetCount = static_cast(computeDescriptorSets.size()); + allocInfo.pSetLayouts = layouts; + + // Allocate descriptor sets + if (vkAllocateDescriptorSets(logicalDevice, &allocInfo, computeDescriptorSets.data()) != VK_SUCCESS) { + throw std::runtime_error("Failed to allocate descriptor set"); + } + + std::vector descriptorWrites(3 * computeDescriptorSets.size()); + + for (uint32_t i = 0; i < scene->GetBlades().size(); ++i) { + VkDescriptorBufferInfo bladeBuffer = {}; + bladeBuffer.buffer = scene->GetBlades()[i]->GetBladesBuffer(); + bladeBuffer.offset = 0; + bladeBuffer.range = NUM_BLADES * sizeof(Blade); + + VkDescriptorBufferInfo bladeBufferCulled = {}; + bladeBufferCulled.buffer = scene->GetBlades()[i]->GetCulledBladesBuffer(); + bladeBufferCulled.offset = 0; + bladeBufferCulled.range = NUM_BLADES * sizeof(Blade); + + VkDescriptorBufferInfo numBladeBuffer = {}; + numBladeBuffer.buffer = scene->GetBlades()[i]->GetNumBladesBuffer(); + numBladeBuffer.offset = 0; + numBladeBuffer.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 = &bladeBuffer; + 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 = &bladeBufferCulled; + 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 = &numBladeBuffer; + descriptorWrites[3 * i + 2].pImageInfo = nullptr; + descriptorWrites[3 * i + 2].pTexelBufferView = nullptr; + } + + // Update descriptor sets + vkUpdateDescriptorSets(logicalDevice, static_cast(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr); } void Renderer::CreateGraphicsPipeline() { @@ -553,6 +718,7 @@ void Renderer::CreateGrassPipeline() { fragShaderStageInfo.module = fragShaderModule; fragShaderStageInfo.pName = "main"; + // Temporarily remove tesselation shaders from the pipeline so we can do an easy visualization of the grass VkPipelineShaderStageCreateInfo shaderStages[] = { vertShaderStageInfo, tescShaderStageInfo, teseShaderStageInfo, fragShaderStageInfo }; // --- Set up fixed-function stages --- @@ -654,7 +820,7 @@ void Renderer::CreateGrassPipeline() { colorBlending.blendConstants[2] = 0.0f; colorBlending.blendConstants[3] = 0.0f; - std::vector descriptorSetLayouts = { cameraDescriptorSetLayout, modelDescriptorSetLayout }; + std::vector descriptorSetLayouts = { cameraDescriptorSetLayout, modelDescriptorSetLayout, grassDescriptorSetLayout }; // Pipeline layout: used to specify uniform values VkPipelineLayoutCreateInfo pipelineLayoutInfo = {}; @@ -716,8 +882,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 }; + // Add the compute dsecriptor set layout you create to this list + std::vector descriptorSetLayouts = { cameraDescriptorSetLayout, timeDescriptorSetLayout, computeDescriptorSetLayout }; // Create pipeline layout VkPipelineLayoutCreateInfo pipelineLayoutInfo = {}; @@ -884,6 +1050,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 j = 0; j < scene->GetBlades().size(); ++j) { + vkCmdBindDescriptorSets(computeCommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, computePipelineLayout, 2, 1, &computeDescriptorSets[j], 0, nullptr); + vkCmdDispatch(computeCommandBuffer, (uint32_t) ceil((NUM_BLADES + WORKGROUP_SIZE - 1) / WORKGROUP_SIZE), 1, 1); + } // ~ End recording ~ if (vkEndCommandBuffer(computeCommandBuffer) != VK_SUCCESS) { @@ -974,15 +1144,17 @@ void Renderer::RecordCommandBuffers() { for (uint32_t j = 0; j < scene->GetBlades().size(); ++j) { VkBuffer vertexBuffers[] = { scene->GetBlades()[j]->GetCulledBladesBuffer() }; + //VkBuffer vertexBuffers[] = { scene->GetBlades()[j]->GetBladesBuffer() }; // bypass the compute pipeline? VkDeviceSize offsets[] = { 0 }; - // TODO: Uncomment this when the buffers are populated - // vkCmdBindVertexBuffers(commandBuffers[i], 0, 1, vertexBuffers, offsets); - // TODO: Bind the descriptor set for each grass blades model + // Bind vertex buffer(s) + vkCmdBindVertexBuffers(commandBuffers[i], 0, 1, vertexBuffers, offsets); + + // Bind the descriptor set for each grass blades model + //vkCmdBindDescriptorSets(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, grassPipelineLayout, 2, 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 @@ -1056,6 +1228,7 @@ Renderer::~Renderer() { vkDestroyDescriptorSetLayout(logicalDevice, cameraDescriptorSetLayout, nullptr); vkDestroyDescriptorSetLayout(logicalDevice, modelDescriptorSetLayout, nullptr); + vkDestroyDescriptorSetLayout(logicalDevice, grassDescriptorSetLayout, nullptr); vkDestroyDescriptorSetLayout(logicalDevice, timeDescriptorSetLayout, nullptr); vkDestroyDescriptorPool(logicalDevice, descriptorPool, nullptr); diff --git a/src/Renderer.h b/src/Renderer.h index 95e025f..b4b9f57 100644 --- a/src/Renderer.h +++ b/src/Renderer.h @@ -17,6 +17,7 @@ class Renderer { void CreateCameraDescriptorSetLayout(); void CreateModelDescriptorSetLayout(); + void CreateGrassDescriptorSetLayout(); void CreateTimeDescriptorSetLayout(); void CreateComputeDescriptorSetLayout(); @@ -55,12 +56,16 @@ class Renderer { VkDescriptorSetLayout cameraDescriptorSetLayout; VkDescriptorSetLayout modelDescriptorSetLayout; + VkDescriptorSetLayout grassDescriptorSetLayout; + VkDescriptorSetLayout computeDescriptorSetLayout; VkDescriptorSetLayout timeDescriptorSetLayout; VkDescriptorPool descriptorPool; VkDescriptorSet cameraDescriptorSet; std::vector modelDescriptorSets; + std::vector grassDescriptorSets; + std::vector computeDescriptorSets; VkDescriptorSet timeDescriptorSet; VkPipelineLayout graphicsPipelineLayout; diff --git a/src/shaders/compute.comp b/src/shaders/compute.comp index 0fd0224..6104628 100644 --- a/src/shaders/compute.comp +++ b/src/shaders/compute.comp @@ -29,12 +29,20 @@ struct Blade { // 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; + +layout(set = 2, binding = 1) buffer BladesIn { + Blade[] blades; +} blades; + +layout(set = 2, binding = 0) buffer BladesOut { + Blade[] blades; +} culledBlades; bool inBounds(float value, float bounds) { return (value >= -bounds) && (value <= bounds); @@ -43,14 +51,106 @@ bool inBounds(float value, float bounds) { 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 + + // Recovery Force + // Initial pose of V2 is v0 + up * height + const Blade currentBladeIn = blades.blades[gl_GlobalInvocationID.x]; + vec3 I_V2 = currentBladeIn.v0.xyz + currentBladeIn.up.xyz * currentBladeIn.v1.w; + const vec3 recoveryForce = (I_V2 - currentBladeIn.v2.xyz) * currentBladeIn.up.w; + + // Gravity + const vec3 gravityDir = vec3(0, 1, 0); + const float gravityAccel = 9.8; + const vec3 gravityForceE = gravityAccel * gravityDir; + const vec3 forwardDir = normalize(vec3(cos(currentBladeIn.v0.w), 0, sin(currentBladeIn.v0.w))); + const vec3 gravityForceF = length(gravityForceE) * forwardDir * 0.25; + const vec3 gravityForceTotal = gravityForceE + gravityForceF; + //const vec3 gravityForceTotal = vec3(0.0); + + // Wind + const vec3 windDir = vec3(cos(totalTime * 0.25 + currentBladeIn.v2.x * 0.25 + length(currentBladeIn.v2.z * 0.25 + totalTime * 0.25)), 0.0, sin(totalTime - currentBladeIn.v2.z * 0.25)) * 1.5; + const float directionalAlignment = 1.0 - length(windDir - normalize(currentBladeIn.v2.xyz - currentBladeIn.v0.xyz)); + const float heightRatio = dot((currentBladeIn.v2.xyz - currentBladeIn.v0.xyz), currentBladeIn.up.xyz) / currentBladeIn.v1.w; + const vec3 windForce = heightRatio * directionalAlignment * windDir; + + vec3 totalForce = recoveryForce + gravityForceTotal + windForce; + totalForce *= deltaTime; + + // Move V2 and V1 properly + Blade bladeOut; + + bladeOut.v2.xyz = currentBladeIn.v2.xyz + totalForce; + + // Pass v0 + bladeOut.v0 = currentBladeIn.v0; + + // Pass up + bladeOut.up = currentBladeIn.up; + + // Move V2 + bladeOut.v2.xyz = bladeOut.v2.xyz - bladeOut.up.xyz * min(dot(bladeOut.up.xyz, bladeOut.v2.xyz - bladeOut.v0.xyz), 0.0); + bladeOut.v2.w = currentBladeIn.v2.w; + + // Move V1 + const float lProj = length(bladeOut.v2.xyz - bladeOut.v0.xyz - bladeOut.up.xyz * dot(bladeOut.v2.xyz - bladeOut.v0.xyz, bladeOut.up.xyz)); + bladeOut.v1.xyz = bladeOut.v0.xyz + currentBladeIn.v1.w * bladeOut.up.xyz * max(1.0 - lProj / currentBladeIn.v1.w, 0.05 * max(lProj / currentBladeIn.v1.w, 1.0)); + bladeOut.v1.w = currentBladeIn.v1.w; + + // Check length of the Bezier curve + const float L0 = length(bladeOut.v2.xyz - bladeOut.v0.xyz); + const float L1 = length(bladeOut.v2.xyz - bladeOut.v1.xyz) + length(bladeOut.v1.xyz - bladeOut.v0.xyz); + const float n = 2.0; // degree 2 Bezier curve + const float L = (2.0 * L0 + (n - 1.0) * L1) / (n + 1.0); + const float r = currentBladeIn.v1.w / L; + bladeOut.v1.xyz = bladeOut.v0.xyz + r * (currentBladeIn.v1.xyz - bladeOut.v0.xyz); + bladeOut.v2.xyz = bladeOut.v1.xyz + r * (bladeOut.v2.xyz - currentBladeIn.v1.xyz); + + // CULLING + + bool shouldCull = false; + + // Orientation culling + const vec3 cameraPos = (inverse(camera.view) * vec4(0, 0, 0, 1)).xyz; + vec3 widthDir = normalize(vec3(cos(currentBladeIn.v0.w + 1.57079632679), 0, sin(currentBladeIn.v0.w + 1.57079632679))); // that's PI / 2 + widthDir = normalize((camera.view * vec4(widthDir, 0)).xyz); // put it in view space + if(abs(widthDir.z) < 0.2) { + shouldCull = true; + } + + // Frustum culling on v0, v2 and m + const vec3 m = 0.25 * bladeOut.v0.xyz + 0.5 * bladeOut.v1.xyz + 0.25 * bladeOut.v2.xyz; + // check if v0, v1, and m are all outside the frustum + + const vec4 p_prime_0 = camera.proj * camera.view * vec4(bladeOut.v0.xyz, 1.0); + const vec4 p_prime_m = camera.proj * camera.view * vec4(m, 1.0); + const vec4 p_prime_2 = camera.proj * camera.view * vec4(bladeOut.v2.xyz, 1.0); + + const float tolerance = 5.0; + const vec3 h = vec3(p_prime_0.w, p_prime_m.w, p_prime_2.w) + tolerance; + + if(!(p_prime_0.x > -h.x && p_prime_0.y > -h.x && p_prime_0.z > -h.x && + p_prime_0.x < h.x && p_prime_0.y < h.x && p_prime_0.z < h.x) && + !(p_prime_m.x > -h.y && p_prime_m.y > -h.y && p_prime_m.z > -h.y && + p_prime_m.x < h.y && p_prime_m.y < h.y && p_prime_m.z < h.y) && + !(p_prime_2.x > -h.z && p_prime_2.y > -h.z && p_prime_2.z > -h.z && + p_prime_2.x < h.z && p_prime_2.y < h.z && p_prime_2.z < h.z)) { + //shouldCull = true; + } + + // Distance culling + const float dMax = 20.0; // After this point, all blades are culled + const float dProj = length(bladeOut.v0.xyz - cameraPos - bladeOut.up.xyz * (dot(bladeOut.v0.xyz - cameraPos, bladeOut.up.xyz))); + //shouldCull = shouldCull || mod(gl_GlobalInvocationID.x, 8.0) > floor(8.0 * (1.0 - dProj / dMax)); + + if(!shouldCull) { + // Increment num blades + culledBlades.blades[atomicAdd(numBlades.vertexCount, 1)] = bladeOut; + } - // 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 + blades.blades[gl_GlobalInvocationID.x] = bladeOut; } diff --git a/src/shaders/grass.frag b/src/shaders/grass.frag index c7df157..a146ec8 100644 --- a/src/shaders/grass.frag +++ b/src/shaders/grass.frag @@ -6,12 +6,14 @@ layout(set = 0, binding = 0) uniform CameraBufferObject { mat4 proj; } camera; -// TODO: Declare fragment shader inputs +// Declare fragment shader inputs + +layout(location = 0) in vec3 fs_nor; +layout (location = 1) in vec3 fs_posVC; layout(location = 0) out vec4 outColor; void main() { - // TODO: Compute fragment color - - outColor = vec4(1.0); + float lambertTerm = dot(faceforward(fs_nor, normalize(fs_posVC), fs_nor), normalize((camera.view * vec4(1.0, 3.0, 1.0, 0.0)).xyz)); + outColor = vec4(0.0, 1.0, 0.0, 1.0) * lambertTerm; } diff --git a/src/shaders/grass.tesc b/src/shaders/grass.tesc index f9ffd07..3ef86d6 100644 --- a/src/shaders/grass.tesc +++ b/src/shaders/grass.tesc @@ -3,24 +3,28 @@ layout(vertices = 1) out; -layout(set = 0, binding = 0) uniform CameraBufferObject { - mat4 view; - mat4 proj; -} camera; +// Declare tessellation control shader inputs and outputs -// TODO: Declare tessellation control shader inputs and outputs +layout (location = 0) in vec4[] v1; // becomes an array in the tesselation shader despite being passed in as a vec4 from the vertex shader? magic! +layout (location = 1) in vec4[] v2; + +layout (location = 0) out vec4[] v1Array; +layout (location = 1) out vec4[] v2Array; 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 + // Writ shader outputs + v1Array[gl_InvocationID] = v1[gl_InvocationID]; + v2Array[gl_InvocationID] = v2[gl_InvocationID]; + + // Set level of tesselation - make quads + gl_TessLevelInner[0] = 3; + gl_TessLevelInner[1] = 3; - // 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] = 5; + gl_TessLevelOuter[1] = 4; + gl_TessLevelOuter[2] = 6; + gl_TessLevelOuter[3] = 7; } diff --git a/src/shaders/grass.tese b/src/shaders/grass.tese index 751fff6..c6ee8b3 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 +// Declare tessellation evaluation shader inputs and outputs + +layout (location = 0) in vec4[] v1Array; +layout (location = 1) in vec4[] v2Array; + +layout (location = 0) out vec3 fs_nor; +layout (location = 1) out vec3 fs_posVC; 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 + vec4 v0 = gl_in[0].gl_Position; + vec4 v1 = v1Array[0]; + vec4 v2 = v2Array[0]; + + float width = v2.w; + + // Do deCasteljau's algorithm to get the point on the Bezier curve and then use the tesselation weights + // Formulas are actually taken directly from the paper itself + vec3 a = v0.xyz + v * (v1.xyz - v0.xyz); + vec3 b = v1.xyz + v * (v2.xyz - v1.xyz); + vec3 c = a + v * (b - a); + + // Account for the rotation of this blade of grass + vec3 bit = normalize(vec3(cos(v1.w + 1.57079632679), 0, sin(v1.w + 1.57079632679))); // in the xz-plane + + vec3 tan = b - a; + vec3 normal = normalize(cross(tan, bit)); + + fs_nor = (camera.view * vec4(normal, 0)).xyz; + + u *= 1.0 - v; + u = 2.0 * u - 1.0; + vec3 offset = u * width * bit; + + vec4 pos = vec4(c.x, c.yz, 1.0) + vec4(offset, 0.0); + fs_posVC = (camera.view * pos).xyz; + gl_Position = camera.proj * camera.view * pos; } diff --git a/src/shaders/grass.vert b/src/shaders/grass.vert index db9dfe9..cb57e96 100644 --- a/src/shaders/grass.vert +++ b/src/shaders/grass.vert @@ -2,11 +2,19 @@ #version 450 #extension GL_ARB_separate_shader_objects : enable -layout(set = 1, binding = 0) uniform ModelBufferObject { - mat4 model; -}; +layout(set = 0, binding = 0) uniform CameraBufferObject { + mat4 view; + mat4 proj; +} camera; + +// Declare vertex shader inputs and outputs + +layout (location = 0) in vec4 v0In; +layout (location = 1) in vec4 v1In; +layout (location = 2) in vec4 v2In; -// TODO: Declare vertex shader inputs and outputs +layout (location = 0) out vec4 v1; +layout (location = 1) out vec4 v2; out gl_PerVertex { vec4 gl_Position; @@ -14,4 +22,9 @@ out gl_PerVertex { void main() { // TODO: Write gl_Position and any other shader outputs + // pass stuff that we will need later in the pipeline aka tesselation shaders + v1 = v1In; + v1.w = v0In.w; + v2 = v2In; + gl_Position = vec4(v0In.xyz, 1.0); }