Skip to content
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
75 changes: 70 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,75 @@ 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)
* Yifan Lu
* [LinkedIn](https://www.linkedin.com/in/yifan-lu-495559231/), [personal website](http://portfolio.samielouse.icu/index.php/category/featured/)
* Tested on: Windows 11, AMD Ryzen 7 5800H 3.20 GHz, Nvidia GeForce RTX 3060 Laptop GPU (Personal Laptop)

### (TODO: Your README)
![](img/cover.gif)

### Feature
- Real-time rendering of grass blades with lambert shading
- Three distinct culling tests: orientation, view-frustum, and distance culling
- Tessellation shader to transform Bezier curves into grass blade geometry

### Introduction
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).

This project presents a Vulkan-based grass simulator and renderer using compute shaders for efficient, realistic grass animation. Each grass blade is represented as a Bezier curve, with physics calculations applied to simulate natural movements influenced by gravity, wind, and recovery forces. Grass blades are dynamically culled to improve performance, removing those outside the frame or that won’t contribute meaningfully to the scene. The rendering pipeline includes a vertex shader for transforming Bezier control points, tessellation shaders to generate grass geometry, and a fragment shader for shading the blades.

### Grass Animation
The grass blades' animation is griven by three forces:
- wind force
- recovery
- gravity

The forces are applied to the "guid point" v2:

<p float="center">
<img src="https://github.com/user-attachments/assets/0829f754-21f3-4bba-b343-39360392326e" width="25%" />
</p>

A comparison of applying force before and after:
<p float="center">
<img src="https://github.com/user-attachments/assets/62c07c7c-34f0-491b-ada9-2ff98e4c32f6" width="25%" />
<img src="https://github.com/user-attachments/assets/df4a1b9b-4f75-4572-8df5-95e4cbf60bcd" width="25%" />
</p>


### Culling
In this grass simulation project, three culling methods are employed to optimize performance by omitting grass blades that won’t significantly impact the final render:

- Orientation Culling:

Grass blades whose front faces are perpendicular to the camera view are removed, as they would appear thinner than a pixel and cause aliasing artifacts. This is determined by comparing the dot product of the view vector and the blade’s front face direction.

- View-Frustum Culling:

Blades outside the camera’s view are discarded. This is determined by checking three key points along each Bezier curve (v0, v2, and an approximated midpoint) to ensure the blade is within the view-frustum. If all points fall outside, the blade is culled.

- Distance Culling:

Blades that are too far from the camera to be visually impactful are culled. The scene is divided into distance-based "buckets" with blades progressively culled in each bucket as they are farther from the camera, allowing for a controlled fade-out of distant grass blades.

The following three gifs show the Orientation Culling, View-Frustum Culling and Distance Culling respectively.

<p float="center">
<img src="/img/orientation.gif" width="25%" />
<img src="/img/view.gif" width="25%" />
<img src="/img/distance.gif" width="25%" />
</p>


### Performance Analysis
I added a fps counter to the main loop. The following graph shows the fps change with different culling methods:

<p float="center">
<img src="https://github.com/user-attachments/assets/1f826aaf-f137-417d-bbc0-d81e5e099215" width="45%" />
</p>

The following chart illustrates the average frames per second (FPS) at different tessellation levels with a default camera angle in a grass simulation project. As the tessellation level increases, FPS decreases, reflecting the higher computational load. At lower tessellation levels (2 to 16), FPS remains high, with a peak of 3210 FPS at level 2, gradually decreasing to 2339 FPS at level 16. Beyond level 32, FPS drops sharply, stabilizing around 260 FPS from level 64 onward, indicating that increasing tessellation beyond this point has minimal impact on visual fidelity but significantly affects performance.

<p float="center">
<img src="https://github.com/user-attachments/assets/81c91b8e-a1cf-44b5-8c98-efc3ae17a468" width="45%" />
</p>

