diff --git a/README.md b/README.md index 20ee451..e7568cb 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,149 @@ Vulkan Grass Rendering **University of Pennsylvania, CIS 565: GPU Programming and Architecture, Project 5** -* (TODO) YOUR NAME HERE -* Tested on: (TODO) Windows 22, i7-2222 @ 2.22GHz 22GB, GTX 222 222MB (Moore 2222 Lab) +### Yuhan Liu -### (TODO: Your README) +[LinkedIn](https://www.linkedin.com/in/yuhan-liu-), [Personal Website](https://liuyuhan.me/), [Twitter](https://x.com/yuhanl_?lang=en) + +**Tested on: Windows 11 Pro, Ultra 7 155H @ 1.40 GHz 32GB, RTX 4060 8192MB (Personal Laptop)** + + + +In this project, I created a real-time grass simulation and rendering application using Vulkan. Each blade of grass is represented by a Bezier curve, enabling realistic motion and appearance. The simulator uses compute shaders for physics calculations and for culling non-visible blades to improve performance. After culling non-necessary blades, the remaining are passed through the graphics pipeline for rendering. The pipeline includes 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. + +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). + +## Vulkan Rendering + +This project uses the Vulkan API and GPU shaders to build a grass simulator and renderer that operates at real-time performance. It leverages a compute shader to apply physics to Bezier curve representations of individual grass blades. By culling non-visible grass blades in each frame, compute shaders optimize efficiency. The remaining visible blades are sent through a graphics pipeline with vertex, tessellation, and fragment shaders to transform, shape, and render them in detail. + +Below is the full pipeline: + +> 1. **CPU / Main Application** +> - Sets up camera and model matrices +> - Loads grass blade data +> - Issues draw calls (uses `vkCmdDrawIndirect`) +> +> 2. **Compute Shader** +> - Updates grass physics and performs culling +> - Writes updated blades and draw arguments for indirect draw +> - IN: camera buffer, blades (control points) buffer +> - OUT: updated blades buffer, culled blades buffer, draw command buffer +> +> 3. **Vertex Shader** +> - Transforms blade control points (`v0`, `v1`, `v2`, `up`) to world space +> - Passes data to tessellation stage +> - IN: blade control points, model matrix +> - OUT: transformed control points, glPosition +> +> 4. **Tessellation** +> - **Tessellation Control Shader (TCS):** sets tessellation level based on distance to camera +> - **Tessellation Evaluation Shader (TES):** +> - Generates vertices for each blade using quadratic Bezier interpolation +> - Computes blade width, orientation, and per-vertex attributes (normal, type, height) +> - IN: blade positions, camera +> - OUT: final vertex position, normal, height, blade type +> +> 5. **Fragment Shader** +> - Colors each pixel of the grass blade +> - Uses normal, height, and blade type to vary shading +> - Supports multiple blade styles (normal, spiky, bubble) +> - OUT: final pixel color +> +> 6. **Framebuffer** +> - Final shaded grass is drawn to the screen + +## Representing Grass as Bezier Curves + + + +Grass blades are represented as Bezier curves, defined by three control points ```(v0, v1, v2)```. Each point carries specific roles: ```v0``` anchors the blade on the ground, ```v1``` serves as a guiding point above ```v0```, and ```v2``` is used for physics-based transformations. Additional blade attributes include orientation, height, width, up vector, and stiffness, which are compactly stored across four ```vec4``` values. These attributes facilitate realistic grass movement and structural integrity in the simulation. + +During rendering, each blade is drawn as a 2D object positioned in 3D space. By evaluating the curve interpolation of the control points for each generated vertex, the quad becomes aligned to the Bezier curve, and this is achieved using De Casteljau's algorithm (Jahrmann & Wimmer). + +## Simulating Forces + +Simulating forces, namely gravity, recovery, and wind, involves updating the ```v2``` control point of each grass blade's Bezier curve. Total force is computed and applied as translation to ```v2```, ensuring blades remain stable and preserve length by correcting ```v1``` and ```v2``` positions. These forces are all applied in a compute shader. + +|Force|Details|Result| +|---|---|---| +| **No Forces** | Result of rendering 4,096 blades of grass with no additional forces. | | +| **Gravity**: computed with both environmental and front-facing components, acts downward on each blade. | Environment gravity represents the downward gravity of the whole scene, whereas front gravity simulates each blades' individual elasticity, causing the tips to bend. With only gravity as a force applied, the blades squah down towards the plane. | | +| **Recovery**: derived from Hooke's Law, counteract deformation, restoring blades to their initial position | Recovery counters the gravity force by factoring the blades' stiffness. This force brings structure back into the blades. | | +| Wind: calculated with custom heuristic functions, considers blade position and time to produce swaying effect. | The impact of this force depends on the winds' strength and direction, as well as the position of the blade. In my renderer, the wind blows across the x-direction in sequential gusts. The impact of the wind also depends on the alignment of the blade: straighter blades are more affected than blades closer to the ground. | | + +### State Validation + +In addition to physical forces, I implemented state validation conditions to ensure that blades of grass are confined to legal positions. I use the following conditions: ```v2``` must not be pushed underneath the ground, ```v1``` must be set according to ```v2```, and the length of the curve must be equal to the height of the grass blade. + +## Culling Blades + +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. Culling optimizes performance by removing non-contributing grass blades from the render pipeline. Three main culling techniques are employed in the compute shader: + +|Culling Type|Details|Result| +|---|---|---| +| **Orientation Culling**: Removes blades perpendicular to the view vector, as these would appear too thin and create artifacts. | The thinnest blade of grass in the GIF to the right is culled based on the camera's view. | | +| **Frustum Culling**: Discards blades entirely outside the camera’s view, based on the visibility of control points ```v0```, ```v2```, and midpoint ```m```. | Notice that blades on the edges of the screen space are culled. This threshold is adjustable, and indicates that unseen blades will not be rendered. | | +| **Distance Culling**: Blades far from the camera are culled to avoid rendering details that are indistinguishable at a distance. | As seen in the GIF, blades are culled based on their distance to the camera, which is defined by adjustable, discrete levels. | | + +### Performance Improvements + + + +I conducted a performance analysis on the improvement from culling techniques for rendering 2^14 blades of grass. In analyzing the performance of different culling techniques, we see that combining multiple culling methods significantly optimizes rendering speed, as reflected in FPS improvements. + +Comparing the effectiveness of each culling technique reveals distinct strengths and limitations. Frustum culling, at 530 FPS, specifically targets blades outside the camera’s view, helping reduce the load but missing potential optimizations related to distance. Orientation culling offers a moderate performance improvement, reaching 660 FPS by discarding blades not directly facing the camera. However, it doesn’t account for blades that fall outside the field of view or are too distant to be noticeable, so its impact is limited. Distance culling is notably more effective, achieving 1050 FPS by removing blades beyond a specified range, a factor that greatly minimizes the number of blades processed for rendering while maintaining visual fidelity. + +When used together, all three culling methods produce the best results, with an FPS of 1150, as each technique filters out a unique subset of blades, ultimately reducing the computational burden more effectively than any single technique alone. + +## Tessellating Bezier curves into grass blades + +Each Bezier curve passes into the grass graphics pipeline as a patch, then tessellated in the tessellation control shader. This step generates vertices that shape each blade’s quad geometry. The tessellation evaluation shader then positions these vertices in world space, adjusting them to match the blade’s width, height, and orientation. This process creates detailed, lifelike grass blades that reflect their underlying Bezier curves and attributes, producing a visually accurate and efficient rendering. + +### Distance-Based Level of Detail + +I modified the tessellation shader for rendering grass by introducing a dynamic level of detail based on the distance from the camera. By calculating the distance between each grass blade and the camera position, the shader adjusts the tessellation levels accordingly: blades closer than 15 units receive a high tessellation level of 20, those between 15 and 25 units receive a medium level of 6, and blades beyond 25 units are assigned a low level of 4. This distance-based approach enhances performance by reducing the complexity of rendering distant blades while maintaining visual fidelity for closer ones, optimizing the overall rendering process in a scene with varying levels of detail. + + + +The blade shown in left image has tesselation level of 4 because it is at the farthest distance level. After moving the camera closer towards the blade as seen in the right image, the blade is rendered at a higher tesselation level of 20, producing a smoother curve. + + + +The performance analysis of the tessellation shader reveals significant improvements when incorporating distance-based tessellation. For 8 blades, the rendering time with distance tessellation is 2230 milliseconds, only slightly better than the 2270 milliseconds required without distance tessellation. However, as the number of blades increases, the benefits become more pronounced. At 12 blades, the rendering time dramatically decreases from 990 milliseconds without distance tessellation to 2100 milliseconds with it, showcasing an efficient use of resources as complexity grows. The most striking difference occurs at 16 blades, where the time plummets to just 530 milliseconds with distance tessellation compared to 101 milliseconds without. In short, distance-based tessellation effectively optimizes performance, particularly as the scene's complexity increases. + +### Complex Blade Shapes + +Inspired by the paper's investigation into more complex blade shapes (the paper discusses creating a dandelion leaf, but does not specify the equation used for it), I experimented with different interpolation parameter equations that produced the following various blade types: + +|Basic Blade|Spiky Blade|Bubble Blade| +|---|---|---| +| | | | +| Triangular interpolation, follows equation given in paper. | Add symmetric sinusoidal displacement to create ridges, with smoothstep to lower spike amplitude towards tip. | Rounder displacement that forms "bubbles" around a blade. Also angled/tilted for aesthetic variation. | + +I combined the above blade types into one final scene. To do this, in the tessellation evaluation shader, I added a generic hash function that maps each blade's X and Y position to a float on the interval [0,1]. The value of this float determines which blade shape it is to take on, with basic blades being the most common and bubble blades being the rarest. The blade type is then passed to the fragment shader, in which each blade type is assigned different pairs of colors that are interpolated. + +It's also worthy to note that the displaced blade types require high tessellation levels for good results. A closeup GIF is provided below: + + + +### Overall Performance Analysis + + + +The above performance graph examines the overall renderer runtime for increasing numbers of blades. Note that all three blade types were present, and distance-based tessellation is off. + +As expected, FPS declines as the number of blades increases. Starting with 6 blades, the system achieves a robust FPS of 2300, and this slightly improves to 2320 FPS at 2^8 blades, indicating comparable discrepancy as well as efficient rendering at lower complexities. However, as the number of blades continues to rise, the FPS experiences a sharp drop. At 2^10 blades, the FPS decreases to 2260, suggesting a minor impact on performance. At 2^18 blades, the FPS falls to 34, indicating that the system is struggling to maintain performance under higher complexity in a dense scene. Nonetheless, 30 FPS is the frame rate widely used for media like TV and games, which still preserves the perception of real-time motion. To support this, I included a GIF of the scene with 2^18 blades, showing wind that is still relatively smooth and a rather satisfying sense of lush grass movement in the scene. + + + +### Bloopers (that Produced Cool Imagery) + +The following images are included just for fun. These were bloopers I encountered in my development process that resulted in some rather cool effects. + + +I encountered this result when trying to map different blade types to different color pairs between the tessellation evaluation shader and the grass fragment shader. I determined color from the hash function using discrete comparison instead of range buckets, producing the colored stripes on the blade. + + +I really like the output of this specific mix of bugs. The colored stripes from the above scenario combines with interpolating blade color based on normals instead of y-position. I might add this to a branch and explore it a bit on the technical art side (with additional shaders)? It reminds me of the 2D, pattern-like flora art style of [Henri Rousseau](https://henrirousseau.org/) -*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. diff --git a/bin/Release/vulkan_grass_rendering.exe b/bin/Release/vulkan_grass_rendering.exe index f68db3a..f311e58 100644 Binary files a/bin/Release/vulkan_grass_rendering.exe and b/bin/Release/vulkan_grass_rendering.exe differ diff --git a/img/biggrass.gif b/img/biggrass.gif new file mode 100644 index 0000000..4905f0c Binary files /dev/null and b/img/biggrass.gif differ diff --git a/img/blade1.png b/img/blade1.png new file mode 100644 index 0000000..96ce767 Binary files /dev/null and b/img/blade1.png differ diff --git a/img/blade2.png b/img/blade2.png new file mode 100644 index 0000000..ec7e040 Binary files /dev/null and b/img/blade2.png differ diff --git a/img/blade3.png b/img/blade3.png new file mode 100644 index 0000000..f6040bf Binary files /dev/null and b/img/blade3.png differ diff --git a/img/blooper.png b/img/blooper.png new file mode 100644 index 0000000..ecf5168 Binary files /dev/null and b/img/blooper.png differ diff --git a/img/blooper2.png b/img/blooper2.png new file mode 100644 index 0000000..cc88902 Binary files /dev/null and b/img/blooper2.png differ diff --git a/img/closeup.gif b/img/closeup.gif new file mode 100644 index 0000000..c04b349 Binary files /dev/null and b/img/closeup.gif differ diff --git a/img/cover.gif b/img/cover.gif new file mode 100644 index 0000000..8bd1d5f Binary files /dev/null and b/img/cover.gif differ diff --git a/img/cullfps.png b/img/cullfps.png new file mode 100644 index 0000000..6b7ca01 Binary files /dev/null and b/img/cullfps.png differ diff --git a/img/distculling.gif b/img/distculling.gif new file mode 100644 index 0000000..a17a7de Binary files /dev/null and b/img/distculling.gif differ diff --git a/img/disttest.png b/img/disttest.png new file mode 100644 index 0000000..834a5a0 Binary files /dev/null and b/img/disttest.png differ diff --git a/img/fps.png b/img/fps.png new file mode 100644 index 0000000..bd1392d Binary files /dev/null and b/img/fps.png differ diff --git a/img/frustculling.gif b/img/frustculling.gif new file mode 100644 index 0000000..092e9a1 Binary files /dev/null and b/img/frustculling.gif differ diff --git a/img/gravity.png b/img/gravity.png new file mode 100644 index 0000000..ce4e241 Binary files /dev/null and b/img/gravity.png differ diff --git a/img/noforces.png b/img/noforces.png new file mode 100644 index 0000000..c496f38 Binary files /dev/null and b/img/noforces.png differ diff --git a/img/orientculling.gif b/img/orientculling.gif new file mode 100644 index 0000000..61703af Binary files /dev/null and b/img/orientculling.gif differ diff --git a/img/recovery.png b/img/recovery.png new file mode 100644 index 0000000..04d7fe3 Binary files /dev/null and b/img/recovery.png differ diff --git a/img/tessfps.png b/img/tessfps.png new file mode 100644 index 0000000..03af823 Binary files /dev/null and b/img/tessfps.png differ diff --git a/img/wind.gif b/img/wind.gif new file mode 100644 index 0000000..49d0887 Binary files /dev/null and b/img/wind.gif differ diff --git a/src/Blades.cpp b/src/Blades.cpp index 80e3d76..0142372 100644 --- a/src/Blades.cpp +++ b/src/Blades.cpp @@ -45,7 +45,7 @@ Blades::Blades(Device* device, VkCommandPool commandPool, float planeDim) : Mode indirectDraw.firstInstance = 0; BufferUtils::CreateBufferFromData(device, commandPool, blades.data(), NUM_BLADES * sizeof(Blade), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, bladesBuffer, bladesBufferMemory); - BufferUtils::CreateBuffer(device, NUM_BLADES * sizeof(Blade), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, culledBladesBuffer, culledBladesBufferMemory); + BufferUtils::CreateBuffer(device, NUM_BLADES * sizeof(Blade), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, culledBladesBuffer, culledBladesBufferMemory); BufferUtils::CreateBufferFromData(device, commandPool, &indirectDraw, sizeof(BladeDrawIndirect), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT, numBladesBuffer, numBladesBufferMemory); } diff --git a/src/Blades.h b/src/Blades.h index 9bd1eed..f329d50 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 << 12; 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..c114217 100644 --- a/src/Renderer.cpp +++ b/src/Renderer.cpp @@ -195,9 +195,45 @@ void Renderer::CreateTimeDescriptorSetLayout() { } void Renderer::CreateComputeDescriptorSetLayout() { - // TODO: Create the descriptor set layout for the compute pipeline + // DONE: Create the descriptor set layout for the compute pipeline // Remember this is like a class definition stating why types of information // will be stored at each binding + + // input blades + VkDescriptorSetLayoutBinding grassBladesLayoutBinding = {}; + grassBladesLayoutBinding.binding = 0; + grassBladesLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + grassBladesLayoutBinding.descriptorCount = 1; + grassBladesLayoutBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + grassBladesLayoutBinding.pImmutableSamplers = nullptr; + + // removed blades + VkDescriptorSetLayoutBinding culledBladesLayoutBinding = {}; + culledBladesLayoutBinding.binding = 1; + culledBladesLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + culledBladesLayoutBinding.descriptorCount = 1; + culledBladesLayoutBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + culledBladesLayoutBinding.pImmutableSamplers = nullptr; + + VkDescriptorSetLayoutBinding numBladesLayoutBinding = {}; + numBladesLayoutBinding.binding = 2; + numBladesLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + numBladesLayoutBinding.descriptorCount = 1; + numBladesLayoutBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + numBladesLayoutBinding.pImmutableSamplers = nullptr; + + std::vector bindings = { grassBladesLayoutBinding, culledBladesLayoutBinding, numBladesLayoutBinding }; + + // Create the descriptor set layout + VkDescriptorSetLayoutCreateInfo layoutInfo = {}; + layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + layoutInfo.bindingCount = static_cast(bindings.size()); + layoutInfo.pBindings = bindings.data(); + + if (vkCreateDescriptorSetLayout(logicalDevice, &layoutInfo, nullptr, &computeDescriptorSetLayout) != VK_SUCCESS) { + throw std::runtime_error("Failed to create descriptor set layout"); + } + } void Renderer::CreateDescriptorPool() { @@ -215,7 +251,8 @@ void Renderer::CreateDescriptorPool() { // Time (compute) { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER , 1 }, - // TODO: Add any additional types and counts of descriptors you will need to allocate + // DONE: Add any additional types and counts of descriptors you will need to allocate (Grass compute) + { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER , static_cast(3 * scene->GetBlades().size()) } }; VkDescriptorPoolCreateInfo poolInfo = {}; @@ -230,7 +267,7 @@ void Renderer::CreateDescriptorPool() { } void Renderer::CreateCameraDescriptorSet() { - // Describe the desciptor set + // Describe the descriptor set VkDescriptorSetLayout layouts[] = { cameraDescriptorSetLayout }; VkDescriptorSetAllocateInfo allocInfo = {}; allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; @@ -267,7 +304,7 @@ void Renderer::CreateCameraDescriptorSet() { void Renderer::CreateModelDescriptorSets() { modelDescriptorSets.resize(scene->GetModels().size()); - // Describe the desciptor set + // Describe the descriptor set VkDescriptorSetLayout layouts[] = { modelDescriptorSetLayout }; VkDescriptorSetAllocateInfo allocInfo = {}; allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; @@ -318,12 +355,56 @@ void Renderer::CreateModelDescriptorSets() { } void Renderer::CreateGrassDescriptorSets() { - // TODO: Create Descriptor sets for the grass. + // DONE: Create Descriptor sets for the grass. // This should involve creating descriptor sets which point to the model matrix of each group of grass blades + grassDescriptorSets.resize(scene->GetBlades().size()); + + // Describe the descriptor set + VkDescriptorSetLayout layouts[] = { modelDescriptorSetLayout }; + VkDescriptorSetAllocateInfo allocInfo = {}; + allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + allocInfo.descriptorPool = descriptorPool; + allocInfo.descriptorSetCount = static_cast(grassDescriptorSets.size()); + allocInfo.pSetLayouts = layouts; + + // Allocate descriptor sets + 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) { + + // Configure the descriptors to refer to buffers + VkDescriptorBufferInfo modelBufferInfo = {}; + modelBufferInfo.buffer = scene->GetBlades()[i]->GetModelBuffer(); + modelBufferInfo.offset = 0; + modelBufferInfo.range = sizeof(ModelBufferObject); + + // Bind image and sampler resources to the descriptor + VkDescriptorImageInfo imageInfo = {}; + imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + imageInfo.imageView = scene->GetModels()[i]->GetTextureView(); + imageInfo.sampler = scene->GetModels()[i]->GetTextureSampler(); + + descriptorWrites[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + descriptorWrites[0].dstSet = grassDescriptorSets[i]; + descriptorWrites[0].dstBinding = 0; + descriptorWrites[0].dstArrayElement = 0; + descriptorWrites[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + descriptorWrites[0].descriptorCount = 1; + descriptorWrites[0].pBufferInfo = &modelBufferInfo; + descriptorWrites[0].pImageInfo = nullptr; + descriptorWrites[0].pTexelBufferView = nullptr; + } + + // Update descriptor sets + vkUpdateDescriptorSets(logicalDevice, static_cast(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr); } void Renderer::CreateTimeDescriptorSet() { - // Describe the desciptor set + // Describe the descriptor set VkDescriptorSetLayout layouts[] = { timeDescriptorSetLayout }; VkDescriptorSetAllocateInfo allocInfo = {}; allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; @@ -358,8 +439,85 @@ void Renderer::CreateTimeDescriptorSet() { } void Renderer::CreateComputeDescriptorSets() { - // TODO: Create Descriptor sets for the compute pipeline + // DONE: Create Descriptor sets for the compute pipeline // The descriptors should point to Storage buffers which will hold the grass blades, the culled grass blades, and the output number of grass blades + + computeDescriptorSets.resize(scene->GetModels().size()); + + // Describe the descriptor set + VkDescriptorSetLayout layouts[] = { computeDescriptorSetLayout }; + VkDescriptorSetAllocateInfo allocInfo = {}; + allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + allocInfo.descriptorPool = descriptorPool; + allocInfo.descriptorSetCount = static_cast(computeDescriptorSets.size()); + allocInfo.pSetLayouts = layouts; + + // Allocate descriptor sets + if (vkAllocateDescriptorSets(logicalDevice, &allocInfo, computeDescriptorSets.data()) != VK_SUCCESS) { + throw std::runtime_error("Failed to allocate descriptor set"); + } + + std::vector descriptorWrites(3 * computeDescriptorSets.size()); + + for (uint32_t i = 0; i < scene->GetBlades().size(); ++i) { + + // Blades buffer -------------------------------------- + VkDescriptorBufferInfo bladesBufferInfo = {}; + bladesBufferInfo.buffer = scene->GetBlades()[i]->GetBladesBuffer(); + bladesBufferInfo.offset = 0; + bladesBufferInfo.range = sizeof(Blade) * NUM_BLADES; + + // Bind image and sampler resources to the descriptor + int idx = 3 * i; + descriptorWrites[idx].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + descriptorWrites[idx].dstSet = computeDescriptorSets[i]; + descriptorWrites[idx].dstBinding = 0; + descriptorWrites[idx].dstArrayElement = 0; + descriptorWrites[idx].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + descriptorWrites[idx].descriptorCount = 1; + descriptorWrites[idx].pBufferInfo = &bladesBufferInfo; + descriptorWrites[idx].pImageInfo = nullptr; + descriptorWrites[idx].pTexelBufferView = nullptr; + + // Culled blades buffer ---------------------------------- + VkDescriptorBufferInfo culledBladesBufferInfo = {}; + culledBladesBufferInfo.buffer = scene->GetBlades()[i]->GetCulledBladesBuffer(); + culledBladesBufferInfo.offset = 0; + culledBladesBufferInfo.range = sizeof(Blade) * NUM_BLADES; + + // Bind image and sampler resources to the descriptor + idx++; + descriptorWrites[idx].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + descriptorWrites[idx].dstSet = computeDescriptorSets[i]; + descriptorWrites[idx].dstBinding = 1; + descriptorWrites[idx].dstArrayElement = 0; + descriptorWrites[idx].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + descriptorWrites[idx].descriptorCount = 1; + descriptorWrites[idx].pBufferInfo = &culledBladesBufferInfo; + descriptorWrites[idx].pImageInfo = nullptr; + descriptorWrites[idx].pTexelBufferView = nullptr; + + // Num blades buffer ---------------------------------- + VkDescriptorBufferInfo numBladesBufferInfo = {}; + numBladesBufferInfo.buffer = scene->GetBlades()[i]->GetNumBladesBuffer(); + numBladesBufferInfo.offset = 0; + numBladesBufferInfo.range = sizeof(BladeDrawIndirect); + + // Bind image and sampler resources to the descriptor + idx++; + descriptorWrites[idx].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + descriptorWrites[idx].dstSet = computeDescriptorSets[i]; + descriptorWrites[idx].dstBinding = 2; + descriptorWrites[idx].dstArrayElement = 0; + descriptorWrites[idx].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + descriptorWrites[idx].descriptorCount = 1; + descriptorWrites[idx].pBufferInfo = &numBladesBufferInfo; + descriptorWrites[idx].pImageInfo = nullptr; + descriptorWrites[idx].pTexelBufferView = nullptr; + } + + // Update descriptor sets + vkUpdateDescriptorSets(logicalDevice, static_cast(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr); } void Renderer::CreateGraphicsPipeline() { @@ -716,8 +874,13 @@ void Renderer::CreateComputePipeline() { computeShaderStageInfo.module = computeShaderModule; computeShaderStageInfo.pName = "main"; - // TODO: Add the compute dsecriptor set layout you create to this list - std::vector descriptorSetLayouts = { cameraDescriptorSetLayout, timeDescriptorSetLayout }; + // DONE: Add the compute dsecriptor set layout you create to this list + std::vector descriptorSetLayouts = + { + cameraDescriptorSetLayout, + timeDescriptorSetLayout, + computeDescriptorSetLayout + }; // Create pipeline layout VkPipelineLayoutCreateInfo pipelineLayoutInfo = {}; @@ -883,7 +1046,11 @@ void Renderer::RecordComputeCommandBuffer() { // Bind descriptor set for time uniforms vkCmdBindDescriptorSets(computeCommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, computePipelineLayout, 1, 1, &timeDescriptorSet, 0, nullptr); - // TODO: For each group of blades bind its descriptor set and dispatch + // DONE: For each group of blades bind its descriptor set and dispatch + for (uint32_t i = 0; i < scene->GetBlades().size(); ++i) { + vkCmdBindDescriptorSets(computeCommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, computePipelineLayout, 2, 1, &computeDescriptorSets[i], 0, nullptr); + vkCmdDispatch(computeCommandBuffer, (NUM_BLADES / WORKGROUP_SIZE), 1, 1); + } // ~ End recording ~ if (vkEndCommandBuffer(computeCommandBuffer) != VK_SUCCESS) { @@ -926,7 +1093,7 @@ void Renderer::RecordCommandBuffers() { renderPassInfo.renderArea.extent = swapChain->GetVkExtent(); std::array clearValues = {}; - clearValues[0].color = { 0.0f, 0.0f, 0.0f, 1.0f }; + clearValues[0].color = { 0.392f, 0.396f, 0.71f, 1.0f }; // background color clearValues[1].depthStencil = { 1.0f, 0 }; renderPassInfo.clearValueCount = static_cast(clearValues.size()); renderPassInfo.pClearValues = clearValues.data(); @@ -975,14 +1142,16 @@ void Renderer::RecordCommandBuffers() { for (uint32_t j = 0; j < scene->GetBlades().size(); ++j) { VkBuffer vertexBuffers[] = { scene->GetBlades()[j]->GetCulledBladesBuffer() }; VkDeviceSize offsets[] = { 0 }; - // TODO: Uncomment this when the buffers are populated - // vkCmdBindVertexBuffers(commandBuffers[i], 0, 1, vertexBuffers, offsets); - // TODO: Bind the descriptor set for each grass blades model + // DONE: Uncomment this when the buffers are populated + vkCmdBindVertexBuffers(commandBuffers[i], 0, 1, vertexBuffers, offsets); + + // DONE: Bind the descriptor set for each grass blades model + vkCmdBindDescriptorSets(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, grassPipelineLayout, 1, 1, &grassDescriptorSets[j], 0, nullptr); // Draw - // TODO: Uncomment this when the buffers are populated - // vkCmdDrawIndirect(commandBuffers[i], scene->GetBlades()[j]->GetNumBladesBuffer(), 0, 1, sizeof(BladeDrawIndirect)); + // DONE: Uncomment this when the buffers are populated + vkCmdDrawIndirect(commandBuffers[i], scene->GetBlades()[j]->GetNumBladesBuffer(), 0, 1, sizeof(BladeDrawIndirect)); } // End render pass @@ -1041,7 +1210,7 @@ void Renderer::Frame() { Renderer::~Renderer() { vkDeviceWaitIdle(logicalDevice); - // TODO: destroy any resources you created + // DONE: destroy any resources you created vkFreeCommandBuffers(logicalDevice, graphicsCommandPool, static_cast(commandBuffers.size()), commandBuffers.data()); vkFreeCommandBuffers(logicalDevice, computeCommandPool, 1, &computeCommandBuffer); @@ -1050,6 +1219,7 @@ Renderer::~Renderer() { vkDestroyPipeline(logicalDevice, grassPipeline, nullptr); vkDestroyPipeline(logicalDevice, computePipeline, nullptr); + vkDestroyPipelineLayout(logicalDevice, graphicsPipelineLayout, nullptr); vkDestroyPipelineLayout(logicalDevice, graphicsPipelineLayout, nullptr); vkDestroyPipelineLayout(logicalDevice, grassPipelineLayout, nullptr); vkDestroyPipelineLayout(logicalDevice, computePipelineLayout, nullptr); @@ -1057,6 +1227,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..044e899 100644 --- a/src/Renderer.h +++ b/src/Renderer.h @@ -56,11 +56,14 @@ class Renderer { VkDescriptorSetLayout cameraDescriptorSetLayout; VkDescriptorSetLayout modelDescriptorSetLayout; VkDescriptorSetLayout timeDescriptorSetLayout; + VkDescriptorSetLayout computeDescriptorSetLayout; VkDescriptorPool descriptorPool; VkDescriptorSet cameraDescriptorSet; std::vector modelDescriptorSets; + std::vector grassDescriptorSets; + std::vector computeDescriptorSets; VkDescriptorSet timeDescriptorSet; VkPipelineLayout graphicsPipelineLayout; diff --git a/src/SwapChain.cpp b/src/SwapChain.cpp index 711fec0..6ab6ed0 100644 --- a/src/SwapChain.cpp +++ b/src/SwapChain.cpp @@ -74,14 +74,17 @@ SwapChain::SwapChain(Device* device, VkSurfaceKHR vkSurface, unsigned int numBuf } } -void SwapChain::Create() { +void SwapChain::Create(int w, int h) { auto* instance = device->GetInstance(); const auto& surfaceCapabilities = instance->GetSurfaceCapabilities(); VkSurfaceFormatKHR surfaceFormat = chooseSwapSurfaceFormat(instance->GetSurfaceFormats()); VkPresentModeKHR presentMode = chooseSwapPresentMode(instance->GetPresentModes()); - VkExtent2D extent = chooseSwapExtent(surfaceCapabilities, GetGLFWWindow()); + VkExtent2D extent{ w, h }; + if (w == 0 || h == 0) { + extent = chooseSwapExtent(surfaceCapabilities, GetGLFWWindow()); + } uint32_t imageCount = surfaceCapabilities.minImageCount + 1; imageCount = numBuffers > imageCount ? numBuffers : imageCount; @@ -188,9 +191,9 @@ VkSemaphore SwapChain::GetRenderFinishedVkSemaphore() const { return renderFinishedSemaphore; } -void SwapChain::Recreate() { +void SwapChain::Recreate(int w, int h) { Destroy(); - Create(); + Create(w, h); } bool SwapChain::Acquire() { diff --git a/src/SwapChain.h b/src/SwapChain.h index dbafcf0..318b41b 100644 --- a/src/SwapChain.h +++ b/src/SwapChain.h @@ -17,14 +17,14 @@ class SwapChain { VkSemaphore GetImageAvailableVkSemaphore() const; VkSemaphore GetRenderFinishedVkSemaphore() const; - void Recreate(); + void Recreate(int w = 0, int h = 0); bool Acquire(); bool Present(); ~SwapChain(); private: SwapChain(Device* device, VkSurfaceKHR vkSurface, unsigned int numBuffers); - void Create(); + void Create(int w = 0, int h = 0); void Destroy(); Device* device; diff --git a/src/main.cpp b/src/main.cpp index 8bf822b..88b04d6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,6 +5,8 @@ #include "Camera.h" #include "Scene.h" #include "Image.h" +#include +#include Device* device; SwapChain* swapChain; @@ -16,7 +18,7 @@ namespace { if (width == 0 || height == 0) return; vkDeviceWaitIdle(device->GetVkDevice()); - swapChain->Recreate(); + swapChain->Recreate(width, height); renderer->RecreateFrameResources(); } @@ -143,10 +145,27 @@ int main() { glfwSetMouseButtonCallback(GetGLFWWindow(), mouseDownCallback); glfwSetCursorPosCallback(GetGLFWWindow(), mouseMoveCallback); + // Track program FPS for performance analysis + auto lastTime = std::chrono::high_resolution_clock::now(); + int frameCount = 0; + while (!ShouldQuit()) { glfwPollEvents(); scene->UpdateTime(); renderer->Frame(); + + frameCount++; + + // Calculate FPS every second + auto currentTime = std::chrono::high_resolution_clock::now(); + auto elapsed = std::chrono::duration_cast(currentTime - lastTime).count(); + + // print FPS + if (elapsed >= 1) { + //std::cout << "FPS: " << frameCount << std::endl; + frameCount = 0; + lastTime = currentTime; + } } vkDeviceWaitIdle(device->GetVkDevice()); diff --git a/src/shaders/compute.comp b/src/shaders/compute.comp index 0fd0224..2a960b1 100644 --- a/src/shaders/compute.comp +++ b/src/shaders/compute.comp @@ -1,6 +1,14 @@ #version 450 #extension GL_ARB_separate_shader_objects : enable +#define WIND 1 +#define GRAVITY 1 +#define RECOVERY 1 +#define ORIENT_CULL 1 +#define FRUSTUM_CULL 1 +#define DIST_CULL 1 + + #define WORKGROUP_SIZE 32 layout(local_size_x = WORKGROUP_SIZE, local_size_y = 1, local_size_z = 1) in; @@ -21,36 +29,195 @@ struct Blade { vec4 up; }; -// TODO: Add bindings to: +// DONE: Add bindings: + // 1. Store the input blades +layout(set = 2, binding = 0) buffer InputBlades { + Blade inputBlades[]; +}; + // 2. Write out the culled blades -// 3. Write the total number of blades remaining +layout(set = 2, binding = 1) buffer CulledBlades { + Blade culledBlades[]; +}; +// 3. Write the total number of blades remaining // The project is using vkCmdDrawIndirect to use a buffer as the arguments for a draw call // This is sort of an advanced feature so we've showed you what this buffer should look like -// -// layout(set = ???, binding = ???) buffer NumBlades { -// uint vertexCount; // Write the number of blades remaining here -// uint instanceCount; // = 1 -// uint firstVertex; // = 0 -// uint firstInstance; // = 0 -// } numBlades; + +layout(set = 2, binding = 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); } +bool inViewFrustum(vec3 p) { + vec4 ndcP = camera.proj * camera.view * vec4(p, 1.0); + float tolerance = -0.05; // -0.3 to show clearly in x and y + float h = ndcP.w + tolerance; + return inBounds(ndcP.x, h) && inBounds(ndcP.y, h) && inBounds(ndcP.z, h); +} + +vec3 calcWindInfluence(vec3 v0, float height) { + + float windPower = 5.0; + + // Wind direction oscillation through plane + + float turbulence = 0.1 * cos(v0.z * 0.2 + totalTime * 1.5); // add some noise to make less uniform + float sway = sin(0.5 * v0.x + totalTime) * height * 0.2; // height-dependent oscillation based on position + + return windPower * vec3(sway, 0.0, 0.0); + +} + void main() { // Reset the number of blades to 0 if (gl_GlobalInvocationID.x == 0) { - // numBlades.vertexCount = 0; + numBlades.vertexCount = 0; } barrier(); // Wait till all threads reach this point - // TODO: Apply forces on every blade and update the vertices in the buffer + // get blade info + Blade blade = inputBlades[gl_GlobalInvocationID.x]; + vec3 v0 = blade.v0.xyz; + vec3 v1 = blade.v1.xyz; + vec3 v2 = blade.v2.xyz; + vec3 up = blade.up.xyz; + + float direction = blade.v0.w; + float height = blade.v1.w; + float width = blade.v2.w; + float stiffness = blade.up.w * 0.8; + + // DONE: Apply forces on every blade and update the vertices in the buffer + + // Gravity ----------------------------------------------------- + + float g = 19.8; // gravitational acceleration + vec4 d = vec4(0.f,-1.f,0.f,g); + + float mass = 1.f; // mass of blade + float t = 0.5; + + vec3 environmentGravity = mass * ( (normalize(d.xyz) * d.w * (1-t)) + (t) ); + + vec3 widthDir = vec3(cos(direction), 0.0, -sin(direction)); // (first col of rotation matrix) + vec3 f = cross(widthDir, up); + vec3 frontGravity = 0.25 * length(environmentGravity) * f; + +#if GRAVITY + vec3 gravity = environmentGravity + frontGravity; +#else + vec3 gravity = vec3(0.f); +#endif + + // Recovery ----------------------------------------------------- + + vec3 iv2 = v0 + height * up; // initial pose of blade + +#if RECOVERY + vec3 recovery = (iv2 - v2) * stiffness; // Hooke's law +#else + vec3 recovery = vec3(0.f); +#endif + + // Wind ----------------------------------------------------- - // TODO: Cull blades that are too far away or not in the camera frustum and write them - // to the culled blades buffer + vec3 wi = calcWindInfluence(v0, height); // wind influence, calculated by analytic wind function + + float fd = 1 - abs(dot( normalize(wi), normalize(v2 - v0))); // directional alignment (towards wind influence w_i) + float fr = dot(v2-v0, up) / height; // height ratio (straightness of blade) + + float alignment = fd * fr; // (theta) + +#if WIND + vec3 wind = wi * alignment; +#else + vec3 wind = vec3(0.f); +#endif + + // Total Forces ----------------------------------------------------- + + vec3 force = (gravity + recovery + wind) * deltaTime; + v2 += force; + + // State validation (update v_1 and correct positions) -------------- + + // v2 must not be pushed beneath the ground + v2 -= up * min(dot(up, v2 - v0), 0); + + // position of v1 set according to position of v2 + float lProj = length(v2 - v0 - up * dot(v2 - v0, up)); + v1 = v0 + height * up * max(1 - (lProj/height), 0.05 * max(lProj / height, 1)); + + // length of curve must be equal to height of blade grass + + float l0 = distance(v0, v2); // sum of distances between first and last control point + float l1 = distance(v0, v1) + distance(v1, v2); // sum of all distances between a control point and subsequent one + + float n = 2.f; // degree + float l = (2 * l0 + (n - 1) * l1) / (n + 1); // length of a Bezier curve + + float r = height / l; // ratio between height of blade and measured length + + vec3 v1old = v1; + v1 = v0 + r * (v1old - v0); + v2 = v1 + r * (v2 - v1old); + + blade.v1.xyz = v1; + blade.v2.xyz = v2; + inputBlades[gl_GlobalInvocationID.x] = blade; + + // Culling Blades -------------------------------------------------- + + // 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 + + // Orientation culling -------------------------------- + + vec3 cameraDir = normalize(-inverse(camera.view)[2].xyz); + if (abs(dot(cameraDir, widthDir)) > 0.9) { +#if ORIENT_CULL + return; // keep blade +#else +#endif + } + + // View frustum culling --------------------------------- + + vec3 m = (0.25 * v0) + (0.5 * v1) + (0.25 * v2); + if (!inViewFrustum(v0) && !inViewFrustum(v2) && !inViewFrustum(m)) { +#if FRUSTUM_CULL + return; // keep blade +#else +#endif + } + + // Distance culling ------------------------------------- + + float dMax = 30.f; + + vec3 v0Minusc = v0 - inverse(camera.view)[3].xyz; // v0 - c + float dProj = length(v0Minusc - up * dot(v0Minusc, up)); + + int distLevel = 20; + + if (dProj > dMax || gl_GlobalInvocationID.x % distLevel > floor(distLevel * (1.0 - (dProj / dMax)))) { +#if DIST_CULL + return; // keep blade +#else +#endif + } + + // Update buffer + culledBlades[atomicAdd(numBlades.vertexCount, 1)] = inputBlades[gl_GlobalInvocationID.x]; + + } diff --git a/src/shaders/grass.frag b/src/shaders/grass.frag index c7df157..b38a4bc 100644 --- a/src/shaders/grass.frag +++ b/src/shaders/grass.frag @@ -1,17 +1,51 @@ #version 450 #extension GL_ARB_separate_shader_objects : enable +#define EXTRA_BLADES 1 + layout(set = 0, binding = 0) uniform CameraBufferObject { mat4 view; mat4 proj; } camera; -// TODO: Declare fragment shader inputs +// DONE: Declare fragment shader inputs +layout(location = 0) in vec3 fsNor; +layout(location = 1) in float fsPosY; +layout(location = 2) in float fsType; layout(location = 0) out vec4 outColor; void main() { - // TODO: Compute fragment color + // DONE: Compute fragment color + + vec3 light = vec3(0.0, 1.0, 0.0); + float diffuseTerm = dot(normalize(fsNor), normalize(light)); + + vec3 darkGreen; + vec3 lightGreen; + +#if EXTRA_BLADES + if (fsType < 0.7f) { // normal grass +#endif + darkGreen = vec3(0.184f, 0.329f, 0.027f); + lightGreen = vec3(0.431f, 0.62f, 0.2f); + +#if EXTRA_BLADES + } else if (fsType < 0.95) { // spiky grass (cool tone) + + darkGreen = vec3(0.204f, 0.192f, 0.212f); + lightGreen = vec3(0.369f, 0.49f, 0.345f); + + } else { // bubble grass (warm tone) + + darkGreen = vec3(0.376f, 0.588f, 0.361f); + lightGreen = vec3(0.51f, 0.529f, 0.318f); + + } +#endif + + vec3 green = mix(darkGreen, lightGreen, fsPosY); + + outColor = vec4(green * (1.f + diffuseTerm),1.0); - outColor = vec4(1.0); } diff --git a/src/shaders/grass.tesc b/src/shaders/grass.tesc index f9ffd07..cd261ca 100644 --- a/src/shaders/grass.tesc +++ b/src/shaders/grass.tesc @@ -10,17 +10,52 @@ layout(set = 0, binding = 0) uniform CameraBufferObject { // TODO: Declare tessellation control 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; + void main() { // Don't move the origin location of the patch gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position; - // TODO: Write any shader outputs + // DONE: Write any shader outputs + outV0[gl_InvocationID] = inV0[gl_InvocationID]; + outV1[gl_InvocationID] = inV1[gl_InvocationID]; + outV2[gl_InvocationID] = inV2[gl_InvocationID]; + outUp[gl_InvocationID] = inUp[gl_InvocationID]; + + // Calculate distance to grass blade + + vec3 bladePos = vec3(gl_in[gl_InvocationID].gl_Position); + vec3 cameraPos = vec3(inverse(camera.view)[3]); + float dist = length(bladePos - cameraPos); + + // Tessellate to varying levels of detail as a function of how far the grass blade is from the camera + + float tessLevel; + if (dist < 15.0) { + tessLevel = 20.0; + } else if (dist < 25.0) { + tessLevel = 6.0; + } else { + tessLevel = 4.0; + } + + // Low/varying tesselations doesn't work for the dandelion leaves, so use this instead + tessLevel = 30.0; + + // Set tessellation level + gl_TessLevelInner[0] = tessLevel; + gl_TessLevelInner[1] = tessLevel; + gl_TessLevelOuter[0] = tessLevel; + gl_TessLevelOuter[1] = tessLevel; + gl_TessLevelOuter[2] = tessLevel; + gl_TessLevelOuter[3] = tessLevel; - // TODO: Set level of tesselation - // gl_TessLevelInner[0] = ??? - // gl_TessLevelInner[1] = ??? - // gl_TessLevelOuter[0] = ??? - // gl_TessLevelOuter[1] = ??? - // gl_TessLevelOuter[2] = ??? - // gl_TessLevelOuter[3] = ??? } diff --git a/src/shaders/grass.tese b/src/shaders/grass.tese index 751fff6..dcf994e 100644 --- a/src/shaders/grass.tese +++ b/src/shaders/grass.tese @@ -1,6 +1,8 @@ #version 450 #extension GL_ARB_separate_shader_objects : enable +#define EXTRA_BLADES 1 + layout(quads, equal_spacing, ccw) in; layout(set = 0, binding = 0) uniform CameraBufferObject { @@ -8,11 +10,100 @@ layout(set = 0, binding = 0) uniform CameraBufferObject { mat4 proj; } camera; -// TODO: Declare tessellation evaluation shader inputs and outputs +// DONE: Declare tessellation evaluation shader inputs and outputs +layout(location = 0) in vec4 inV0[]; +layout(location = 1) in vec4 inV1[]; +layout(location = 2) in vec4 inV2[]; +layout(location = 3) in vec4 inUp[]; + +layout(location = 0) out vec3 fsNor; +layout(location = 1) out float fsPosY; +layout(location = 2) out float fsType; + +float hash(float x, float z) { + return fract(sin(dot(vec2(x, z), vec2(12.9898, 78.233))) * 43758.5453); +} void main() { float u = gl_TessCoord.x; float v = gl_TessCoord.y; - // TODO: Use u and v to parameterize along the grass blade and output positions for each vertex of the grass blade + // DONE: Use u and v to parameterize along the grass blade and output positions for each vertex of the grass blade + + vec3 v0 = inV0[0].xyz; + vec3 v1 = inV1[0].xyz; + vec3 v2 = inV2[0].xyz; + + float direction = inV0[0].w; + float width = inV2[0].w; + + // bitangent: direction vector along width of the blade (first col of rotation matrix) + vec3 t1 = vec3(cos(direction), 0.0, -sin(direction)); + + // De Casteljau's Algorithm + + vec3 a = v0 + v * (v1 - v0); + vec3 b = v1 + v * (v2 - v1); + vec3 c = a + v * (b - a); + + vec3 c0 = c - width * t1; + vec3 c1 = c + width * t1; + + vec3 t0 = normalize(b - a); + + // ------------------------------------------------------------------- + + // Generate a random float based on the blade’s position + float bladeHash = hash(v0.x, v0.y); // Change ID for unique pattern per blade + + // Choose shape style based on the hash + float t; // Interpolation parameter + vec3 pos; + + fsType = bladeHash; + +#if EXTRA_BLADES + if (bladeHash < 0.70) { +#endif + // normal grass + + float threshold = 0.0; + t = 0.5 + (u - 0.5) * (1 - (max(v - threshold, 0) / (1 - threshold))); + pos = (1 - t) * c0 + t * c1; +#if EXTRA_BLADES + } else if (bladeHash < 0.95) { + + // spiky grass + + float threshold = 0.0; + t = 0.5 + (u - 0.5) * (1 - (max(v - threshold, 0) / (1 - threshold))); + + // enhance shape of grass, inspired by paper's section on dandelion leaves + float spikes = 0.5 * abs(fract(50.f * v)) * smoothstep(0.9, 0.1, v); + + if (u > 0) { + t += spikes; + } else { + t -= spikes; + } + + pos = (1 - t) * c0 + t * c1; + + } else { + + // bubble grass + + float leafFrequency = 25.0; + float leafAmplitude = 0.15; + t = 0.5 + (u - 0.5) * (1 - (max(v - 0.5, 0) / 0.5)); + float offset = sin(leafFrequency * v) * leafAmplitude * (u < 0.5 ? -1.0 : 1.0) + v; + pos = (1 - t) * (c0 + offset * t1) + t * (c1 + offset * t1); + + } +#endif + fsNor = normalize(cross(t0, t1)); + fsPosY = pos.y; + + gl_Position = camera.proj * camera.view * vec4(pos, 1.0); + } diff --git a/src/shaders/grass.vert b/src/shaders/grass.vert index db9dfe9..ec61e5a 100644 --- a/src/shaders/grass.vert +++ b/src/shaders/grass.vert @@ -6,12 +6,29 @@ layout(set = 1, binding = 0) uniform ModelBufferObject { mat4 model; }; -// TODO: Declare vertex shader inputs and outputs +// DONE: Declare vertex shader inputs and outputs +layout(location = 0) in vec4 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; }; void main() { - // TODO: Write gl_Position and any other shader outputs + // DONE: Write gl_Position and any other shader outputs + + outV0 = model * inV0; + outV1 = model * inV1; + outV2 = model * inV2; + outUp = model * inUp; + + gl_Position = outV0; }