diff --git a/CMakeLists.txt b/CMakeLists.txt index 36d5dcb..44e7a61 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/README.md b/README.md index 20ee451..039ab37 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/bin/Release/vulkan_grass_rendering.exe b/bin/Release/vulkan_grass_rendering.exe index f68db3a..f49f281 100644 Binary files a/bin/Release/vulkan_grass_rendering.exe and b/bin/Release/vulkan_grass_rendering.exe differ diff --git a/external/GLFW/CMakeLists.txt b/external/GLFW/CMakeLists.txt index 56c1f38..3ee137a 100644 --- a/external/GLFW/CMakeLists.txt +++ b/external/GLFW/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 2.8.12) +cmake_minimum_required(VERSION 3.5) project(GLFW C) @@ -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") diff --git a/img/analysis_chart.png b/img/analysis_chart.png new file mode 100644 index 0000000..190ecc5 Binary files /dev/null and b/img/analysis_chart.png differ diff --git a/img/analysis_table.png b/img/analysis_table.png new file mode 100644 index 0000000..9383445 Binary files /dev/null and b/img/analysis_table.png differ diff --git a/img/basic.png b/img/basic.png new file mode 100644 index 0000000..322f733 Binary files /dev/null and b/img/basic.png differ diff --git a/img/culling.gif b/img/culling.gif new file mode 100644 index 0000000..57bc661 Binary files /dev/null and b/img/culling.gif differ diff --git a/img/debug.png b/img/debug.png new file mode 100644 index 0000000..56ac08f Binary files /dev/null and b/img/debug.png differ diff --git a/img/demo.gif b/img/demo.gif new file mode 100644 index 0000000..aac5c59 Binary files /dev/null and b/img/demo.gif differ diff --git a/src/Blades.h b/src/Blades.h index 9bd1eed..b438e24 100644 --- a/src/Blades.h +++ b/src/Blades.h @@ -4,7 +4,7 @@ #include #include "Model.h" -constexpr static unsigned int NUM_BLADES = 1 << 13; +constexpr static unsigned int NUM_BLADES = 1 << 15; constexpr static float MIN_HEIGHT = 1.3f; constexpr static float MAX_HEIGHT = 2.5f; constexpr static float MIN_WIDTH = 0.1f; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index aea02fe..6e153c3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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 @@ -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) diff --git a/src/Renderer.cpp b/src/Renderer.cpp index b445d04..d0556a2 100644 --- a/src/Renderer.cpp +++ b/src/Renderer.cpp @@ -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(); @@ -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 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(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() { @@ -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(scene->GetBlades().size() * 3)} }; VkDescriptorPoolCreateInfo poolInfo = {}; poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; poolInfo.poolSizeCount = static_cast(poolSizes.size()); - poolInfo.pPoolSizes = poolSizes.data(); + poolInfo.pPoolSizes = poolSizes.data(); poolInfo.maxSets = 5; if (vkCreateDescriptorPool(logicalDevice, &poolInfo, nullptr, &descriptorPool) != VK_SUCCESS) { @@ -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(grassDescriptorSets.size()); + allocInfo.pSetLayouts = layouts; + + // Allocate descriptor sets + if (vkAllocateDescriptorSets(logicalDevice, &allocInfo, grassDescriptorSets.data()) != VK_SUCCESS) { + throw std::runtime_error("Failed to allocate descriptor set"); + } + + std::vector descriptorWrites(grassDescriptorSets.size()); + + for (uint32_t i = 0; i < scene->GetBlades().size(); ++i) { + 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 +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(computeDescriptorSets.size()); + allocInfo.pSetLayouts = layouts; + + // Allocate descriptor sets + if (vkAllocateDescriptorSets(logicalDevice, &allocInfo, computeDescriptorSets.data()) != VK_SUCCESS) { + throw std::runtime_error("Failed to allocate descriptor set"); + } + + std::vector descriptorWrites(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(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr); } void Renderer::CreateGraphicsPipeline() { @@ -717,7 +860,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,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"); @@ -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 diff --git a/src/Renderer.h b/src/Renderer.h index 95e025f..0f7ecb9 100644 --- a/src/Renderer.h +++ b/src/Renderer.h @@ -56,12 +56,15 @@ class Renderer { VkDescriptorSetLayout cameraDescriptorSetLayout; VkDescriptorSetLayout modelDescriptorSetLayout; VkDescriptorSetLayout timeDescriptorSetLayout; + VkDescriptorSetLayout computeDescriptorSetLayout; VkDescriptorPool descriptorPool; VkDescriptorSet cameraDescriptorSet; std::vector modelDescriptorSets; VkDescriptorSet timeDescriptorSet; + std::vector computeDescriptorSets; + std::vector grassDescriptorSets; VkPipelineLayout graphicsPipelineLayout; VkPipelineLayout grassPipelineLayout; diff --git a/src/shaders/compute.comp b/src/shaders/compute.comp index 0fd0224..6cba9fe 100644 --- a/src/shaders/compute.comp +++ b/src/shaders/compute.comp @@ -2,6 +2,15 @@ #extension GL_ARB_separate_shader_objects : enable #define WORKGROUP_SIZE 32 +#define GRAVITY -100.0 +#define WIND_DIR vec3(1.0, 0.0, 0.3) +#define WIND_SPEED 1.5 +#define WIND_AMP 1000.0 +#define ORIENTATION_THRESHOLD 0.9 +#define FRUSTUM_TOLERANCE 0.1 +#define DISTANCE_NUMBUCKETS 5 +#define DISTANCE_MAX 30.0 + layout(local_size_x = WORKGROUP_SIZE, local_size_y = 1, local_size_z = 1) in; layout(set = 0, binding = 0) uniform CameraBufferObject { @@ -15,42 +24,192 @@ layout(set = 1, binding = 0) uniform Time { }; struct Blade { - vec4 v0; - vec4 v1; - vec4 v2; - vec4 up; + vec4 v0; // x,y,z = position, w = orientation + vec4 v1; // x,y,z = control point, w = height + vec4 v2; // x,y,z = tip position, w = width + vec4 up; // x,y,z = up vector, w = stiffness }; // TODO: Add bindings to: // 1. Store the input blades + +layout(set = 2, binding = 0) buffer InputBlades { + Blade blades[]; +} inputBlades; + // 2. Write out the culled blades + +layout(set = 2, binding = 1) buffer CulledBlades { + Blade blades[]; +} culledBlades; + // 3. Write the total number of blades remaining // The project is using vkCmdDrawIndirect to use a buffer as the arguments for a draw call // This is sort of an advanced feature so we've showed you what this buffer should look like -// -// layout(set = ???, binding = ???) buffer NumBlades { -// uint vertexCount; // Write the number of blades remaining here -// uint instanceCount; // = 1 -// uint firstVertex; // = 0 -// uint firstInstance; // = 0 -// } numBlades; + +layout(set = 2, binding = 2) buffer NumBlades { + uint vertexCount; // Write the number of blades remaining here + uint instanceCount; // = 1 + uint firstVertex; // = 0 + uint firstInstance; // = 0 +} numBlades; bool inBounds(float value, float bounds) { return (value >= -bounds) && (value <= bounds); } +// ----------- Helper functions ------------ +// Compute 2D noise +float hash(vec2 p) +{ + return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); +} + +float noise(vec2 p) +{ + vec2 i = floor(p); + vec2 f = fract(p); + float a = hash(i); + float b = hash(i + vec2(1.0, 0.0)); + float c = hash(i + vec2(0.0, 1.0)); + float d = hash(i + vec2(1.0, 1.0)); + vec2 u = f * f * (3.0 - 2.0 * f); + return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y; +} + +// Compute wind +vec3 computeWind(vec3 windDir, float windSpeed, float windAmp, vec3 pos, float totalTime) { + float phase = dot(pos.xz - windDir.xz * totalTime * 0.5, vec2(0.05, 0.08)); + + float w1 = sin(phase * 1.0 + totalTime * windSpeed); + float w2 = cos(phase * 2.3 + totalTime * windSpeed * 1.3); + float w3 = sin(phase * 0.7 - totalTime * windSpeed * 0.7); + + float strength = windAmp * (w1 + 0.5 * w2 + 0.3 * w3); + + float n = noise(pos.xz + totalTime); + n = n * 2.0 - 1.0; + strength += n; + + return windDir * strength; +} + +// Orientation culling +bool cullByOrientation(vec3 v0, vec3 forward, float threshold, vec3 cameraPos) { + vec3 viewVec = normalize(v0 - cameraPos); + viewVec.y = 0.0; + viewVec = normalize(viewVec); + float facing = abs(dot(forward, viewVec)); + + return facing < threshold; +} + +// Test if the point is outside view frustum +bool isOutsideFrustum(vec3 pos, float tolerance) +{ + vec4 clip = camera.proj * camera.view * vec4(pos, 1.0); + vec3 ndc = clip.xyz / clip.w; + float bound = 1.0 + tolerance; + return ndc.x < -bound || ndc.x > bound || + ndc.y < -bound || ndc.y > bound || + ndc.z < -bound || ndc.z > bound; +} + +// View frusturm culling +bool cullByFrustum(vec3 v0, vec3 v1, vec3 v2, float tolerance) { + vec3 m = 0.25 * v0 + 0.5 * v1 + 0.25 * v2; + bool v0Out = isOutsideFrustum(v0, tolerance); + bool v2Out = isOutsideFrustum(v2, tolerance); + bool mOut = isOutsideFrustum(m, tolerance); + + return v0Out && v2Out && mOut; +} + +// Distance culling +bool cullByDistance(vec3 v0, vec3 up, float d_max, uint n, uint idx, vec3 cameraPos) { + float d = length(v0 - cameraPos); + if (d > d_max) return true; + + float d_proj = length(v0 - cameraPos - up * dot(v0 - cameraPos, up)); + return (mod(idx, n) < floor(n * (1.0 - d_proj / d_max))); +} + +// ----------------------------------------- + void main() { + uint idx = gl_GlobalInvocationID.x; // Reset the number of blades to 0 - if (gl_GlobalInvocationID.x == 0) { - // numBlades.vertexCount = 0; + if (idx == 0) { + numBlades.vertexCount = 0; } barrier(); // Wait till all threads reach this point + if (idx >= inputBlades.blades.length()) return; + // TODO: Apply forces on every blade and update the vertices in the buffer + Blade inBlade = inputBlades.blades[idx]; + vec3 v0 = inBlade.v0.xyz; + vec3 v1 = inBlade.v1.xyz; + vec3 v2 = inBlade.v2.xyz; + vec3 up = inBlade.up.xyz; + float orientation = inBlade.v0.w; + float height = inBlade.v1.w; + float stiffness = inBlade.up.w; + + // Apply gravity + vec3 forward = normalize(vec3(cos(orientation), 0.0, sin(orientation))); + vec3 gE = vec3(0.0, GRAVITY, 0.0); + vec3 gF = 0.25 * length(gE) * forward; + vec3 gravity = gE + gF; + + // Apply recovery + vec3 iv2 = v0 + height * up; + vec3 recovery = (iv2 - inBlade.v2.xyz) * stiffness; + + // Apply wind + vec3 windDir = normalize(WIND_DIR); + vec3 windInfluence = computeWind(windDir, WIND_SPEED, WIND_AMP, v0, totalTime); + float fd = 1 - abs(dot(normalize(v2 - v0), normalize(windDir))); // directional alignment + float fr = dot(v2 - v0, up) / height; // height ratio + vec3 wind = windInfluence * fd * fr; + + v2 += (gravity + recovery + wind) * 0.0005; + + // Validation + // - Make sure the tip of the blade doesn't go below the ground + v2 = v2 - up * min(dot(up, v2 - v0), 0.0); + + // - Calculate v1 accordingly + float l_proj = length(v2 - v0 - up * dot(up, v2 - v0)); + v1 = v0 + height * up * max(1.0 - l_proj / height, 0.05 * max(l_proj / height, 1)); + + // - Keep the blade length constant + float L0 = length(v2 - v0); + float L1 = length(v1 - v0) + length(v2 - v1); + float L = (2 * L0 + L1) / 3.0; + float r = height / L; + v1 = v0 + r * (v1 - v0); + v2 = v1 + r * (v2 - v1); + + inBlade.v1.xyz = v1; + inBlade.v2.xyz = v2; // TODO: Cull blades that are too far away or not in the camera frustum and write them // to the culled blades buffer // Note: to do this, you will need to use an atomic operation to read and update numBlades.vertexCount // You want to write the visible blades to the buffer without write conflicts between threads + + vec3 cameraPos = inverse(camera.view)[3].xyz; + // Orientation culling + if (cullByOrientation(v0, forward, ORIENTATION_THRESHOLD, cameraPos)) return; + + // View frustum culling + if (cullByFrustum(v0, v1, v2, FRUSTUM_TOLERANCE)) return; + + // Distance culling + if (cullByDistance(v0, up, DISTANCE_MAX, DISTANCE_NUMBUCKETS, idx, cameraPos)) return; + + uint dst = atomicAdd(numBlades.vertexCount, 1); + culledBlades.blades[dst] = inBlade; } diff --git a/src/shaders/grass.frag b/src/shaders/grass.frag index c7df157..7c80c2d 100644 --- a/src/shaders/grass.frag +++ b/src/shaders/grass.frag @@ -7,11 +7,15 @@ layout(set = 0, binding = 0) uniform CameraBufferObject { } camera; // TODO: Declare fragment shader inputs +layout(location = 0) in float inHeight; layout(location = 0) out vec4 outColor; void main() { // TODO: Compute fragment color + vec3 base = vec3(0.11, 0.388, 0.102); + vec3 tip = vec3(0.725, 1, 0.267); + vec3 color = mix(base, tip, inHeight); - outColor = vec4(1.0); + outColor = vec4(color, 1.0); } diff --git a/src/shaders/grass.tesc b/src/shaders/grass.tesc index f9ffd07..2e16f04 100644 --- a/src/shaders/grass.tesc +++ b/src/shaders/grass.tesc @@ -9,18 +9,31 @@ layout(set = 0, binding = 0) uniform CameraBufferObject { } camera; // TODO: Declare tessellation control shader inputs and outputs +layout(location = 0) in vec4 v0_in[]; +layout(location = 1) in vec4 v1_in[]; +layout(location = 2) in vec4 v2_in[]; +layout(location = 3) in vec4 up_in[]; + +layout(location = 0) out vec4 v0_out[]; +layout(location = 1) out vec4 v1_out[]; +layout(location = 2) out vec4 v2_out[]; +layout(location = 3) out vec4 up_out[]; 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 + v0_out[gl_InvocationID] = v0_in[gl_InvocationID]; + v1_out[gl_InvocationID] = v1_in[gl_InvocationID]; + v2_out[gl_InvocationID] = v2_in[gl_InvocationID]; + up_out[gl_InvocationID] = up_in[gl_InvocationID]; // TODO: Set level of tesselation - // gl_TessLevelInner[0] = ??? - // gl_TessLevelInner[1] = ??? - // gl_TessLevelOuter[0] = ??? - // gl_TessLevelOuter[1] = ??? - // gl_TessLevelOuter[2] = ??? - // gl_TessLevelOuter[3] = ??? + gl_TessLevelInner[0] = 5.0; + gl_TessLevelInner[1] = 5.0; + gl_TessLevelOuter[0] = 5.0; + gl_TessLevelOuter[1] = 5.0; + gl_TessLevelOuter[2] = 5.0; + gl_TessLevelOuter[3] = 5.0; } diff --git a/src/shaders/grass.tese b/src/shaders/grass.tese index 751fff6..765fb80 100644 --- a/src/shaders/grass.tese +++ b/src/shaders/grass.tese @@ -9,10 +9,44 @@ layout(set = 0, binding = 0) uniform CameraBufferObject { } camera; // TODO: Declare tessellation evaluation shader inputs and outputs +layout(location = 0) in vec4 v0_in[]; +layout(location = 1) in vec4 v1_in[]; +layout(location = 2) in vec4 v2_in[]; +layout(location = 3) in vec4 up_in[]; + +layout(location = 0) out float v_height; + +out gl_PerVertex { + vec4 gl_Position; +}; 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 + vec4 v0 = v0_in[0]; + vec4 v1 = v1_in[0]; + vec4 v2 = v2_in[0]; + vec3 up = normalize(up_in[0].xyz); + + float orientation = v0.w; + float height = v1.w; + float width = v2.w; + + vec3 forward = vec3(cos(orientation), 0.0, sin(orientation)); + vec3 right = normalize(cross(up, forward)); + + vec3 p = pow(1.0 - v, 2.0) * v0.xyz + + 2.0 * (1.0 - v) * v * v1.xyz + + pow(v, 2.0) * v2.xyz; + + float halfWidth = width * 0.5 * (1.0 - v); + vec3 offset = right * (u - 0.5) * 2.0 * halfWidth; + vec3 worldPos = p + offset; + + vec4 viewPos = camera.view * vec4(worldPos, 1.0); + gl_Position = camera.proj * viewPos; + + v_height = v; } diff --git a/src/shaders/grass.vert b/src/shaders/grass.vert index db9dfe9..13beeda 100644 --- a/src/shaders/grass.vert +++ b/src/shaders/grass.vert @@ -7,11 +7,31 @@ layout(set = 1, binding = 0) uniform ModelBufferObject { }; // TODO: Declare vertex shader inputs and outputs +layout(location = 0) in vec4 v0_in; +layout(location = 1) in vec4 v1_in; +layout(location = 2) in vec4 v2_in; +layout(location = 3) in vec4 up_in; out gl_PerVertex { vec4 gl_Position; }; +layout(location = 0) out vec4 v0_out; +layout(location = 1) out vec4 v1_out; +layout(location = 2) out vec4 v2_out; +layout(location = 3) out vec4 up_out; + void main() { // TODO: Write gl_Position and any other shader outputs + v0_out = model * vec4(v0_in.xyz, 1.0); + v1_out = model * vec4(v1_in.xyz, 1.0); + v2_out = model * vec4(v2_in.xyz, 1.0); + up_out.xyz = normalize(mat3(model) * up_in.xyz); + + v0_out.w = v0_in.w; + v1_out.w = v1_in.w; + v2_out.w = v2_in.w; + up_out.w = up_in.w; + + gl_Position = v0_out; // placeholder }