*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.
Binary file added img/cover.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/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/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/view.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/Blades.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
163 changes: 159 additions & 4 deletions src/Renderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Renderer::Renderer(Device* device, SwapChain* swapChain, Scene* scene, Camera* c
CreateModelDescriptorSetLayout();
CreateTimeDescriptorSetLayout();
CreateComputeDescriptorSetLayout();

CreateDescriptorPool();
CreateCameraDescriptorSet();
CreateModelDescriptorSets();
Expand Down Expand Up @@ -198,6 +199,40 @@ 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 BladeBufferLayoutBinding = {};
BladeBufferLayoutBinding.binding = 0;
BladeBufferLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
BladeBufferLayoutBinding.descriptorCount = 1;
BladeBufferLayoutBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT | VK_SHADER_STAGE_VERTEX_BIT;
BladeBufferLayoutBinding.pImmutableSamplers = nullptr;

VkDescriptorSetLayoutBinding CullingBufferLayoutBinding = {};
CullingBufferLayoutBinding.binding = 1;
CullingBufferLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
CullingBufferLayoutBinding.descriptorCount = 1;
CullingBufferLayoutBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT | VK_SHADER_STAGE_VERTEX_BIT;
CullingBufferLayoutBinding.pImmutableSamplers = nullptr;

VkDescriptorSetLayoutBinding NumBladesBufferLayoutBinding = {};
NumBladesBufferLayoutBinding.binding = 2;
NumBladesBufferLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
NumBladesBufferLayoutBinding.descriptorCount = 1;
NumBladesBufferLayoutBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT | VK_SHADER_STAGE_VERTEX_BIT;
NumBladesBufferLayoutBinding.pImmutableSamplers = nullptr;

std::vector<VkDescriptorSetLayoutBinding> bindings = { BladeBufferLayoutBinding, CullingBufferLayoutBinding, NumBladesBufferLayoutBinding };

// Create the descriptor set layout
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 descriptor set layout");
}

}

