Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Project 5 - Catherine Cheng #20

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 113 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,118 @@ 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)
* Catherine Cheng
* [LinkedIn](https://www.linkedin.com/in/catherine-wanning-cheng/), [personal website](https://www.catherine-wanning-cheng.com/projects-1)
* Tested on: Windows 11, AMD Ryzen 7 5800H @ 3.20GHz 16GB, NVIDIA GeForce RTX 3070

### (TODO: Your README)
## Introduction

*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.
This project is a Vulkan-based grass simulator and renderer that uses compute shaders for efficient physics calculations on Bezier curves to represent individual grass blades. To optimize rendering, I have used compute shaders to cull unnecessary blades per frame, passing only relevant blades to the graphics pipeline. In the graphics pipeline, I used custom shaders to handle each stage: a vertex shader transforms Bezier control points, tessellation shaders dynamically generate grass geometry, and a fragment shader shades the blades for final visual effects.

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)

![](img/result-grass.gif)

### Features
- Vulkan setup for grass and compute pipeline
- Tessellating Bezier curves into grass blades
- Force Simulation
- Gravity
- Recovery
- Wind
- Validation for total forces
- Culling
- Orientation Culling
- Frustum Culling
- Distance Culling


## Representing Grass Using Bezier Curves

In this project, each grass blade is modeled as a Bezier curve to enable realistic physics calculations and efficient culling.

Each Bezier curve is defined by three control points:

- `v0`: The base position of the grass blade on the geometry.
- `v1`: A guide point positioned "above" v0 in alignment with the blade's up vector (explained below).
- `v2`: A control point influenced by simulated forces.
Additionally, each blade has specific attributes that aid in accurate simulation and tessellation:
- `up`: The blade's up vector, representing the normal of the underlying geometry at v0.
Orientation: Defines the blade's facing direction.
- `Height`: The blade's length.
- `Width`: The thickness of the blade's face.
- `Stiffness`: Influences the force interactions acting on the blade.

These characteristics are efficiently packed into four `vec4` structures, where `v0.w` stores orientation, `v1.w` stores height, `v2.w` stores width, and `up.w` holds the stiffness coefficient.

![](img/blade_model.jpg)

Below is the view when basic grass blades' shapes are set up without any further simulation.

![](img/no_force.png)

## Simulating Forces on Grass Blades
This project then simulates forces on grass blades, which are calculated within a compute shader in the provided compute pipeline. Since `v2` acts as the physical guide, transformations are applied to it, followed by corrections to maintain realism. Finally, `v1` is adjusted to preserve the intended blade length.

### Gravity Simulation
Gravity is defined using a direction vector, `D.xyz`, and an acceleration magnitude, `D.w`, allowing us to compute environmental gravity, `gE = normalize(D.xyz) * D.w`. The gravity's impact relative to the blade’s forward direction, `f`, results in "front gravity," calculated as `gF = (1/4) * ||gE|| * f`. The total gravity affecting the blade is `g = gE + gF`.

### Recovery Forces
Recovery forces counterbalance gravity, restoring the blade to equilibrium using Hooke's law. This is done by comparing the current `v2` position to its initial state, `iv2`. Initially, `v1` and `v2` are set a distance equal to the blade's height along the up vector. The recovery force is then computed as `r = (iv2 - v2) * stiffness`.

Below is the view when only gravity and recovery forces are applied:

![](img/fgr.png)

### Wind Simulation
Wind function is a sine-based function that incorporates `v0` (the blade’s base position) and a time variable. The wind force also depends on "wind alignment," where blades more aligned with the wind direction experience a stronger effect. The total wind force, w, is given by `windDirection * windAlignment`.

### Total Force Calculation
Finally, the translation for v2 is determined by combining forces: `F = (gravity + recovery + wind) * deltaTime`. In addition, there are extra adjustments to ensure that v2 remains above ground and that both `v1` and `v2` maintain the blade's intended length according to section 5.2 (State Validation) of the reference paper.

Below is the view with all forces applied:

![](img/wind-no-cull.gif)

## Culling Tests

To optimize rendering, while forces are simulated on every grass blade each frame, but we only render those that visibly contribute. Below are heuristics used to cull unnecessary blades.

