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
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 2.8.12)
cmake_minimum_required(VERSION 3.5)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake")

project(cis565_project5_vulkan_grass_rendering)
Expand Down
41 changes: 36 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,41 @@ 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)
* Joanne Li
* Tested on: Windows 11, AMD Ryzen 5 7600X @ 4.70GHz 32GB, NVIDIA GeForce RTX 3070 Ti

### (TODO: Your README)
![](img/demo.gif)
*The gif takes some time to load*

*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.
## Overview
This project implements a **real-time grass** simulator and renderer using **Vulkan**. Each grass blade is represented by a Bezier curve. The project has 2 passes:
- A compute pass, which simulates physics effects of the grass blades, and optimize performance by culling them.
- A graphics pass, including a vertex shader that transforms Bezier control points, tessellation shaders that generate grass geometry, and a fragment shader that shades each blade.

## Features
### Basic Grass
![](img/basic.png)
In the compute pass, the grass blade buffer is sent to the compute shader for simulation. Then, this updated buffer is passed to the graphics pass. The vertex shader sends the data to the tessellation shader, which dynamically creates quads for each grass blade. Finally, the fragment shader shades the grass, coloring it based on its height to create a natural gradient effect.

### Force Simulation
The grass blades are affected by three forces:
- Gravity pulls the blades downward.
- Wind pushes the blades sideways. A 2D noise function is used to vary wind strength to add some variation.
- Recovery force helps the blades return to their upright position after being bent.

### Culling
![](img/culling.gif)
*The gif takes some time to load*

To improve performance, several culling methods are implemented:
- Orientation culling removes blades facing away from the camera.
- View frustum culling excludes blades outside the camera’s view.
- Distance culling skips blades that are too far to be noticed.

### Performance Analysis
![](img/analysis_chart.png)
![](img/analysis_table.png)
I compared the time per frame under different conditions: no culling, orientation culling, distance culling, frustum culling, and all three combined. Each method gave a clear performance boost, and combining all three achieved the best optimization, greatly reducing rendering time while keeping visual quality.

To capture the frame rate, I used RenderDoc to inject the program.
![](img/debug.png)
Binary file modified bin/Release/vulkan_grass_rendering.exe
Binary file not shown.
4 changes: 2 additions & 2 deletions external/GLFW/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 2.8.12)
cmake_minimum_required(VERSION 3.5)

project(GLFW C)

Expand All @@ -7,7 +7,7 @@ set(CMAKE_LEGACY_CYGWIN_WIN32 OFF)
if (NOT CMAKE_VERSION VERSION_LESS "3.0")
# Until all major package systems have moved to CMake 3,
# we stick with the older INSTALL_NAME_DIR mechanism
cmake_policy(SET CMP0042 OLD)
# cmake_policy(SET CMP0042 OLD)
endif()