void Renderer::CreateDescriptorPool() {
Expand All @@ -216,13 +251,15 @@ void Renderer::CreateDescriptorPool() {
{ VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER , 1 },

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

VkDescriptorPoolCreateInfo poolInfo = {};
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
poolInfo.poolSizeCount = static_cast<uint32_t>(poolSizes.size());
poolInfo.pPoolSizes = poolSizes.data();
poolInfo.maxSets = 5;
poolInfo.maxSets = 8;

if (vkCreateDescriptorPool(logicalDevice, &poolInfo, nullptr, &descriptorPool) != VK_SUCCESS) {
throw std::runtime_error("Failed to create descriptor pool");
Expand Down Expand Up @@ -320,8 +357,47 @@ 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 (size_t i = 0; i < scene->GetBlades().size(); ++i) {
VkDescriptorBufferInfo grassBufferInfo{};
grassBufferInfo.buffer = scene->GetBlades()[i]->GetModelBuffer();
grassBufferInfo.offset = 0;
grassBufferInfo.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 = &grassBufferInfo;
descriptorWrites[i].pImageInfo = nullptr;
descriptorWrites[i].pTexelBufferView = nullptr;
}

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


void Renderer::CreateTimeDescriptorSet() {
// Describe the desciptor set
VkDescriptorSetLayout layouts[] = { timeDescriptorSetLayout };
Expand Down Expand Up @@ -360,6 +436,79 @@ void Renderer::CreateTimeDescriptorSet() {
void Renderer::CreateComputeDescriptorSets() {
// TODO: Create Descriptor sets for the compute pipeline
// The descriptors should point to Storage buffers which will hold the grass blades, the culled grass blades, and the output number of grass blades
// Describe the desciptor set
computeDescriptorSets.resize(scene->GetBlades().size());
VkDescriptorSetLayout layouts[] = { computeDescriptorSetLayout };
VkDescriptorSetAllocateInfo allocInfo = {};
allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
allocInfo.descriptorPool = descriptorPool;
allocInfo.descriptorSetCount = static_cast<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 descriptor set");
}

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

for (size_t i = 0; i < scene->GetBlades().size(); ++i) {
// create input blades buffer descriptor sets
VkDescriptorBufferInfo inputBladesBufferInfo{};
inputBladesBufferInfo.buffer = scene->GetBlades()[i]->GetBladesBuffer();
inputBladesBufferInfo.offset = 0;
inputBladesBufferInfo.range = NUM_BLADES * sizeof(Blade);

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

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

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

// create num blades buffer descriptor sets
VkDescriptorBufferInfo numBladesBufferInfo{};
numBladesBufferInfo.buffer = scene->GetBlades()[i]->GetNumBladesBuffer();
numBladesBufferInfo.offset = 0;
numBladesBufferInfo.range = sizeof(BladeDrawIndirect);

VkWriteDescriptorSet& numBladesDescriptorWrite = descriptorWrites[3 * i + 2];
numBladesDescriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
numBladesDescriptorWrite.dstSet = computeDescriptorSets[i];
numBladesDescriptorWrite.dstBinding = 2;
numBladesDescriptorWrite.dstArrayElement = 0;
numBladesDescriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
numBladesDescriptorWrite.descriptorCount = 1;
numBladesDescriptorWrite.pBufferInfo = &numBladesBufferInfo;
numBladesDescriptorWrite.pImageInfo = nullptr;
numBladesDescriptorWrite.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 +866,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 +1033,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) / WORKGROUP_SIZE, 1, 1);
}

// ~ End recording ~
if (vkEndCommandBuffer(computeCommandBuffer) != VK_SUCCESS) {
Expand Down Expand Up @@ -976,13 +1129,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 +1211,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
4 changes: 4 additions & 0 deletions src/Renderer.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class Renderer {
void CreateModelDescriptorSetLayout();
void CreateTimeDescriptorSetLayout();
void CreateComputeDescriptorSetLayout();
void CreateGrassDescriptorSetLayout(); // new

void CreateDescriptorPool();

Expand Down Expand Up @@ -56,12 +57,15 @@ class Renderer {
VkDescriptorSetLayout cameraDescriptorSetLayout;
VkDescriptorSetLayout modelDescriptorSetLayout;
VkDescriptorSetLayout timeDescriptorSetLayout;
VkDescriptorSetLayout computeDescriptorSetLayout; // new

VkDescriptorPool descriptorPool;

VkDescriptorSet cameraDescriptorSet;
std::vector<VkDescriptorSet> modelDescriptorSets;
VkDescriptorSet timeDescriptorSet;
std::vector<VkDescriptorSet> computeDescriptorSets; // new
std::vector<VkDescriptorSet> grassDescriptorSets; // new

VkPipelineLayout graphicsPipelineLayout;
VkPipelineLayout grassPipelineLayout;
Expand Down
11 changes: 11 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "Camera.h"
#include "Scene.h"
#include "Image.h"
#include <iostream>

Device* device;
SwapChain* swapChain;
Expand Down Expand Up @@ -143,12 +144,22 @@ int main() {
glfwSetMouseButtonCallback(GetGLFWWindow(), mouseDownCallback);
glfwSetCursorPosCallback(GetGLFWWindow(), mouseMoveCallback);

// add fps counter
high_resolution_clock::time_point lastTime = high_resolution_clock::now();
int frameCount = 0;

while (!ShouldQuit()) {
glfwPollEvents();
scene->UpdateTime();
renderer->Frame();
frameCount++;
}

high_resolution_clock::time_point currentTime = high_resolution_clock::now();
duration<float> timeSpan = duration_cast<duration<float>>(currentTime - lastTime);
float fps = frameCount / timeSpan.count();
std::cout << "FPS: " << fps << std::endl;

vkDeviceWaitIdle(device->GetVkDevice());

vkDestroyImage(device->GetVkDevice(), grassImage, nullptr);
Expand Down
Loading