### Orientation Culling

When a grass blade’s front face is perpendicular to the view vector, it appears smaller than a pixel, potentially causing aliasing. To prevent this, we cull these blades by performing a dot product test between the view vector and the blade’s front face direction.

![](img/orientation.gif)

### Frustum Culling

Blades outside the view frustum are also culled, as they won’t be visible. To determine visibility, we check three points: `v0`, `v2`, and `m`, where `m = (1/4)v0 + (1/2)v1 + (1/4)v2`. Since `v1` only serves as a guide, `m` provides a more accurate midpoint for visibility checks.

Culling is applied if all three points are outside the frustum, with a tolerance value to allow conservative culling, ensuring blades near the frustum edges are retained for a more complete visual.

![](img/frustum.gif)

### Distance Culling

Grass blades far from the camera may appear smaller than a pixel, risking rendering artifacts. To manage this, we cull blades based on their distance from the camera.

The closest bucket retains more blades, with each subsequent bucket culling an increasing number.

![](img/distance.gif)

## Performance Analysis

### Performance Change By Blade Number
With different number of blades, the performance is shown as below (with orientation, frustum, and distance culling):

![](img/performance.png)

In this way, we could see that FPS decreases as the blade number increases. When the number of Blades is greater han $2^{16}$, the runtime (FPS) starts to drop dramatically.

### Performance Change By Culling

In this project, I have applied three types of culling methods (Orientation Culling, Frustum Culling, and Distance Culling) to optimize the performance as shown below:

![](img/culling.png)

Based on the chart, any culling method would result in a faster runtime than when no culling is applied. The amount of runtime optimized would also vary based on the type of culling method and the circumstances (e.g., for distance culling, the FPS would be higher when the camera being far away from the plane). When all three types of culling methods are applied, the render would reach a even higher FPS.
Binary file added img/culling.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/distance.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/fgr.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/frustum.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/no_force.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/orientation.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/performance.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/result-grass.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/wind-no-cull.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 6 additions & 3 deletions src/Blades.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,12 @@ Blades::Blades(Device* device, VkCommandPool commandPool, float planeDim) : Mode
indirectDraw.firstVertex = 0;
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::CreateBufferFromData(device, commandPool, &indirectDraw, sizeof(BladeDrawIndirect), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT, numBladesBuffer, numBladesBufferMemory);
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_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);
}

VkBuffer Blades::GetBladesBuffer() const {
Expand Down
2 changes: 1 addition & 1 deletion src/Blades.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#include <array>
#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;
Expand Down
144 changes: 141 additions & 3 deletions src/Renderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,37 @@ 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
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;

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<VkDescriptorSetLayoutBinding> bindings = { grassBladesLayoutBinding, culledBladesLayoutBinding, numBladesLayoutBinding };

// Create the descriptor set layout for the compute pipeline
VkDescriptorSetLayoutCreateInfo layoutInfo = {};
layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
layoutInfo.bindingCount = static_cast<uint32_t>(bindings.size());
layoutInfo.pBindings = bindings.data();
if (vkCreateDescriptorSetLayout(logicalDevice, &layoutInfo, nullptr, &computeDescriptorSetLayout) != VK_SUCCESS) {
throw std::runtime_error("Failed to create compute descriptor set layout");
}
}