if (NOT CMAKE_VERSION VERSION_LESS "3.1")
Expand Down
Binary file added img/analysis_chart.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/analysis_table.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/basic.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/culling.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/debug.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/demo.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.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 << 15;
constexpr static float MIN_HEIGHT = 1.3f;
constexpr static float MAX_HEIGHT = 2.5f;
constexpr static float MIN_WIDTH = 0.1f;
Expand Down
11 changes: 8 additions & 3 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
file(GLOB SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp ${CMAKE_CURRENT_SOURCE_DIR}/*.h)
file(GLOB CPP_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
file(GLOB HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/*.h)

file(GLOB IMAGES
${CMAKE_CURRENT_SOURCE_DIR}/images/*.jpg
Expand All @@ -20,13 +21,17 @@ file(GLOB_RECURSE SHADER_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/*.tesc
)

source_group("Sources" FILES ${CPP_SOURCES})
source_group("Headers" FILES ${HEADERS})
source_group("Shaders" FILES ${SHADER_SOURCES})

set(ALL_SOURCES ${CPP_SOURCES} ${HEADERS} ${SHADER_SOURCES})

if(WIN32)
add_executable(vulkan_grass_rendering WIN32 ${SOURCES} ${SHADER_SOURCES})
add_executable(vulkan_grass_rendering WIN32 ${SOURCES} ${ALL_SOURCES})
target_link_libraries(vulkan_grass_rendering ${WINLIBS})
else(WIN32)
add_executable(vulkan_grass_rendering ${SOURCES})
add_executable(vulkan_grass_rendering ${ALL_SOURCES})
target_link_libraries(vulkan_grass_rendering ${CMAKE_THREAD_LIBS_INIT})
endif(WIN32)

Expand Down
157 changes: 152 additions & 5 deletions src/Renderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,22 @@ Renderer::Renderer(Device* device, SwapChain* swapChain, Scene* scene, Camera* c

CreateCommandPools();
CreateRenderPass();

// Create Descriptor Set Layouts
CreateCameraDescriptorSetLayout();
CreateModelDescriptorSetLayout();
CreateTimeDescriptorSetLayout();
CreateComputeDescriptorSetLayout();

CreateDescriptorPool();

// Create Descriptor Sets
CreateCameraDescriptorSet();
CreateModelDescriptorSets();
CreateGrassDescriptorSets();
CreateTimeDescriptorSet();
CreateComputeDescriptorSets();

CreateFrameResources();
CreateGraphicsPipeline();
CreateGrassPipeline();
Expand Down Expand Up @@ -198,6 +204,43 @@ 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

// Bind 0: Blades storage buffer
VkDescriptorSetLayoutBinding bladesBufferBinding = {};
bladesBufferBinding.binding = 0;
bladesBufferBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
bladesBufferBinding.descriptorCount = 1;
bladesBufferBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;

// Bind 1: Culled blades storage buffer
VkDescriptorSetLayoutBinding culledBladesBufferBinding = {};
culledBladesBufferBinding.binding = 1;
culledBladesBufferBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
culledBladesBufferBinding.descriptorCount = 1;
culledBladesBufferBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;

// Bind 2: Number of blades storage buffer
VkDescriptorSetLayoutBinding numBladesBufferBinding = {};
numBladesBufferBinding.binding = 2;
numBladesBufferBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
numBladesBufferBinding.descriptorCount = 1;
numBladesBufferBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;

std::vector<VkDescriptorSetLayoutBinding> bindings = {
bladesBufferBinding,
culledBladesBufferBinding,
numBladesBufferBinding
};

// 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,12 +259,14 @@ void Renderer::CreateDescriptorPool() {
{ VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER , 1 },

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

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

if (vkCreateDescriptorPool(logicalDevice, &poolInfo, nullptr, &descriptorPool) != VK_SUCCESS) {
Expand Down Expand Up @@ -320,6 +365,42 @@ 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;
}

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

void Renderer::CreateTimeDescriptorSet() {
Expand Down Expand Up @@ -360,6 +441,68 @@ void Renderer::CreateTimeDescriptorSet() {
void Renderer::CreateComputeDescriptorSets() {
// TODO: Create Descriptor sets for the compute pipeline
// The descriptors should point to Storage buffers which will hold the grass blades, the culled grass blades, and the output number of grass blades
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 descriptor set");
}

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

for (uint32_t i = 0; i < scene->GetBlades().size(); ++i) {

// bladesBuffer
VkDescriptorBufferInfo bladesBufferInfo = {};
bladesBufferInfo.buffer = scene->GetBlades()[i]->GetBladesBuffer();
bladesBufferInfo.offset = 0;
bladesBufferInfo.range = VK_WHOLE_SIZE;

// culledBladesBuffer
VkDescriptorBufferInfo culledBladesBufferInfo = {};
culledBladesBufferInfo.buffer = scene->GetBlades()[i]->GetCulledBladesBuffer();
culledBladesBufferInfo.offset = 0;
culledBladesBufferInfo.range = VK_WHOLE_SIZE;

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

// bladesBuffer
descriptorWrites[i * 3 + 0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrites[i * 3 + 0].dstSet = computeDescriptorSets[i];
descriptorWrites[i * 3 + 0].dstBinding = 0;
descriptorWrites[i * 3 + 0].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
descriptorWrites[i * 3 + 0].descriptorCount = 1;
descriptorWrites[i * 3 + 0].pBufferInfo = &bladesBufferInfo;

// culledBladesBuffer
descriptorWrites[i * 3 + 1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrites[i * 3 + 1].dstSet = computeDescriptorSets[i];
descriptorWrites[i * 3 + 1].dstBinding = 1;
descriptorWrites[i * 3 + 1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
descriptorWrites[i * 3 + 1].descriptorCount = 1;
descriptorWrites[i * 3 + 1].pBufferInfo = &culledBladesBufferInfo;

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

void Renderer::CreateGraphicsPipeline() {
Expand Down Expand Up @@ -717,7 +860,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,7 +1027,11 @@ 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) {
throw std::runtime_error("Failed to record compute command buffer");
Expand Down Expand Up @@ -976,13 +1123,13 @@ void Renderer::RecordCommandBuffers() {
VkBuffer vertexBuffers[] = { scene->GetBlades()[j]->GetCulledBladesBuffer() };
VkDeviceSize offsets[] = { 0 };
// TODO: Uncomment this when the buffers are populated
// vkCmdBindVertexBuffers(commandBuffers[i], 0, 1, vertexBuffers, offsets);
vkCmdBindVertexBuffers(commandBuffers[i], 0, 1, vertexBuffers, offsets);

// TODO: Bind the descriptor set for each grass blades model

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

// End render pass
Expand Down
3 changes: 3 additions & 0 deletions src/Renderer.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,15 @@ class Renderer {
VkDescriptorSetLayout cameraDescriptorSetLayout;
VkDescriptorSetLayout modelDescriptorSetLayout;
VkDescriptorSetLayout timeDescriptorSetLayout;
VkDescriptorSetLayout computeDescriptorSetLayout;

VkDescriptorPool descriptorPool;

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

VkPipelineLayout graphicsPipelineLayout;
VkPipelineLayout grassPipelineLayout;
Expand Down
Loading