diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..89942d9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,560 @@
+*.orig
+*.filters
+*.sln
+*.vcxproj
+*.xcodeproj
+build
+
+# Created by https://www.gitignore.io/api/linux,osx,sublimetext,windows,jetbrains,vim,emacs,cmake,c++,cuda,visualstudio,webstorm,eclipse,xcode
+
+### Linux ###
+*~
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+
+### OSX ###
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+
+### SublimeText ###
+# cache files for sublime text
+*.tmlanguage.cache
+*.tmPreferences.cache
+*.stTheme.cache
+
+# workspace files are user-specific
+*.sublime-workspace
+
+# project files should be checked into the repository, unless a significant
+# proportion of contributors will probably not be using SublimeText
+# *.sublime-project
+
+# sftp configuration file
+sftp-config.json
+
+
+### Windows ###
+# Windows image file caches
+Thumbs.db
+ehthumbs.db
+
+# Folder config file
+Desktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+
+### JetBrains ###
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio
+
+*.iml
+
+## Directory-based project format:
+#.idea/
+# if you remove the above rule, at least ignore the following:
+
+# User-specific stuff:
+.idea/workspace.xml
+.idea/tasks.xml
+.idea/dictionaries
+
+# Sensitive or high-churn files:
+.idea/dataSources.ids
+.idea/dataSources.xml
+.idea/sqlDataSources.xml
+.idea/dynamic.xml
+.idea/uiDesigner.xml
+
+# Gradle:
+.idea/gradle.xml
+.idea/libraries
+
+# Mongo Explorer plugin:
+.idea/mongoSettings.xml
+
+## File-based project format:
+*.ipr
+*.iws
+
+## Plugin-specific files:
+
+# IntelliJ
+/out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+
+
+### Vim ###
+[._]*.s[a-w][a-z]
+[._]s[a-w][a-z]
+*.un~
+Session.vim
+.netrwhist
+*~
+
+
+### Emacs ###
+# -*- mode: gitignore; -*-
+*~
+\#*\#
+/.emacs.desktop
+/.emacs.desktop.lock
+*.elc
+auto-save-list
+tramp
+.\#*
+
+# Org-mode
+.org-id-locations
+*_archive
+
+# flymake-mode
+*_flymake.*
+
+# eshell files
+/eshell/history
+/eshell/lastdir
+
+# elpa packages
+/elpa/
+
+# reftex files
+*.rel
+
+# AUCTeX auto folder
+/auto/
+
+# cask packages
+.cask/
+
+
+### CMake ###
+CMakeCache.txt
+CMakeFiles
+CMakeScripts
+Makefile
+cmake_install.cmake
+install_manifest.txt
+
+
+### C++ ###
+# Compiled Object files
+*.slo
+*.lo
+*.o
+*.obj
+
+# Precompiled Headers
+*.gch
+*.pch
+
+# Compiled Dynamic libraries
+*.so
+*.dylib
+*.dll
+
+# Fortran module files
+*.mod
+
+# Compiled Static libraries
+*.lai
+*.la
+*.a
+*.lib
+
+# Executables
+*.exe
+*.out
+*.app
+
+
+### CUDA ###
+*.i
+*.ii
+*.gpu
+*.ptx
+*.cubin
+*.fatbin
+
+
+### VisualStudio ###
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+build/
+bld/
+[Bb]in/
+[Oo]bj/
+
+# Visual Studio 2015 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# DNX
+project.lock.json
+artifacts/
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opensdf
+*.sdf
+*.cachefile
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# TODO: Comment the next line if you want to checkin your web deploy settings
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/packages/*
+# except build/, which is used as an MSBuild target.
+!**/packages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/packages/repositories.config
+
+# Windows Azure Build Output
+csx/
+*.build.csdef
+
+# Windows Store app package directory
+AppPackages/
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
+
+# Others
+ClientBin/
+[Ss]tyle[Cc]op.*
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.pfx
+*.publishsettings
+node_modules/
+orleans.codegen.cs
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+
+### WebStorm ###
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio
+
+*.iml
+
+## Directory-based project format:
+.idea/
+# if you remove the above rule, at least ignore the following:
+
+# User-specific stuff:
+# .idea/workspace.xml
+# .idea/tasks.xml
+# .idea/dictionaries
+
+# Sensitive or high-churn files:
+# .idea/dataSources.ids
+# .idea/dataSources.xml
+# .idea/sqlDataSources.xml
+# .idea/dynamic.xml
+# .idea/uiDesigner.xml
+
+# Gradle:
+# .idea/gradle.xml
+# .idea/libraries
+
+# Mongo Explorer plugin:
+# .idea/mongoSettings.xml
+
+## File-based project format:
+*.ipr
+*.iws
+
+## Plugin-specific files:
+
+# IntelliJ
+/out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+
+
+### Eclipse ###
+*.pydevproject
+.metadata
+.gradle
+bin/
+tmp/
+*.tmp
+*.bak
+*.swp
+*~.nib
+local.properties
+.settings/
+.loadpath
+
+# Eclipse Core
+#.project
+
+# External tool builders
+.externalToolBuilders/
+
+# Locally stored "Eclipse launch configurations"
+#*.launch
+
+# CDT-specific
+#.cproject
+
+# JDT-specific (Eclipse Java Development Tools)
+.classpath
+
+# Java annotation processor (APT)
+.factorypath
+
+# PDT-specific
+.buildpath
+
+# sbteclipse plugin
+.target
+
+# TeXlipse plugin
+.texlipse
+
+
+### Xcode ###
+# Xcode
+#
+# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
+
+## Build generated
+build/
+DerivedData
+
+## Various settings
+*.pbxuser
+!default.pbxuser
+*.mode1v3
+!default.mode1v3
+*.mode2v3
+!default.mode2v3
+*.perspectivev3
+!default.perspectivev3
+xcuserdata
+
+## Other
+*.xccheckout
+*.moved-aside
+*.xcuserstate
diff --git a/README.md b/README.md
index 20ee451..eba84f0 100644
--- a/README.md
+++ b/README.md
@@ -3,10 +3,44 @@ 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)
+* Zhuohao Lin
+ * [LinkedIn](https://www.linkedin.com/in/zhuohao-lin-960b54194/)
+* Tested on: Windows 10, i7-10875H @ 2.30GHz 16GB, NVIDIA Grforce RTX 2060 6GB (personal machine)
-### (TODO: Your README)
+# Overview
+This project implements real-time grass rendering based on a [paper](https://www.cg.tuwien.ac.at/research/publications/2017/JAHRMANN-2017-RRTG/JAHRMANN-2017-RRTG-draft.pdf) about `responsive real-time rendering for general 3D scenes`.
+
-*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.
+# Features
+### `Physics Simulation`
+I apply recovery, gravity and wind forces on the grass in this project. The comparison between using forces and not using forces is shown below.
+Without Forces | With Forces
+:-------------------------:|:-------------------------:
+ | 
+
+### `Optimizations by Culling`
+By culling grass based on orientation, view frustrum and distance, the program gets quite obvious FPS improvement.
+
+Orientation Culling:
+
+
+View Frustrum Culling:
+
+
+Distance Culling:
+
+
+# Performance Analysis
+
+### `How performance of the renderer change with different number of grass blades`
+
+
+
+The FPS drops as the number of grass blades increases. This is expected since there are more data needs to be computed with more grass blades.
+
+### `Improvement from Culling`
+All of the data below are derived under 640 x 480 resolution with 213 grass blades.
+
+
+
+As we can see, all of culling methods contribute decent amount of performance improvements, especially for the orientation culling. This is simply because I set the threshold for culling low.
diff --git a/bin/Release/vulkan_grass_rendering.exe b/bin/Release/vulkan_grass_rendering.exe
index f68db3a..409dfe3 100644
Binary files a/bin/Release/vulkan_grass_rendering.exe and b/bin/Release/vulkan_grass_rendering.exe differ
diff --git a/img/bladeNumber.PNG b/img/bladeNumber.PNG
new file mode 100644
index 0000000..264ab7f
Binary files /dev/null and b/img/bladeNumber.PNG differ
diff --git a/img/culling.PNG b/img/culling.PNG
new file mode 100644
index 0000000..ff33379
Binary files /dev/null and b/img/culling.PNG differ
diff --git a/img/distance.gif b/img/distance.gif
new file mode 100644
index 0000000..3853afd
Binary files /dev/null and b/img/distance.gif differ
diff --git a/img/force.gif b/img/force.gif
new file mode 100644
index 0000000..09ae80a
Binary files /dev/null and b/img/force.gif differ
diff --git a/img/frustrum.gif b/img/frustrum.gif
new file mode 100644
index 0000000..ee86087
Binary files /dev/null and b/img/frustrum.gif differ
diff --git a/img/noForce.PNG b/img/noForce.PNG
new file mode 100644
index 0000000..51a0ff5
Binary files /dev/null and b/img/noForce.PNG differ
diff --git a/img/orientation.gif b/img/orientation.gif
new file mode 100644
index 0000000..33b64e9
Binary files /dev/null and b/img/orientation.gif differ
diff --git a/img/overview.gif b/img/overview.gif
new file mode 100644
index 0000000..65b1d8d
Binary files /dev/null and b/img/overview.gif differ
diff --git a/src/Blades.cpp b/src/Blades.cpp
index 80e3d76..230d65c 100644
--- a/src/Blades.cpp
+++ b/src/Blades.cpp
@@ -44,8 +44,8 @@ Blades::Blades(Device* device, VkCommandPool commandPool, float planeDim) : Mode
indirectDraw.firstVertex = 0;
indirectDraw.firstInstance = 0;
- BufferUtils::CreateBufferFromData(device, commandPool, blades.data(), NUM_BLADES * sizeof(Blade), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, bladesBuffer, bladesBufferMemory);
- BufferUtils::CreateBuffer(device, NUM_BLADES * sizeof(Blade), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, culledBladesBuffer, culledBladesBufferMemory);
+ BufferUtils::CreateBufferFromData(device, commandPool, blades.data(), NUM_BLADES * sizeof(Blade), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, bladesBuffer, bladesBufferMemory);
+ BufferUtils::CreateBuffer(device, NUM_BLADES * sizeof(Blade), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, culledBladesBuffer, culledBladesBufferMemory);
BufferUtils::CreateBufferFromData(device, commandPool, &indirectDraw, sizeof(BladeDrawIndirect), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT, numBladesBuffer, numBladesBufferMemory);
}
diff --git a/src/Renderer.cpp b/src/Renderer.cpp
index b445d04..5698ac4 100644
--- a/src/Renderer.cpp
+++ b/src/Renderer.cpp
@@ -140,7 +140,7 @@ void Renderer::CreateCameraDescriptorSetLayout() {
layoutInfo.pBindings = bindings.data();
if (vkCreateDescriptorSetLayout(logicalDevice, &layoutInfo, nullptr, &cameraDescriptorSetLayout) != VK_SUCCESS) {
- throw std::runtime_error("Failed to create descriptor set layout");
+ throw std::runtime_error("Failed to create camera descriptor set layout");
}
}
@@ -168,7 +168,7 @@ void Renderer::CreateModelDescriptorSetLayout() {
layoutInfo.pBindings = bindings.data();
if (vkCreateDescriptorSetLayout(logicalDevice, &layoutInfo, nullptr, &modelDescriptorSetLayout) != VK_SUCCESS) {
- throw std::runtime_error("Failed to create descriptor set layout");
+ throw std::runtime_error("Failed to create model descriptor set layout");
}
}
@@ -190,7 +190,7 @@ void Renderer::CreateTimeDescriptorSetLayout() {
layoutInfo.pBindings = bindings.data();
if (vkCreateDescriptorSetLayout(logicalDevice, &layoutInfo, nullptr, &timeDescriptorSetLayout) != VK_SUCCESS) {
- throw std::runtime_error("Failed to create descriptor set layout");
+ throw std::runtime_error("Failed to create time descriptor set layout");
}
}
@@ -198,6 +198,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 bladesBinding{};
+ bladesBinding.binding = 0;
+ bladesBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
+ bladesBinding.descriptorCount = 1;
+ bladesBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
+ bladesBinding.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 = { bladesBinding, 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 +250,9 @@ void Renderer::CreateDescriptorPool() {
{ VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER , 1 },
// TODO: Add any additional types and counts of descriptors you will need to allocate
+
+ // blades, culledBlades, numBlades (compute)
+ { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 3 * static_cast(scene->GetBlades().size()) }
};
VkDescriptorPoolCreateInfo poolInfo = {};
@@ -320,6 +357,43 @@ 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 descriptor 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 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(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr);
}
void Renderer::CreateTimeDescriptorSet() {
@@ -360,6 +434,73 @@ 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 bladesBufferInfo = {};
+ bladesBufferInfo.buffer = scene->GetBlades()[i]->GetBladesBuffer();
+ bladesBufferInfo.offset = 0;
+ bladesBufferInfo.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 = &bladesBufferInfo;
+ 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 +858,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 = {};
@@ -802,7 +943,7 @@ void Renderer::CreateFrameResources() {
// CREATE FRAMEBUFFERS
framebuffers.resize(swapChain->GetCount());
- for (size_t i = 0; i < swapChain->GetCount(); i++) {
+ for (uint32_t i = 0; i < swapChain->GetCount(); i++) {
std::vector attachments = {
imageViews[i],
depthImageView
@@ -825,7 +966,7 @@ void Renderer::CreateFrameResources() {
}
void Renderer::DestroyFrameResources() {
- for (size_t i = 0; i < imageViews.size(); i++) {
+ for (uint32_t i = 0; i < imageViews.size(); i++) {
vkDestroyImageView(logicalDevice, imageViews[i], nullptr);
}
@@ -833,7 +974,7 @@ void Renderer::DestroyFrameResources() {
vkFreeMemory(logicalDevice, depthImageMemory, nullptr);
vkDestroyImage(logicalDevice, depthImage, nullptr);
- for (size_t i = 0; i < framebuffers.size(); i++) {
+ for (uint32_t i = 0; i < framebuffers.size(); i++) {
vkDestroyFramebuffer(logicalDevice, framebuffers[i], nullptr);
}
}
@@ -884,6 +1025,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, static_cast(NUM_BLADES / WORKGROUP_SIZE), 1, 1);
+ }
// ~ End recording ~
if (vkEndCommandBuffer(computeCommandBuffer) != VK_SUCCESS) {
@@ -906,7 +1051,7 @@ void Renderer::RecordCommandBuffers() {
}
// Start command buffer recording
- for (size_t i = 0; i < commandBuffers.size(); i++) {
+ for (uint32_t i = 0; i < commandBuffers.size(); i++) {
VkCommandBufferBeginInfo beginInfo = {};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT;
@@ -976,13 +1121,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
@@ -1057,6 +1203,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..36caa9b 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 grassDescriptorSets;
+ std::vector computeDescriptorSets;
VkPipelineLayout graphicsPipelineLayout;
VkPipelineLayout grassPipelineLayout;
diff --git a/src/shaders/compute.comp b/src/shaders/compute.comp
index 0fd0224..f66525f 100644
--- a/src/shaders/compute.comp
+++ b/src/shaders/compute.comp
@@ -1,6 +1,11 @@
#version 450
#extension GL_ARB_separate_shader_objects : enable
+#define APPLY_PHYSICS 1
+#define ORIENTATION_TEST 1
+#define FRUSTRUM_TEST 1
+#define DISTANCE_TEST 1
+
#define WORKGROUP_SIZE 32
layout(local_size_x = WORKGROUP_SIZE, local_size_y = 1, local_size_z = 1) in;
@@ -26,31 +31,182 @@ struct Blade {
// 2. Write out the culled blades
// 3. Write the total number of blades remaining
+layout(set = 2, binding = 0) buffer Blades {
+ Blade blades[];
+} blades;
+
+layout(set = 2, binding = 1) buffer CulledBlades {
+ Blade culledBlades[];
+} culledBlades;
+
// 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);
}
+
+
+vec2 random2(vec2 p) {
+ return fract(sin(vec2(dot(p, vec2(127.1f, 311.7f)), dot(p, vec2(269.5f,183.3f)))) * 43758.5453f);
+}
+
+float surflet(vec2 p, vec2 gridPoint) {
+ // Compute the distance between p and the grid point along each axis, and warp it with a
+ // quintic function so we can smooth our cells
+ vec2 t2 = abs(p - gridPoint);
+ // vec2 t = vec2(1.f) - 6.f * pow(t2, vec2(5.f)) + 15.f * pow(t2, vec2(4.f)) - 10.f * pow(t2, vec2(3.f));
+ vec2 t;
+ t.x = 1.0f - 6.0f*t2.x*t2.x*t2.x*t2.x*t2.x + 15.0f*t2.x*t2.x*t2.x*t2.x - 10.0f*t2.x*t2.x*t2.x;
+ t.y = 1.0f - 6.0f*t2.y*t2.y*t2.y*t2.y*t2.y + 15.0f*t2.y*t2.y*t2.y*t2.y - 10.0f*t2.y*t2.y*t2.y;
+ // Get the random vector for the grid point (assume we wrote a function random2
+ // that returns a vec2 in the range [0, 1])
+ vec2 gradient = random2(gridPoint) * 2.f - vec2(1.f,1.f);
+ // Get the vector from the grid point to P
+ vec2 diff = p - gridPoint;
+ // Get the value of our height field by dotting grid->P with our gradient
+ float height = dot(diff, gradient);
+ // Scale our height field (i.e. reduce it) by our polynomial falloff function
+ return height * t.x * t.y;
+}
+
+float perlin2D(vec2 uv) {
+ float surfletSum = 0.f;
+ // Iterate over the four integer corners surrounding uv
+ for(int dx = 0; dx <= 1; ++dx) {
+ for(int dy = 0; dy <= 1; ++dy) {
+ surfletSum += surflet(uv, floor(uv) + vec2(dx, dy));
+ }
+ }
+ return surfletSum;
+}
+
+
+vec3 windFunction(vec3 pos, float time) {
+ vec2 uv = pos.xz;
+
+ vec3 windDir = vec3(perlin2D(uv * 5.f + time * 0.5f),
+ 0.f,
+ perlin2D(uv * 5.f + 100.f + time * 0.5f));
+
+ return windDir;
+}
+
+
+bool inFrustrum(vec3 point) {
+ float tolerance = 0.1f;
+ vec4 projectedPoint = camera.proj * camera.view * vec4(point, 1.f);
+ float h = projectedPoint.w + tolerance;
+ bool isInFrustrum = inBounds(projectedPoint.x, h) && inBounds(projectedPoint.y, h) && inBounds(projectedPoint.z, h);
+ return isInFrustrum;
+}
+
+
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 blade = blades.blades[gl_GlobalInvocationID.x];
+
+ vec3 v0 = blade.v0.xyz;
+ vec3 v1 = blade.v1.xyz;
+ vec3 v2 = blade.v2.xyz;
+ vec3 up = blade.up.xyz;
+
+ float orientation = blade.v0.w;
+ float height = blade.v1.w;
+ float width = blade.v2.w;
+ float stiffness = blade.up.w;
+
// TODO: Apply forces on every blade and update the vertices in the buffer
+#if APPLY_PHYSICS
+ // recovery
+ vec3 initialV2 = v0 + up * height;
+ vec3 recoveryForce = stiffness * (initialV2 - v2);
+
+ // gravity
+ vec3 right = vec3(cos(orientation), 0.0, sin(orientation));
+ vec3 front = cross(up, right);
+
+ vec3 ge = vec3(0.f, -9.81f, 0.f);
+ vec3 gf = 0.25 * length(ge) * front;
+ vec3 gravity = ge + gf;
+
+ // wind
+ vec3 wind = windFunction(v0, totalTime);
+ float fd = 1 - abs(dot(normalize(wind), normalize(v2 - v0)));
+ float fr = dot(v2 - v0, up) / height;
+ float strength = 5.f;
+ vec3 windForce = wind * fd * fr * strength;;
+
+
+ vec3 totalForce = recoveryForce + gravity + windForce;
+
+ v2 = v2 + deltaTime * totalForce;
+
+ // State Validation
+ v2 = v2 - up * min(dot(up, v2 - v0), 0.f);
+
+ float lProj = length(v2 - v0 - up*dot(v2 - v0, up));
+ v1 = v0 + height * up *max(1.f - lProj / height, 0.05f * max(lProj / height, 1.f));
+
+ float L0 = distance(v0, v2);
+ float L1 = distance(v1, v0) + distance(v1, v2);
+ float n = 2.f;
+ float L = (2.f * L0 + (n - 1.f) * L1) / (n + 1.f);
+ float r = height / L;
+ vec3 v1Corr = v0 + r * (v1 - v0);
+ vec3 v2Corr = v1Corr + r * (v2 - v1);
+#endif
+
+#if !APPLY_PHYSICS
+ vec3 v1Corr = v1;
+ vec3 v2Corr = v2;
+#endif
+
+ blade.v1.xyz = v1Corr;
+ blade.v2.xyz = v2Corr;
+ blades.blades[gl_GlobalInvocationID.x] = blade;
+
// 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 camPos = vec3(inverse(camera.view) * vec4(0.f, 0.f, 0.f, 1.f));
+#if ORIENTATION_TEST
+ // orientation test
+ vec3 viewDir = normalize(camPos - v0);
+ vec3 bladeDir = vec3(cos(orientation), 0.f, sin(orientation));
+ if (abs(dot(viewDir, bladeDir)) > 0.3f) return;
+#endif
+
+#if FRUSTRUM_TEST
+ // view frustum test
+ // center point
+ vec3 m = 0.25f * v0 + 0.5f * v1 + 0.25f * v2;
+ if ( !(inFrustrum(v0) || inFrustrum(m) || inFrustrum(v2)) ) return;
+#endif
+
+#if DISTANCE_TEST
+ // distance test
+ float dProj = length(v0 - camPos - up * dot(v0 - camPos, up));
+ float distMax = 15.f;
+ int distLevel = 10;
+ if (gl_GlobalInvocationID.x % distLevel < int(floor(distLevel * (1.f - dProj / distMax)))) return;
+#endif
+
+ culledBlades.culledBlades[atomicAdd(numBlades.vertexCount, 1)] = blade;
}
diff --git a/src/shaders/grass.frag b/src/shaders/grass.frag
index c7df157..5162719 100644
--- a/src/shaders/grass.frag
+++ b/src/shaders/grass.frag
@@ -7,11 +7,17 @@ layout(set = 0, binding = 0) uniform CameraBufferObject {
} camera;
// TODO: Declare fragment shader inputs
+layout(location = 0) in vec4 pos;
+layout(location = 1) in vec4 nor;
+layout(location = 2) in vec2 uv;
layout(location = 0) out vec4 outColor;
void main() {
// TODO: Compute fragment color
-
- outColor = vec4(1.0);
+ vec3 darkGreen = vec3(0.11f, 0.25f, 0.01f);
+ vec3 lightGreen = vec3(0.6f, 0.97f, 0.39f);
+ vec3 grassColor = mix(darkGreen, lightGreen, uv.y);
+
+ outColor = vec4(grassColor, 1.f);
}
diff --git a/src/shaders/grass.tesc b/src/shaders/grass.tesc
index f9ffd07..5998874 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[];
+layout(location = 1) in vec4 v1[];
+layout(location = 2) in vec4 v2[];
+layout(location = 3) in vec4 up[];
+
+layout(location = 0) out vec4 v0Out[];
+layout(location = 1) out vec4 v1Out[];
+layout(location = 2) out vec4 v2Out[];
+layout(location = 3) out vec4 upOut[];
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
+ v0Out[gl_InvocationID] = v0[gl_InvocationID];
+ v1Out[gl_InvocationID] = v1[gl_InvocationID];
+ v2Out[gl_InvocationID] = v2[gl_InvocationID];
+ upOut[gl_InvocationID] = up[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] = 8.0;
+ gl_TessLevelInner[1] = 8.0;
+ gl_TessLevelOuter[0] = 8.0;
+ gl_TessLevelOuter[1] = 8.0;
+ gl_TessLevelOuter[2] = 8.0;
+ gl_TessLevelOuter[3] = 8.0;
}
diff --git a/src/shaders/grass.tese b/src/shaders/grass.tese
index 751fff6..893cc55 100644
--- a/src/shaders/grass.tese
+++ b/src/shaders/grass.tese
@@ -9,10 +9,46 @@ layout(set = 0, binding = 0) uniform CameraBufferObject {
} camera;
// TODO: Declare tessellation evaluation shader inputs and outputs
+layout(location = 0) in vec4 v0[];
+layout(location = 1) in vec4 v1[];
+layout(location = 2) in vec4 v2[];
+layout(location = 3) in vec4 up[];
+
+layout(location = 0) out vec4 pos;
+layout(location = 1) out vec4 nor;
+layout(location = 2) out vec2 uv;
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
+
+ // De Casteljau¡¯s algorithm
+
+ // bitangent
+ vec3 t1 = vec3(cos(v0[0].w), 0.f, sin(v0[0].w));
+
+ vec3 a = vec3(v0[0] + v * (v1[0] - v0[0]));
+ vec3 b = vec3(v1[0] + v * (v2[0] - v1[0]));
+ vec3 c = a + v * (b - a);
+ vec3 c0 = c - v2[0].w * t1;
+ vec3 c1 = c + v2[0].w * t1;
+
+ // tangent
+ vec3 t0 = normalize(b - a);
+ // normal
+ vec3 n = normalize(cross(t0, t1));
+
+ // vertex position (triangle-tip)
+ float threshold = 0.2f; // This value is between 0 and 1
+ float t = 0.5f + (u - 0.5f) * (1.f - max(v - threshold, 0.f) / (1.f - threshold));
+ vec3 p = mix(c0, c1, t);
+
+ pos = vec4(p, 1.f);
+ nor = vec4(n, 0.f);
+ uv = vec2(u, v);
+
+ gl_Position = camera.proj * camera.view * pos;
+
}
diff --git a/src/shaders/grass.vert b/src/shaders/grass.vert
index db9dfe9..b41035c 100644
--- a/src/shaders/grass.vert
+++ b/src/shaders/grass.vert
@@ -7,6 +7,16 @@ layout(set = 1, binding = 0) uniform ModelBufferObject {
};
// TODO: Declare vertex shader inputs and outputs
+layout(location = 0) in vec4 v0;
+layout(location = 1) in vec4 v1;
+layout(location = 2) in vec4 v2;
+layout(location = 3) in vec4 up;
+
+// Outputs are passed to tessellation shaders
+layout(location = 0) out vec4 v0Out;
+layout(location = 1) out vec4 v1Out;
+layout(location = 2) out vec4 v2Out;
+layout(location = 3) out vec4 upOut;
out gl_PerVertex {
vec4 gl_Position;
@@ -14,4 +24,10 @@ out gl_PerVertex {
void main() {
// TODO: Write gl_Position and any other shader outputs
+
+ gl_Position = model * vec4(v0.xyz, 1.f);
+ v0Out = vec4((model * vec4(v0.xyz, 1.f)).xyz, v0.w);
+ v1Out = vec4((model * vec4(v1.xyz, 1.f)).xyz, v1.w);
+ v2Out = vec4((model * vec4(v2.xyz, 1.f)).xyz, v2.w);
+ upOut = vec4((model * vec4(up.xyz, 0.f)).xyz, up.w);
}