void Renderer::CreateDescriptorPool() {
Expand All @@ -216,6 +247,7 @@ void Renderer::CreateDescriptorPool() {
{ VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER , 1 },

// TODO: Add any additional types and counts of descriptors you will need to allocate
{ VK_DESCRIPTOR_TYPE_STORAGE_BUFFER , static_cast<uint32_t>(3 * scene->GetBlades().size()) },
};

VkDescriptorPoolCreateInfo poolInfo = {};
Expand Down Expand Up @@ -320,6 +352,41 @@ void Renderer::CreateModelDescriptorSets() {
void Renderer::CreateGrassDescriptorSets() {
// TODO: Create Descriptor sets for the grass.
// This should involve creating descriptor sets which point to the model matrix of each group of grass blades
grassDescriptorSets.resize(scene->GetBlades().size());

// Describe the desciptor set
VkDescriptorSetLayout layouts[] = { modelDescriptorSetLayout };
VkDescriptorSetAllocateInfo allocInfo = {};
allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
allocInfo.descriptorPool = descriptorPool;
allocInfo.descriptorSetCount = static_cast<uint32_t>(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<VkWriteDescriptorSet> descriptorWrites(grassDescriptorSets.size());

for (uint32_t i = 0; i < scene->GetBlades().size(); ++i) {
VkDescriptorBufferInfo modelBufferInfo = {};
modelBufferInfo.buffer = scene->GetBlades()[i]->GetModelBuffer();
modelBufferInfo.offset = 0;
modelBufferInfo.range = sizeof(ModelBufferObject);

descriptorWrites[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrites[i].dstSet = grassDescriptorSets[i];
descriptorWrites[i].dstBinding = 0;
descriptorWrites[i].dstArrayElement = 0;
descriptorWrites[i].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
descriptorWrites[i].descriptorCount = 1;
descriptorWrites[i].pBufferInfo = &modelBufferInfo;
descriptorWrites[i].pImageInfo = nullptr;
descriptorWrites[i].pTexelBufferView = nullptr;
}

vkUpdateDescriptorSets(logicalDevice, static_cast<uint32_t>(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr);
}

void Renderer::CreateTimeDescriptorSet() {
Expand Down Expand Up @@ -360,6 +427,71 @@ void Renderer::CreateTimeDescriptorSet() {
void Renderer::CreateComputeDescriptorSets() {
// TODO: Create Descriptor sets for the compute pipeline
// The descriptors should point to Storage buffers which will hold the grass blades, the culled grass blades, and the output number of grass blades
computeDescriptorSets.resize(scene->GetBlades().size());

// 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<uint32_t>(computeDescriptorSets.size());
allocInfo.pSetLayouts = layouts;

// Allocate descriptor sets
if (vkAllocateDescriptorSets(logicalDevice, &allocInfo, computeDescriptorSets.data()) != VK_SUCCESS) {
throw std::runtime_error("Failed to allocate compute descriptor set");
}

std::vector<VkWriteDescriptorSet> descriptorWrites(computeDescriptorSets.size() * 3);

for (uint32_t i = 0; i < scene->GetBlades().size(); ++i) {
VkDescriptorBufferInfo bladesBufferInfo = {};
bladesBufferInfo.buffer = scene->GetBlades()[i]->GetBladesBuffer();
bladesBufferInfo.offset = 0;
bladesBufferInfo.range = NUM_BLADES * sizeof(Blade);

VkDescriptorBufferInfo culledBladesBufferInfo = {};
culledBladesBufferInfo.buffer = scene->GetBlades()[i]->GetCulledBladesBuffer();
culledBladesBufferInfo.offset = 0;
culledBladesBufferInfo.range = NUM_BLADES * sizeof(Blade);

VkDescriptorBufferInfo numBladesBufferInfo = {};
numBladesBufferInfo.buffer = scene->GetBlades()[i]->GetNumBladesBuffer();
numBladesBufferInfo.offset = 0;
numBladesBufferInfo.range = sizeof(BladeDrawIndirect);

descriptorWrites[3 * i + 0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrites[3 * i + 0].dstSet = computeDescriptorSets[i];
descriptorWrites[3 * i + 0].dstBinding = 0;
descriptorWrites[3 * i + 0].dstArrayElement = 0;
descriptorWrites[3 * i + 0].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
descriptorWrites[3 * i + 0].descriptorCount = 1;
descriptorWrites[3 * i + 0].pBufferInfo = &bladesBufferInfo;
descriptorWrites[3 * i + 0].pImageInfo = nullptr;
descriptorWrites[3 * i + 0].pTexelBufferView = nullptr;

descriptorWrites[3 * i + 1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrites[3 * i + 1].dstSet = computeDescriptorSets[i];
descriptorWrites[3 * i + 1].dstBinding = 1;
descriptorWrites[3 * i + 1].dstArrayElement = 0;
descriptorWrites[3 * i + 1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
descriptorWrites[3 * i + 1].descriptorCount = 1;
descriptorWrites[3 * i + 1].pBufferInfo = &culledBladesBufferInfo;
descriptorWrites[3 * i + 1].pImageInfo = nullptr;
descriptorWrites[3 * i + 1].pTexelBufferView = nullptr;

descriptorWrites[3 * i + 2].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrites[3 * i + 2].dstSet = computeDescriptorSets[i];
descriptorWrites[3 * i + 2].dstBinding = 2;
descriptorWrites[3 * i + 2].dstArrayElement = 0;
descriptorWrites[3 * i + 2].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
descriptorWrites[3 * i + 2].descriptorCount = 1;
descriptorWrites[3 * i + 2].pBufferInfo = &numBladesBufferInfo;
descriptorWrites[3 * i + 2].pImageInfo = nullptr;
descriptorWrites[3 * i + 2].pTexelBufferView = nullptr;
}
// Update descriptor sets
vkUpdateDescriptorSets(logicalDevice, static_cast<uint32_t>(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr);
}

void Renderer::CreateGraphicsPipeline() {
Expand Down Expand Up @@ -717,7 +849,7 @@ void Renderer::CreateComputePipeline() {
computeShaderStageInfo.pName = "main";

// TODO: Add the compute dsecriptor set layout you create to this list
std::vector<VkDescriptorSetLayout> descriptorSetLayouts = { cameraDescriptorSetLayout, timeDescriptorSetLayout };
std::vector<VkDescriptorSetLayout> descriptorSetLayouts = { cameraDescriptorSetLayout, timeDescriptorSetLayout, computeDescriptorSetLayout };

// Create pipeline layout
VkPipelineLayoutCreateInfo pipelineLayoutInfo = {};
Expand Down Expand Up @@ -884,6 +1016,10 @@ void Renderer::RecordComputeCommandBuffer() {
vkCmdBindDescriptorSets(computeCommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, computePipelineLayout, 1, 1, &timeDescriptorSet, 0, nullptr);

// TODO: For each group of blades bind its descriptor set and dispatch
for (uint32_t i = 0; i < scene->GetBlades().size(); ++i) {
vkCmdBindDescriptorSets(computeCommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, computePipelineLayout, 2, 1, &computeDescriptorSets[i], 0, nullptr);
vkCmdDispatch(computeCommandBuffer, (NUM_BLADES / WORKGROUP_SIZE), 1, 1);
}

// ~ End recording ~
if (vkEndCommandBuffer(computeCommandBuffer) != VK_SUCCESS) {
Expand Down Expand Up @@ -976,13 +1112,14 @@ void Renderer::RecordCommandBuffers() {
VkBuffer vertexBuffers[] = { scene->GetBlades()[j]->GetCulledBladesBuffer() };
VkDeviceSize offsets[] = { 0 };
// TODO: Uncomment this when the buffers are populated
// vkCmdBindVertexBuffers(commandBuffers[i], 0, 1, vertexBuffers, offsets);
vkCmdBindVertexBuffers(commandBuffers[i], 0, 1, vertexBuffers, offsets);

// TODO: Bind the descriptor set for each grass blades model
vkCmdBindDescriptorSets(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, grassPipelineLayout, 1, 1, &grassDescriptorSets[j], 0, nullptr);

// Draw
// TODO: Uncomment this when the buffers are populated
// vkCmdDrawIndirect(commandBuffers[i], scene->GetBlades()[j]->GetNumBladesBuffer(), 0, 1, sizeof(BladeDrawIndirect));
vkCmdDrawIndirect(commandBuffers[i], scene->GetBlades()[j]->GetNumBladesBuffer(), 0, 1, sizeof(BladeDrawIndirect));
}

// End render pass
Expand Down Expand Up @@ -1057,6 +1194,7 @@ Renderer::~Renderer() {
vkDestroyDescriptorSetLayout(logicalDevice, cameraDescriptorSetLayout, nullptr);
vkDestroyDescriptorSetLayout(logicalDevice, modelDescriptorSetLayout, nullptr);
vkDestroyDescriptorSetLayout(logicalDevice, timeDescriptorSetLayout, nullptr);
vkDestroyDescriptorSetLayout(logicalDevice, computeDescriptorSetLayout, nullptr);

vkDestroyDescriptorPool(logicalDevice, descriptorPool, nullptr);

Expand Down
Loading