diff --git a/README.md b/README.md
index 20ee451..6c95a58 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,124 @@
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)
+* Joanna Fisch
+ * [LinkedIn](https://www.linkedin.com/in/joanna-fisch-bb2979186/), [Website](https://sites.google.com/view/joannafischsportfolio/home)
+* Tested on: Windows 11, i7-12700H @ 2.30GHz 16GB, NVIDIA GeForce RTX 3060 (Laptop)
-### (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 implements a grass simulation and renderer using Vulkan, based on 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). It uses compute shaders to simulate forces on Bezier curves, which represent individual grass blades, and graphics shaders to dynamically tessellate and shade the grass. Key features include physics-based blade movement, culling techniques to optimize performance, and tessellation shaders to shape the grass blades.
+
+### Features Implemented
+* **Compute Shader for Physics Calculations:** Forces are applied to simulate gravity, wind, and recovery on each grass blade. Adjustments ensure blades retain realistic movement.
+* **Vertex Shader:** Applies transformations to the Bezier control points, preparing the blades for tessellation.
+* **Tessellation Control and Evaluation Shaders:** Dynamically generate detailed grass geometry, creating realistic curvature in blades. The evaluation shader converts tessellated patches into clip space for rendering.
+* **Fragment Shader:** Handles shading based on environmental lighting, producing a natural grass appearance.
+* **Resource Binding:** Storage buffers store grass blade data across frames, and descriptors manage compute pipeline resources.
+* **Culling Techniques:**
+ * _Orientation Culling:_ Avoids rendering blades angled perpendicular to the camera.
+ * _View-Frustum Culling:_ Skips blades outside the camera view.
+ * _Distance Culling:_ Limits rendering for distant blades to improve performance.
+* **Extra Credit:** Variable tessellation levels based on camera distance for optimized detail.
+
+### Project Stages
+1. Initial Blade Display - Lighting
+ * The grass in the rendering is lit using Lambertian shading, where ambient and diffuse lighting are combined to produce a natural effect. This shader code adjusts the grass color based on height, producing a darker green near the base and lighter green at the top, to simulate natural lighting and height effects.
+
+
+ |
+ |
+ |
+ |
+
+
+ Diffuse Lighting |
+ Position |
+ Normal |
+ Full Lighting |
+
+
+ Grass color based on height and ambient and diffuse lighting |
+
+
+
+2. Physics Simulation
+
+
+ |
+ |
+
+
+ No forces |
+ With Forces |
+
+
+ Gravity, wind, and recovery forces affecting blade movement |
+
+
+
+3. Culling Optimization
+
+
+ |
+ |
+ |
+
+
+ Orientation Culling |
+ View-Frustum Culling |
+ Distance Culling |
+
+
+ Culling based on orientation, distance, and frustum tests |
+
+
+
+4. Tesselation
+ * The variable tessellation levels refine the geometry of each grass blade based on camera distance, creating a more detailed appearance without an excessive increase in the vertex count, which is critical for efficiently rendering complex scenes like grass fields.
+
+
+ |
+ |
+
+
+ Tesselation Max Distance 50 |
+ Tesselation Max Distance 30 |
+
+
+
+### Performance Analysis
+#### Varying Blade Counts
+Testing different numbers of grass blades to analyze frame rate and memory usage, as shown in the table below:
+
+To calculate the memory usage as a percentage of the memory budget for each heap, we use the following formula:
+
+**Memory Usage Percentage** = (Usage / Budget) * 100
+
+ where:
+- **Usage** is the memory currently in use (in MB).
+- **Budget** is the maximum available memory for that heap (in MB).
+
+For each performance scenario, the total memory usage is calculated by summing the memory usage across all heaps and computing the overall usage percentage relative to the total budget.
+
+| Blade Count | FPS | Memory Usage (MB) | % of Budget |
+|-------|-------------|-------------|-------------|
+| 2^5 | 1958 | 57.8867 | 0.46% |
+| 2^9 | 1678 | 57.9141 | 0.46% |
+| 2^13 | 1552 | 58.8203 | 0.47% |
+| 2^17 | 329 | 73.8203 | 0.59% |
+
+> **Note:** Frame rate values are in frames per second (FPS).
+
+#### Culling Performance
+
+| Culling Technique | FPS |
+|-------|-------------|
+| No Culling | 1162 |
+| Orientation Culling | 1288 |
+| View-Frustum Culling | 1293 |
+| Distance Culling | 1385 |
+| Full Culling | 1535 |
diff --git a/img/distance_culling.gif b/img/distance_culling.gif
new file mode 100644
index 0000000..0b94fbe
Binary files /dev/null and b/img/distance_culling.gif differ
diff --git a/img/distance_culling_2.gif b/img/distance_culling_2.gif
new file mode 100644
index 0000000..d6b2144
Binary files /dev/null and b/img/distance_culling_2.gif differ
diff --git a/img/frustrum_culling.gif b/img/frustrum_culling.gif
new file mode 100644
index 0000000..f30ecab
Binary files /dev/null and b/img/frustrum_culling.gif differ
diff --git a/img/frustrum_culling_2.gif b/img/frustrum_culling_2.gif
new file mode 100644
index 0000000..ea1037d
Binary files /dev/null and b/img/frustrum_culling_2.gif differ
diff --git a/img/full_demo.gif b/img/full_demo.gif
new file mode 100644
index 0000000..c8b3bb9
Binary files /dev/null and b/img/full_demo.gif differ
diff --git a/img/grass_diffuse.png b/img/grass_diffuse.png
new file mode 100644
index 0000000..3addad5
Binary files /dev/null and b/img/grass_diffuse.png differ
diff --git a/img/grass_full.png b/img/grass_full.png
new file mode 100644
index 0000000..51ce1b2
Binary files /dev/null and b/img/grass_full.png differ
diff --git a/img/grass_noForce.png b/img/grass_noForce.png
new file mode 100644
index 0000000..78d10e8
Binary files /dev/null and b/img/grass_noForce.png differ
diff --git a/img/grass_nor.png b/img/grass_nor.png
new file mode 100644
index 0000000..de989cc
Binary files /dev/null and b/img/grass_nor.png differ
diff --git a/img/grass_pos.png b/img/grass_pos.png
new file mode 100644
index 0000000..26500dd
Binary files /dev/null and b/img/grass_pos.png differ
diff --git a/img/grass_tesselationLevel.png b/img/grass_tesselationLevel.png
new file mode 100644
index 0000000..b4b01de
Binary files /dev/null and b/img/grass_tesselationLevel.png differ
diff --git a/img/grass_tesselationLevel_2.png b/img/grass_tesselationLevel_2.png
new file mode 100644
index 0000000..71ef707
Binary files /dev/null and b/img/grass_tesselationLevel_2.png differ
diff --git a/img/orientation_culling.gif b/img/orientation_culling.gif
new file mode 100644
index 0000000..0d8a3c9
Binary files /dev/null and b/img/orientation_culling.gif differ
diff --git a/img/orientation_culling_2.gif b/img/orientation_culling_2.gif
new file mode 100644
index 0000000..0334777
Binary files /dev/null and b/img/orientation_culling_2.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/Instance.cpp b/src/Instance.cpp
index 7f6b01c..4775e66 100644
--- a/src/Instance.cpp
+++ b/src/Instance.cpp
@@ -49,7 +49,7 @@ Instance::Instance(const char* applicationName, unsigned int additionalExtension
appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.pEngineName = "No Engine";
appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
- appInfo.apiVersion = VK_API_VERSION_1_0;
+ appInfo.apiVersion = VK_API_VERSION_1_1;
// --- Create Vulkan instance ---
VkInstanceCreateInfo createInfo = {};
diff --git a/src/Renderer.cpp b/src/Renderer.cpp
index b445d04..f28748c 100644
--- a/src/Renderer.cpp
+++ b/src/Renderer.cpp
@@ -198,6 +198,38 @@ 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 storeBladesBinding = {};
+ storeBladesBinding.binding = 0;
+ storeBladesBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
+ storeBladesBinding.descriptorCount = 1;
+ storeBladesBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
+ storeBladesBinding.pImmutableSamplers = nullptr;
+
+ VkDescriptorSetLayoutBinding culledBladesBinding = {};
+ culledBladesBinding.binding = 1;
+ culledBladesBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
+ culledBladesBinding.descriptorCount = 1;
+ culledBladesBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
+ culledBladesBinding.pImmutableSamplers = nullptr;
+
+ VkDescriptorSetLayoutBinding numBladesBinding = {};
+ numBladesBinding.binding = 2;
+ numBladesBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
+ numBladesBinding.descriptorCount = 1;
+ numBladesBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
+ numBladesBinding.pImmutableSamplers = nullptr;
+
+ std::vector bindings = { storeBladesBinding, culledBladesBinding, numBladesBinding };
+
+ // 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 compute descriptor set layout");
+ }
}
void Renderer::CreateDescriptorPool() {
@@ -216,6 +248,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(3 * scene->GetBlades().size()) },
};
VkDescriptorPoolCreateInfo poolInfo = {};
@@ -320,6 +353,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(grassDescriptorSets.size());
+ allocInfo.pSetLayouts = layouts;
+
+ // Allocate descriptor sets
+ if (vkAllocateDescriptorSets(logicalDevice, &allocInfo, grassDescriptorSets.data()) != VK_SUCCESS) {
+ throw std::runtime_error("Failed to allocate grass descriptor set");
+ }
+
+ std::vector 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(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr);
}
void Renderer::CreateTimeDescriptorSet() {
@@ -360,6 +429,69 @@ 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(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 descriptorWrites(3 * computeDescriptorSets.size());
+
+ for (uint32_t i = 0; i < scene->GetBlades().size(); ++i) {
+ VkDescriptorBufferInfo storedBladesBufferInfo = {};
+ storedBladesBufferInfo.buffer = scene->GetBlades()[i]->GetBladesBuffer();
+ storedBladesBufferInfo.offset = 0;
+ storedBladesBufferInfo.range = NUM_BLADES * sizeof(Blade);
+ 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 = &storedBladesBufferInfo;
+ descriptorWrites[3 * i + 0].pImageInfo = nullptr;
+ descriptorWrites[3 * i + 0].pTexelBufferView = nullptr;
+
+ VkDescriptorBufferInfo culledBladesBufferInfo = {};
+ culledBladesBufferInfo.buffer = scene->GetBlades()[i]->GetCulledBladesBuffer();
+ culledBladesBufferInfo.offset = 0;
+ culledBladesBufferInfo.range = NUM_BLADES * sizeof(Blade);
+ 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;
+
+ VkDescriptorBufferInfo numBladesBufferInfo = {};
+ numBladesBufferInfo.buffer = scene->GetBlades()[i]->GetNumBladesBuffer();
+ numBladesBufferInfo.offset = 0;
+ numBladesBufferInfo.range = sizeof(BladeDrawIndirect);
+ 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(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr);
}
void Renderer::CreateGraphicsPipeline() {
@@ -717,7 +849,7 @@ void Renderer::CreateComputePipeline() {
computeShaderStageInfo.pName = "main";
// TODO: Add the compute dsecriptor set layout you create to this list
- std::vector descriptorSetLayouts = { cameraDescriptorSetLayout, timeDescriptorSetLayout };
+ std::vector descriptorSetLayouts = { cameraDescriptorSetLayout, timeDescriptorSetLayout, computeDescriptorSetLayout };
// Create pipeline layout
VkPipelineLayoutCreateInfo pipelineLayoutInfo = {};
@@ -884,6 +1016,14 @@ void Renderer::RecordComputeCommandBuffer() {
vkCmdBindDescriptorSets(computeCommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, computePipelineLayout, 1, 1, &timeDescriptorSet, 0, nullptr);
// TODO: For each group of blades bind its descriptor set and dispatch
+ for (uint32_t j = 0; j < scene->GetBlades().size(); ++j) {
+
+ // Bind the descriptor set for each group of blades
+ vkCmdBindDescriptorSets(computeCommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, computePipelineLayout, 2, 1, &computeDescriptorSets[j], 0, nullptr);
+
+ // Draw
+ vkCmdDispatch(computeCommandBuffer, (NUM_BLADES) / WORKGROUP_SIZE, 1, 1);
+ }
// ~ End recording ~
if (vkEndCommandBuffer(computeCommandBuffer) != VK_SUCCESS) {
@@ -973,16 +1113,17 @@ void Renderer::RecordCommandBuffers() {
vkCmdBindPipeline(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, grassPipeline);
for (uint32_t j = 0; j < scene->GetBlades().size(); ++j) {
- VkBuffer vertexBuffers[] = { scene->GetBlades()[j]->GetCulledBladesBuffer() };
+ VkBuffer vertexBuffers[] = { scene->GetBlades()[j]->GetCulledBladesBuffer() }; // 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
@@ -1057,6 +1198,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..a2a47f7 100644
--- a/src/Renderer.h
+++ b/src/Renderer.h
@@ -55,12 +55,15 @@ class Renderer {
VkDescriptorSetLayout cameraDescriptorSetLayout;
VkDescriptorSetLayout modelDescriptorSetLayout;
+ VkDescriptorSetLayout computeDescriptorSetLayout;
VkDescriptorSetLayout timeDescriptorSetLayout;
VkDescriptorPool descriptorPool;
VkDescriptorSet cameraDescriptorSet;
std::vector modelDescriptorSets;
+ std::vector grassDescriptorSets;
+ std::vector computeDescriptorSets;
VkDescriptorSet timeDescriptorSet;
VkPipelineLayout graphicsPipelineLayout;
diff --git a/src/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..03ed161 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -5,18 +5,65 @@
#include "Camera.h"
#include "Scene.h"
#include "Image.h"
+#include
+#include
Device* device;
SwapChain* swapChain;
Renderer* renderer;
Camera* camera;
+// For frame rate calculation
+std::chrono::time_point lastTime = std::chrono::high_resolution_clock::now();
+int frameCount = 0;
+
+void calculateFPS() {
+ frameCount++;
+ auto currentTime = std::chrono::high_resolution_clock::now();
+ std::chrono::duration elapsed = currentTime - lastTime;
+
+ if (elapsed.count() >= 1.0f) {
+ std::cout << "FPS: " << frameCount << std::endl;
+ frameCount = 0;
+ lastTime = currentTime;
+ }
+}
+
+// Function to display memory usage if VK_EXT_memory_budget is available
+void printMemoryUsage() {
+ VkPhysicalDeviceMemoryProperties2 memoryProperties = {};
+ memoryProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2;
+
+ VkPhysicalDeviceMemoryBudgetPropertiesEXT memoryBudgetProperties = {};
+ memoryBudgetProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT;
+ memoryProperties.pNext = &memoryBudgetProperties;
+
+ vkGetPhysicalDeviceMemoryProperties2(device->GetInstance()->GetPhysicalDevice(), &memoryProperties);
+
+ double totalUsageMB = 0;
+ double totalBudgetMB = 0;
+
+ for (uint32_t i = 0; i < memoryProperties.memoryProperties.memoryHeapCount; i++) {
+ double heapUsageMB = memoryBudgetProperties.heapUsage[i] / (1024.0 * 1024.0);
+ double heapBudgetMB = memoryBudgetProperties.heapBudget[i] / (1024.0 * 1024.0);
+
+ totalUsageMB += heapUsageMB;
+ totalBudgetMB += heapBudgetMB;
+
+ // Logging each heap's usage and budget for debugging
+ std::cout << "Heap " << i << " - Budget: " << heapBudgetMB << " MB, Usage: " << heapUsageMB << " MB\n";
+ }
+
+ double memoryUsagePercentage = (totalUsageMB / totalBudgetMB) * 100;
+ std::cout << "| Grass Rendering - Medium Density | " << totalUsageMB << " MB (" << memoryUsagePercentage << "% of Budget) |\n";
+}
+
namespace {
void resizeCallback(GLFWwindow* window, int width, int height) {
if (width == 0 || height == 0) return;
vkDeviceWaitIdle(device->GetVkDevice());
- swapChain->Recreate();
+ swapChain->Recreate(width, height);
renderer->RecreateFrameResources();
}
@@ -143,10 +190,19 @@ int main() {
glfwSetMouseButtonCallback(GetGLFWWindow(), mouseDownCallback);
glfwSetCursorPosCallback(GetGLFWWindow(), mouseMoveCallback);
+ int count = 0;
while (!ShouldQuit()) {
glfwPollEvents();
scene->UpdateTime();
renderer->Frame();
+
+ // Output FPS and memory usage
+ /*if (count < 2) {
+ printMemoryUsage();
+ count++;
+ }*/
+ // calculateFPS();
+
}
vkDeviceWaitIdle(device->GetVkDevice());
diff --git a/src/shaders/compute.comp b/src/shaders/compute.comp
index 0fd0224..7c3b507 100644
--- a/src/shaders/compute.comp
+++ b/src/shaders/compute.comp
@@ -4,6 +4,15 @@
#define WORKGROUP_SIZE 32
layout(local_size_x = WORKGROUP_SIZE, local_size_y = 1, local_size_z = 1) in;
+#define RECOVERY 1
+#define GRAVITY 1
+#define WIND 1
+#define DISTANCE 30.0f
+#define TOLERANCE 0.2f
+#define C_ORIENTATION 1
+#define C_FRUSTRUM 1
+#define C_DISTANCE 1
+
layout(set = 0, binding = 0) uniform CameraBufferObject {
mat4 view;
mat4 proj;
@@ -27,30 +36,147 @@ struct Blade {
// 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 = 0) buffer StoreBlades {
+ Blade inBlades[];
+};
+
+layout(set = 2, binding = 1) buffer CulledBlades {
+ Blade culledBlades[];
+};
+
+layout(set = 2, binding = 2) buffer NumBlades {
+ uint vertexCount; //used atomic add to write number of blades remaining
+ uint instanceCount; // = 1
+ uint firstVertex; // = 0
+ uint firstInstance; // = 0
+} numBlades;
bool inBounds(float value, float bounds) {
return (value >= -bounds) && (value <= bounds);
}
+bool inBoundsZ(float value, float bounds) {
+ return (value >= 0.0f) && (value <= bounds);
+}
+
+bool inFrustum(vec4 p_proj) {
+ float h = p_proj.w + TOLERANCE;
+ return inBounds(p_proj.x, h) && inBounds(p_proj.y, h) && inBoundsZ(p_proj.z, h);
+}
+
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
+ Blade curr = inBlades[gl_GlobalInvocationID.x];
+ vec3 v0 = curr.v0.xyz;
+ vec3 v1 = curr.v1.xyz;
+ vec3 v2 = curr.v2.xyz;
+ vec3 up = curr.up.xyz;
+ float orientation = curr.v0.w;
+ float height = curr.v1.w;
+ float width = curr.v2.w;
+ float stiffness = curr.up.w;
+
// TODO: Apply forces on every blade and update the vertices in the buffer
+ vec3 side = vec3(cos(orientation), 0.0f, sin(orientation));
+ vec3 front = normalize(cross(up, side));
+
+ // Recovery Force
+#if RECOVERY
+ vec3 iv2 = v0 + height * up;
+ vec3 recovery = (iv2 - v2) * stiffness;
+#else
+ vec3 recovery = vec3(0.0f);
+#endif
+
+ // Gravity Force
+#if GRAVITY
+ vec3 gE = vec3(0.0f, -9.8f, 0.0f);
+ vec3 gF = 0.25f * length(gE) * front;
+ vec3 gravity = gE + gF;
+#else
+ vec3 gravity = vec3(0.0f);
+#endif
+
+ // Wind Force
+#if WIND
+ vec3 wind = vec3(5.f, 0.f, 3.f) * sin(totalTime);
+#else
+ vec3 wind = vec3(0.0f);
+#endif
+
+ // Apply forces to v2
+ vec3 delta = (recovery + gravity + wind) * deltaTime;
+
+ v2 += delta;
+ v2 = v2 - up * min(dot(up, (v2 - v0)), 0);
+
+ float l_proj = length(v2 - v0 - up * dot((v2 - v0), up));
+ v1 = v0 + height * up * max(1.0f - l_proj / height, 0.05 * max(l_proj / height, 1.0f));
+
+ float l0 = distance(v0, v2);
+ float l1 = distance(v0, v1) + distance(v1, v2);
+ float l = (l0 + l1) * 0.5f;
+
+ float r = height / l;
+
+ vec3 corr_v1 = v0 + r * (v1 - v0);
+ vec3 corr_v2 = corr_v1 + r * (v2 - v1);
+
+ inBlades[gl_GlobalInvocationID.x].v1.xyz = corr_v1;
+ inBlades[gl_GlobalInvocationID.x].v2.xyz = corr_v2;
// TODO: Cull blades that are too far away or not in the camera frustum and write them
// to the culled blades buffer
+
+ // cull blades whose side is parallel with camera forward
+#if C_ORIENTATION
+ vec3 grass_Forward = normalize(vec3(camera.view * vec4(side, 0.0f)));
+ vec3 cam_Forward = normalize(vec3(camera.view * vec4(v0, 1.0f)));
+
+ if (abs(dot(cam_Forward, grass_Forward)) > 0.9f) {
+ return;
+ }
+#endif
+
+ // Frustum culling for the blade using v0, midpoint m, and v2
+#if C_FRUSTRUM
+ mat4 vp = camera.proj * camera.view;
+
+ // Calculate midpoint
+ // vec3 midpoint = (v0 + v2) * 0.5;
+ vec3 midpoint = 0.25 * v0 + 0.5 * v1 + 0.25 * v2;
+
+ // Transform points to NDC space
+ vec4 v0_proj = vp * vec4(v0, 1.0);
+ vec4 m_proj = vp * vec4(midpoint, 1.0);
+ vec4 v2_proj = vp * vec4(v2, 1.0);
+
+ // Test if any of the points are within the view frustum
+ if (!(inFrustum(v0_proj)) && !(inFrustum(m_proj)) && !(inFrustum(v2_proj))) {
+ return;
+ }
+#endif
+
+ // cull blades whose distance
+#if C_DISTANCE
+ vec3 cam_pos = vec3(camera.view * vec4(v0, 1.0f));
+ vec3 cam_up = vec3(camera.view * vec4(up, 0.0f));
+
+ float d_proj = length(cam_pos - cam_up * dot(cam_pos, cam_up));
+ //if (d_proj > DISTANCE) return;
+ int n = 20;
+ if (gl_GlobalInvocationID.x % n < floor(float(n) * d_proj / DISTANCE)) {
+ return;
+ }
+#endif
+
// 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
+ culledBlades[atomicAdd(numBlades.vertexCount, 1)] = curr;
}
diff --git a/src/shaders/grass.frag b/src/shaders/grass.frag
index c7df157..64adc83 100644
--- a/src/shaders/grass.frag
+++ b/src/shaders/grass.frag
@@ -7,11 +7,37 @@ layout(set = 0, binding = 0) uniform CameraBufferObject {
} camera;
// TODO: Declare fragment shader inputs
+layout(location = 0) in vec3 fs_pos;
+layout(location = 1) in vec3 fs_nor;
+layout(location = 2) in vec2 fs_uv;
+layout(location = 3) in float fs_teslevel;
layout(location = 0) out vec4 outColor;
+vec3 pos_light = vec3(20, 20, 5);
+
void main() {
- // TODO: Compute fragment color
+ // Compute lighting (Lambertian shading)
+ vec3 light_Dir = normalize(fs_pos - pos_light);
+ float diffuseTerm = clamp(dot(light_Dir, fs_nor), 0.0f, 1.0f);
+ float ambientTerm = 0.2;
+ float lightIntensity = diffuseTerm + ambientTerm;
+
+ // Adjust color based on height
+ float minHeight = 0.0; // Minimum expected height of the grass
+ float maxHeight = 2.0; // Maximum expected height of the grass
+ float heightFactor = clamp((fs_pos.y - minHeight) / (maxHeight - minHeight), 0.0, 1.0);
+
+ // Set base green color and adjust brightness by heightFactor
+ vec3 baseColor = vec3(0.0, 0.6, 0.0); // Base green color for grass
+ vec3 heightAdjustedColor = mix(baseColor * 0.8, baseColor * 1.2, heightFactor); // Darker at low height, lighter at high
+
+ //outColor = vec4(fs_pos, 1.0f);
+ //outColor = vec4(vec3(diffuseTerm), 1.0f);
+ //outColor = vec4(fs_nor, 1.0f);
+ //outColor = vec4((fs_teslevel - 1.f) / 7.f);
+ //outColor = vec4(0.0, lightIntensity, 0.0, 1.0);
- outColor = vec4(1.0);
+ // Final output color with lighting applied
+ outColor = vec4(heightAdjustedColor * lightIntensity, 1.0f);
}
diff --git a/src/shaders/grass.tesc b/src/shaders/grass.tesc
index f9ffd07..9f6d7c3 100644
--- a/src/shaders/grass.tesc
+++ b/src/shaders/grass.tesc
@@ -9,18 +9,51 @@ layout(set = 0, binding = 0) uniform CameraBufferObject {
} camera;
// TODO: Declare tessellation control shader inputs and outputs
+in gl_PerVertex
+{
+ vec4 gl_Position;
+} gl_in[gl_MaxPatchVertices];
+
+layout(location = 0) in vec4 tcs_v1[];
+layout(location = 1) in vec4 tcs_v2[];
+layout(location = 2) in vec4 tcs_up[];
+
+layout(location = 0) out vec4 tes_v1[];
+layout(location = 1) out vec4 tes_v2[];
+layout(location = 2) out vec4 tes_up[];
+layout(location = 3) out float tes_teslevel[];
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
+ tes_v1[gl_InvocationID] = tcs_v1[gl_InvocationID];
+ tes_v2[gl_InvocationID] = tcs_v2[gl_InvocationID];
+ tes_up[gl_InvocationID] = tcs_up[gl_InvocationID];
+
+ // Extract camera position from view matrix (assuming the camera's position is in the fourth column)
+ vec3 cam_pos = vec3(camera.view * vec4(gl_out[gl_InvocationID].gl_Position.xyz, 1.0f));
+
+ // Calculate the distance between the patch and the camera
+ float distance = length(cam_pos);
+
+ // Set tessellation level based on distance
+ float maxTessLevel = 8.0;
+ float minTessLevel = 1.0;
+ float thresholdNear = 5.0; // Distance at which tessellation level is highest
+ float thresholdFar = 40.0; // Distance at which tessellation level is lowest
- // TODO: Set level of tesselation
- // gl_TessLevelInner[0] = ???
- // gl_TessLevelInner[1] = ???
- // gl_TessLevelOuter[0] = ???
- // gl_TessLevelOuter[1] = ???
- // gl_TessLevelOuter[2] = ???
- // gl_TessLevelOuter[3] = ???
+ // Interpolate tessellation level based on distance
+ float tessLevel = mix(maxTessLevel, minTessLevel, clamp((distance - thresholdNear) / (thresholdFar - thresholdNear), 0.0, 1.0));
+
+ tes_teslevel[gl_InvocationID] = tessLevel;
+
+ // Assign calculated tessellation levels
+ gl_TessLevelInner[0] = tessLevel;
+ gl_TessLevelInner[1] = tessLevel;
+ gl_TessLevelOuter[0] = tessLevel;
+ gl_TessLevelOuter[1] = tessLevel;
+ gl_TessLevelOuter[2] = tessLevel;
+ gl_TessLevelOuter[3] = tessLevel;
}
diff --git a/src/shaders/grass.tese b/src/shaders/grass.tese
index 751fff6..c118ec2 100644
--- a/src/shaders/grass.tese
+++ b/src/shaders/grass.tese
@@ -9,10 +9,49 @@ layout(set = 0, binding = 0) uniform CameraBufferObject {
} camera;
// TODO: Declare tessellation evaluation shader inputs and outputs
+layout(location = 0) in vec4 tes_v1[];
+layout(location = 1) in vec4 tes_v2[];
+layout(location = 2) in vec4 tes_up[];
+layout(location = 3) in float tes_teslevel[];
+
+layout(location = 0) out vec3 fs_pos;
+layout(location = 1) out vec3 fs_nor;
+layout(location = 2) out vec2 fs_uv;
+layout(location = 3) out float fs_teslevel;
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
+ vec3 v0 = gl_in[0].gl_Position.xyz;
+ vec3 v1 = tes_v1[0].xyz;
+ vec3 v2 = tes_v2[0].xyz;
+
+ float angle = gl_in[0].gl_Position.w;
+ float height = tes_v1[0].w;
+ float width = tes_v2[0].w;
+ float stiffness = tes_up[0].w;
+
+ vec3 a = v0 + v * (v1 - v0);
+ vec3 b = v1 + v * (v2 - v1);
+ vec3 c = a + v * (b - a);
+ vec3 t1 = vec3(cos(angle), 0.0f, sin(angle)); // bitangent
+ vec3 c0 = c - width * t1;
+ vec3 c1 = c + width * t1;
+ vec3 t0 = normalize(b - a);
+ vec3 n = normalize(cross(t0,t1));
+
+ // triangle
+ // float t = u + 0.5f * v - u * v;
+
+ // quadratic
+ float t = u - u * v * v;
+
+ fs_pos = (1 - t) * c0 + t * c1;
+ fs_nor = n;
+ fs_uv = vec2(u,v);
+ fs_teslevel = tes_teslevel[0];
+
+ gl_Position = camera.proj * camera.view * vec4(fs_pos, 1.f);
}
diff --git a/src/shaders/grass.vert b/src/shaders/grass.vert
index db9dfe9..3fb5e80 100644
--- a/src/shaders/grass.vert
+++ b/src/shaders/grass.vert
@@ -7,6 +7,15 @@ layout(set = 1, binding = 0) uniform ModelBufferObject {
};
// TODO: Declare vertex shader inputs and outputs
+layout(location = 0) in vec4 vs_v0;
+layout(location = 1) in vec4 vs_v1;
+layout(location = 2) in vec4 vs_v2;
+layout(location = 3) in vec4 vs_up;
+
+// v0 is gl_Position
+layout(location = 0) out vec4 tcs_v1;
+layout(location = 1) out vec4 tcs_v2;
+layout(location = 2) out vec4 tcs_up;
out gl_PerVertex {
vec4 gl_Position;
@@ -14,4 +23,8 @@ out gl_PerVertex {
void main() {
// TODO: Write gl_Position and any other shader outputs
+ gl_Position = vec4(vec3(model * vec4(vs_v0.xyz, 1.0f)), vs_v0.w);
+ tcs_v1 = vec4(vec3(model * vec4(vs_v1.xyz, 1.0f)), vs_v1.w);
+ tcs_v2 = vec4(vec3(model * vec4(vs_v2.xyz, 1.0f)), vs_v2.w);
+ tcs_up = vec4(vec3(model * vec4(vs_up.xyz, 1.0f)), vs_up.w);
}