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/CMakeLists.txt b/CMakeLists.txt index 860a279..38c2cf0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,7 +18,7 @@ ELSE(USE_D2D_WSI) find_package(XCB REQUIRED) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DVK_USE_PLATFORM_XCB_KHR") ENDIF(USE_D2D_WSI) - # Todo : android? + # : android? ENDIF(WIN32) # Set preprocessor defines diff --git a/README.md b/README.md index 20ee451..0f6c83c 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,86 @@ 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) +* Evan S +* Tested on: Strix G15: Windows 10, Ryzen 7 4800H @ 2.9 GHz, GTX 3050 (Laptop) -### (TODO: Your README) +## Overview -*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 is a grass rendering project 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). Each blade of grass is modeled as a 2nd degree [Bezier curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve), which is subject to gravity, restorative, and wind-based forces. See the paper for further information. + +To achieve optimal performance, the project is implemented in Vulkan. + +### Features + +#### Forces +The static grass is enabled through Vulkan overall, but forces in particular are calculated in the compute shader, and applied to a weight for the Bezier informing the grass tesselation. + +Before any forces, the grass is a static bunch that sticks straight up, and looks somewhat unusual. +

+ +

+ +##### Gravity + + +To the left is only the force of gravity on the blades of grass; on a technical level, it is applied to the tip of the blade. As expected, without any recovery force, they immediately collapse to the ground. Note that the gif uses a lower magnitude of gravity, to better show the blades as they collapse. +
+ +##### Recovery + + +To the left is the force of gravity plus the recovery force, which is essentially an implementation of Hooke's law. In this case, there is a degree of stabilization that happens, so the blades don't fully collapse to the floor, and we end up an equilibrium between the force of gravity and the recovery force. Contrast this with the static grass image, and note that what we have looks much more "grass-like". +
+ +##### Wind + + +Wind is implemented as a noise function. This allows the modeling of a few different types of wind behavior. To the left, a sinusoidal noise function for emulates a breeze passing through the grasses. +
+ +#### Culling +To further improve performance/visuals, we can cull some of the unnecessary blades of grass. + +##### Orientation culling + + +Since our blades are just 2D models, if viewed parallel to their width, they are essentially a line - not even worth rendering. Orientation culling addresses this by removing blades that are aligned with the camera's view, according to a tune-able threshold. The left depicts this with a sparse field of grass for visual clarity. +
+ +##### View-frustum culling + + +[View frustum](https://en.wikipedia.org/wiki/Viewing_frustum) culling is when blades of grass outside the truncated pyramid of view are removed to improve performance - we don't see them anyways. To the left, we have highly aggressive culling from a narrow view frustum. +
+ +##### Distance culling + + +Faraway grass blades can be smaller than a pixel. To avoid z-fighting/rendering artifacts, blades beyond a tune-able distance from the camera are culled such that a "max distance" is enforced where blades of grass will always be culled, with decreasing likelihood of being culled as the blade approaches the camera's position. +
+ +#### Tesselation LOD + + +The level of tesselation can be lowered when blades of grass are further from the camera. To the left, a sparse field of grass becomes more polygonal (the far blades become a straight-up triangle) as the camera moves further, and gain more polygonal detail as the camera moves closer. +
+ +### Performance Analysis +Below is a chart describing performance of the renderer with respect to the log of the number of grass blades, measured in frames per second(FPS). FPS is averaged over 60 seconds. + +The chart compares some of the performance optimizations to regular performance. + +

+ +

+ +The settings for tesselation default to `20`; for changing tesselation LOD the level can go down to `10, 5, 3, 1`, depending on distance. +The orientation is culled according to a threshold setting of `0.9`. +The view-frustum is culled according to a tolerance of `0.95 * frustum`. +The distance is culled with `10` buckets and a maximum distance of `40` units. + +Finally, below is a chart comparing zero vs all of the optimizations. + +

+ +

\ No newline at end of file diff --git a/bin/Release/vulkan_grass_rendering.exe b/bin/Release/vulkan_grass_rendering.exe index f68db3a..b0e874c 100644 Binary files a/bin/Release/vulkan_grass_rendering.exe and b/bin/Release/vulkan_grass_rendering.exe differ diff --git a/img/bladesvsfps.png b/img/bladesvsfps.png new file mode 100644 index 0000000..c70431c Binary files /dev/null and b/img/bladesvsfps.png differ diff --git a/img/distance_culling.gif b/img/distance_culling.gif new file mode 100644 index 0000000..a31cbb1 Binary files /dev/null and b/img/distance_culling.gif differ diff --git a/img/gravity_force.gif b/img/gravity_force.gif new file mode 100644 index 0000000..6cd7a3b Binary files /dev/null and b/img/gravity_force.gif differ diff --git a/img/orient_culling.gif b/img/orient_culling.gif new file mode 100644 index 0000000..a6f8b9d Binary files /dev/null and b/img/orient_culling.gif differ diff --git a/img/performancegraph.png b/img/performancegraph.png new file mode 100644 index 0000000..b745f5d Binary files /dev/null and b/img/performancegraph.png differ diff --git a/img/recovery_force.png b/img/recovery_force.png new file mode 100644 index 0000000..cba47dc Binary files /dev/null and b/img/recovery_force.png differ diff --git a/img/static_grass.png b/img/static_grass.png new file mode 100644 index 0000000..ca3a52d Binary files /dev/null and b/img/static_grass.png differ diff --git a/img/tesselation_lod.gif b/img/tesselation_lod.gif new file mode 100644 index 0000000..90769b7 Binary files /dev/null and b/img/tesselation_lod.gif differ diff --git a/img/vf_culling.gif b/img/vf_culling.gif new file mode 100644 index 0000000..7540c94 Binary files /dev/null and b/img/vf_culling.gif differ diff --git a/img/wind_force1.gif b/img/wind_force1.gif new file mode 100644 index 0000000..a46d442 Binary files /dev/null and b/img/wind_force1.gif differ diff --git a/img/zerovsall.png b/img/zerovsall.png new file mode 100644 index 0000000..68105a8 Binary files /dev/null and b/img/zerovsall.png 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/Blades.h b/src/Blades.h index 9bd1eed..8c61ddd 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 << 16; 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 add88d7..9506aa2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -44,7 +44,7 @@ foreach(SHADER_SOURCE ${SHADER_SOURCES}) add_dependencies(vulkan_grass_rendering ${fname}.spv) endif(WIN32) - # TODO: Build shaders on not windows + # : Build shaders on not windows endforeach() target_link_libraries(vulkan_grass_rendering ${ASSIMP_LIBRARIES} Vulkan::Vulkan glfw) diff --git a/src/Renderer.cpp b/src/Renderer.cpp index b445d04..ba26495 100644 --- a/src/Renderer.cpp +++ b/src/Renderer.cpp @@ -18,7 +18,7 @@ Renderer::Renderer(Device* device, SwapChain* swapChain, Scene* scene, Camera* c CreateCommandPools(); CreateRenderPass(); CreateCameraDescriptorSetLayout(); - CreateModelDescriptorSetLayout(); + CreateModelDescriptorSetLayout(); // bindings: plane + image; ie ubo + image sampler CreateTimeDescriptorSetLayout(); CreateComputeDescriptorSetLayout(); CreateDescriptorPool(); @@ -195,9 +195,37 @@ void Renderer::CreateTimeDescriptorSetLayout() { } 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 grassInLayoutBinding = {}; + grassInLayoutBinding.binding = 0; + grassInLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + grassInLayoutBinding.descriptorCount = 1; + grassInLayoutBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + grassInLayoutBinding.pImmutableSamplers = nullptr; + + VkDescriptorSetLayoutBinding grassOutLayoutBinding = {}; + grassOutLayoutBinding.binding = 1; + grassOutLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + grassOutLayoutBinding.descriptorCount = 1; + grassOutLayoutBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + grassOutLayoutBinding.pImmutableSamplers = nullptr; + + VkDescriptorSetLayoutBinding numRemainLayoutBinding = {}; + numRemainLayoutBinding.binding = 2; + numRemainLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + numRemainLayoutBinding.descriptorCount = 1; + numRemainLayoutBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + numRemainLayoutBinding.pImmutableSamplers = nullptr; + + std::vector bindings = { grassInLayoutBinding, grassOutLayoutBinding, numRemainLayoutBinding }; + + 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() { @@ -215,7 +243,8 @@ void Renderer::CreateDescriptorPool() { // Time (compute) { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER , 1 }, - // TODO: Add any additional types and counts of descriptors you will need to allocate + // Compute (input blades, culled, num blades remaining) + { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER , static_cast(3 * scene->GetBlades().size()) } }; VkDescriptorPoolCreateInfo poolInfo = {}; @@ -318,8 +347,35 @@ 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()); + VkDescriptorSetLayout layouts[] = { modelDescriptorSetLayout }; // cf. CreateModelDescriptorSetLayout() binding 0 + VkDescriptorSetAllocateInfo allocInfo = {}; + allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + allocInfo.descriptorPool = descriptorPool; + allocInfo.descriptorSetCount = static_cast(grassDescriptorSets.size()); + allocInfo.pSetLayouts = layouts; + 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; + } + vkUpdateDescriptorSets(logicalDevice, static_cast(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr); } void Renderer::CreateTimeDescriptorSet() { @@ -358,8 +414,67 @@ 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->GetModels().size()); + // storage buffers to hold the grass blades, the culled grass blades, and the output number of grass blades + 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; + if (vkAllocateDescriptorSets(logicalDevice, &allocInfo, computeDescriptorSets.data()) != VK_SUCCESS) { + throw std::runtime_error("Failed to allocate 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); // buffer size + + VkDescriptorBufferInfo culledBladesBufferInfo = {}; + culledBladesBufferInfo.buffer = scene->GetBlades()[i]->GetCulledBladesBuffer(); + culledBladesBufferInfo.offset = 0; + culledBladesBufferInfo.range = NUM_BLADES * sizeof(Blade); + + VkDescriptorBufferInfo numBladesBufferInfo = {}; + numBladesBufferInfo.buffer = scene->GetBlades()[i]->GetNumBladesBuffer(); + numBladesBufferInfo.offset = 0; + numBladesBufferInfo.range = sizeof(BladeDrawIndirect); + + 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; + + 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; + + 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; + } + + vkUpdateDescriptorSets(logicalDevice, static_cast(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr); } void Renderer::CreateGraphicsPipeline() { @@ -716,8 +831,7 @@ void Renderer::CreateComputePipeline() { computeShaderStageInfo.module = computeShaderModule; 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 = {}; @@ -883,7 +997,11 @@ void Renderer::RecordComputeCommandBuffer() { // Bind descriptor set for time uniforms 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 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, 1); + } // ~ End recording ~ if (vkEndCommandBuffer(computeCommandBuffer) != VK_SUCCESS) { @@ -975,14 +1093,11 @@ void Renderer::RecordCommandBuffers() { for (uint32_t j = 0; j < scene->GetBlades().size(); ++j) { 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 @@ -1041,8 +1156,6 @@ void Renderer::Frame() { Renderer::~Renderer() { vkDeviceWaitIdle(logicalDevice); - // TODO: destroy any resources you created - vkFreeCommandBuffers(logicalDevice, graphicsCommandPool, static_cast(commandBuffers.size()), commandBuffers.data()); vkFreeCommandBuffers(logicalDevice, computeCommandPool, 1, &computeCommandBuffer); @@ -1057,6 +1170,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/main.cpp b/src/main.cpp index 8bf822b..05cc114 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -66,8 +66,10 @@ namespace { } int main() { + int width = 1020; + int height = 720; static constexpr char* applicationName = "Vulkan Grass Rendering"; - InitializeWindow(640, 480, applicationName); + InitializeWindow(width, height, applicationName); unsigned int glfwExtensionCount = 0; const char** glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); @@ -90,7 +92,7 @@ int main() { swapChain = device->CreateSwapChain(surface, 5); - camera = new Camera(device, 640.f / 480.f); + camera = new Camera(device, (float)width / (float)height); VkCommandPoolCreateInfo transferPoolInfo = {}; transferPoolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; @@ -149,7 +151,10 @@ int main() { renderer->Frame(); } + vkDeviceWaitIdle(device->GetVkDevice()); + // from ed discusoin, suppress validation layer error on close main window + vkDestroySurfaceKHR(instance->GetVkInstance(), surface, nullptr); vkDestroyImage(device->GetVkDevice(), grassImage, nullptr); vkFreeMemory(device->GetVkDevice(), grassImageMemory, nullptr); diff --git a/src/shaders/compute.comp b/src/shaders/compute.comp index 0fd0224..9475e00 100644 --- a/src/shaders/compute.comp +++ b/src/shaders/compute.comp @@ -21,36 +21,137 @@ struct Blade { vec4 up; }; -// TODO: Add bindings to: +// bindings for // 1. Store the input blades // 2. Write out the culled blades // 3. Write the total number of blades remaining +layout(set = 2, binding = 0) buffer InputBladesBuffer { + Blade inputBlades[]; +}; + +layout(set = 2, binding = 1) buffer CulledBladesBuffer { + Blade culled[]; +}; // 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); } +vec3 vec3Noise(vec3 v0, float time) { // noise funcn + float period = 0.7; + float magnitude = 8.0; + float windvar = 0.3; // wind variance + return vec3(magnitude * (1 + sin(period * (windvar * v0.x + time))), + 0.0, + (1 + sin(period * (v0.z + time)))); +} + +bool viewFrustumTest(vec3 p, float toleranceRatio) { + vec4 pDevice = camera.proj * camera.view * vec4(p, 1.0); + float h = pDevice.w * toleranceRatio; // compared to addition, ratio seems to be easier to control + return inBounds(pDevice.x, h) && inBounds(pDevice.y, h); // && inBounds(pDevice.z, h); + // culling depth here is kinda scuffed w/ decreased tesselation levels + distance culling +} + void main() { // Reset the number of blades to 0 - if (gl_GlobalInvocationID.x == 0) { - // numBlades.vertexCount = 0; + if (gl_GlobalInvocationID.x == 0) { // .x corresponds 0 to NUM_BLADES based on dispatch in Renderer.cpp + numBlades.vertexCount = 0; } barrier(); // Wait till all threads reach this point - // TODO: Apply forces on every blade and update the vertices in the buffer + vec3 v0 = inputBlades[gl_GlobalInvocationID.x].v0.xyz; + vec3 v1 = inputBlades[gl_GlobalInvocationID.x].v1.xyz; + vec3 v2 = inputBlades[gl_GlobalInvocationID.x].v2.xyz; + vec3 up = inputBlades[gl_GlobalInvocationID.x].up.xyz; + + float angle = inputBlades[gl_GlobalInvocationID.x].v0.w; + float height = inputBlades[gl_GlobalInvocationID.x].v1.w; + float width = inputBlades[gl_GlobalInvocationID.x].v2.w; + float stiffness = inputBlades[gl_GlobalInvocationID.x].up.w; + + // gravity + vec4 gravity = vec4(0, -1, 0, 9.81); // xyz dir, w magnitude + + vec3 bitangent = vec3(-cos(angle), 0, sin(angle)); + vec3 frontDirNorm = normalize(cross(bitangent, up)); // reverse? + vec3 environGravity = normalize(gravity.xyz) * gravity.w; + vec3 frontGravity = 0.25 * length(environGravity) * frontDirNorm; + + vec3 totalGravity = frontGravity + environGravity; + // end gravity + + // eqn 3 - ignore strength? + vec3 recoveryForce = (v0 + height * up - v2) * stiffness; + + //wind - dirAlignment for fd, straightness ratio for fr + vec3 wind = vec3Noise(v0, totalTime); + float dirAlignment = 1 - abs(dot(normalize(wind), normalize(v2 - v0))); + float straightnessRatio = dot(v2 - v0, up) / height; + float windAlignment = dirAlignment * straightnessRatio; + vec3 windForce = wind * windAlignment; + //end wind + + // Apply forces on every blade and update the vertices in the buffer + v2 += (totalGravity + recoveryForce + windForce) * deltaTime; + + // state validation and updating inputBlade + v2 = v2 - up * min(dot(up, v2 - v0), 0); + float lProj = length(v2 - v0 - up * dot(v2 - v0, up)); + v1 = v0 + height * up * max(1 - lProj / height, 0.05 * max(lProj / height, 1.0)); + + float L1 = distance(v2, v0); + float L2 = distance(v1, v0) + distance(v2, v1); + float L = (2.0 * L1 + L2) / 3.0; // eqn 12 in the text; we have degree 2 Bezier so L2*1 = L2 + + float r = height / L; + inputBlades[gl_GlobalInvocationID.x].v1.xyz = v0 + r * (v1 - v0); + inputBlades[gl_GlobalInvocationID.x].v2.xyz = inputBlades[gl_GlobalInvocationID.x].v1.xyz + r * (v2 - v1); + // end state validation + + // Cull blades + v0 = inputBlades[gl_GlobalInvocationID.x].v0.xyz; + v1 = inputBlades[gl_GlobalInvocationID.x].v1.xyz; + v2 = inputBlades[gl_GlobalInvocationID.x].v2.xyz; + // orientation culling + float ORIENTATION_CULL_THRESH = 0.9; //lower = more aggressively cullled + vec3 camPos = inverse(camera.view)[3].xyz; //check matrix transform ? think its correct + vec3 camToBlade = v0 - camPos; + vec3 planarViewDir = camToBlade - up * dot(camToBlade, up); // eqn 19, presuming up is normal of plane + if (abs(dot(normalize(planarViewDir), normalize(bitangent))) > ORIENTATION_CULL_THRESH) { + return; + } + // end orientation culling + + // view-frustum culling + float TOLERANCE = 0.95; // the lower the tolerance, the more the edges are culled + vec3 m = 0.25 * v0 + 0.5 * v1 + 0.25 * v2; + if (!viewFrustumTest(v0, TOLERANCE) && + !viewFrustumTest(v2, TOLERANCE) && + !viewFrustumTest(m, TOLERANCE)) { + return; + } + // end view-frustum culling + + // distance culling + int NUM_BUCKETS = 10; + float MAX_DISTANCE = 40.0; + // eqn 20 - paper seems to have flipped sign typo?? + if (mod(gl_GlobalInvocationID.x, NUM_BUCKETS) >= + floor(NUM_BUCKETS * (1 - length(camToBlade) / MAX_DISTANCE))) { + return; + } + // end distance culling - // 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 + culled[atomicAdd(numBlades.vertexCount, 1)] = inputBlades[gl_GlobalInvocationID.x]; +// atomicAdd(numBlades.vertexCount, 1); +// culled[gl_GlobalInvocationID.x] = inputBlades[gl_GlobalInvocationID.x]; } diff --git a/src/shaders/grass.frag b/src/shaders/grass.frag index c7df157..a885e05 100644 --- a/src/shaders/grass.frag +++ b/src/shaders/grass.frag @@ -6,12 +6,15 @@ layout(set = 0, binding = 0) uniform CameraBufferObject { mat4 proj; } camera; -// TODO: Declare fragment shader inputs +layout(location = 0) in vec2 in_uv; layout(location = 0) out vec4 outColor; +// simple flat color gradient void main() { - // TODO: Compute fragment color + // note in_uv.y -> corresponds to height, .x width of each blade model + vec3 rootColor = vec3(0.1, 0.2, 0.2); + vec3 tipColor = vec3(0.1, 0.8, 0.2); - outColor = vec4(1.0); + outColor = vec4(mix(rootColor, tipColor, in_uv.y), 1.); } diff --git a/src/shaders/grass.tesc b/src/shaders/grass.tesc index f9ffd07..3d45db6 100644 --- a/src/shaders/grass.tesc +++ b/src/shaders/grass.tesc @@ -8,19 +8,42 @@ layout(set = 0, binding = 0) uniform CameraBufferObject { mat4 proj; } camera; -// TODO: Declare tessellation control shader inputs and outputs +layout(location = 0) in vec4[] in_v0; // just 1 element per arr +layout(location = 1) in vec4[] in_v1; +layout(location = 2) in vec4[] in_v2; +//layout(location = 3) in vec4[] in_up; + +layout(location = 0) out vec4[] out_v0; +layout(location = 1) out vec4[] out_v1; +layout(location = 2) out vec4[] out_v2; + +float getTessLevel(float dist) { + if (dist < 2.0) { + return 20.0; + } else if (dist < 5.0) { + return 10.0; + } else if (dist < 10.0) { + return 5.0; + } else if (dist < 15.0) { + return 3.0; + } else { + return 1.0; + } +} void main() { // Don't move the origin location of the patch - gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position; + out_v0[gl_InvocationID] = in_v0[gl_InvocationID]; + out_v1[gl_InvocationID] = in_v1[gl_InvocationID]; + out_v2[gl_InvocationID] = in_v2[gl_InvocationID]; - // TODO: Write any shader outputs + vec3 camPos = inverse(camera.view)[3].xyz; + float tessLevel = getTessLevel(length(in_v0[0].xyz - camPos)); - // 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] = 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..4878335 100644 --- a/src/shaders/grass.tese +++ b/src/shaders/grass.tese @@ -8,11 +8,43 @@ layout(set = 0, binding = 0) uniform CameraBufferObject { mat4 proj; } camera; -// TODO: Declare tessellation evaluation shader inputs and outputs +// https://www.khronos.org/opengl/wiki/Tessellation_Evaluation_Shader +// see grass.tesc for other links +layout(location = 0) in vec4[] in_v0; +layout(location = 1) in vec4[] in_v1; +layout(location = 2) in vec4[] in_v2; + +layout(location = 0) out vec2 out_uv; void main() { + // following along w/ + // https://www.cg.tuwien.ac.at/research/publications/2017/JAHRMANN-2017-RRTG/JAHRMANN-2017-RRTG-draft.pdf + // de casteljau bezier + // Use u and v to parameterize along the grass blade and output positions for each vertex of the grass blade float u = gl_TessCoord.x; float v = gl_TessCoord.y; + + vec3 v0 = in_v0[0].xyz; + vec3 v1 = in_v1[0].xyz; + vec3 v2 = in_v2[0].xyz; + + float dir_angle = in_v0[0].w; + float width = in_v2[0].w; + + // interp along height of blade w/ v + vec3 a = mix(v0, v1, v); + vec3 b = mix(v1, v2, v); + vec3 c = mix(a, b, v); + + // from Blades.cpp convention, y(height) 0 + vec3 t0 = normalize(b - a); + vec3 t1 = normalize(vec3(-cos(dir_angle), 0, sin(dir_angle))); + + vec3 c0 = c - width * t1; + vec3 c1 = c + width * t1; + + float t = (u + 0.5f * v - u * v); - // TODO: Use u and v to parameterize along the grass blade and output positions for each vertex of the grass blade + out_uv = vec2(u, v); + gl_Position = camera.proj * camera.view * vec4(mix(c0, c1, t), 1.); } diff --git a/src/shaders/grass.vert b/src/shaders/grass.vert index db9dfe9..35b982f 100644 --- a/src/shaders/grass.vert +++ b/src/shaders/grass.vert @@ -6,12 +6,18 @@ layout(set = 1, binding = 0) uniform ModelBufferObject { mat4 model; }; -// TODO: Declare vertex shader inputs and outputs +layout(location = 0) in vec4 in_v0; +layout(location = 1) in vec4 in_v1; +layout(location = 2) in vec4 in_v2; +layout(location = 3) in vec4 in_up; -out gl_PerVertex { - vec4 gl_Position; -}; +layout(location = 0) out vec4 out_v0; +layout(location = 1) out vec4 out_v1; +layout(location = 2) out vec4 out_v2; +//layout(location = 3) out vec4 out_up; void main() { - // TODO: Write gl_Position and any other shader outputs + out_v0 = vec4((model * vec4(in_v0.xyz, 1.f)).xyz, in_v0.w); + out_v1 = vec4((model * vec4(in_v1.xyz, 1.f)).xyz, in_v1.w); + out_v2 = vec4((model * vec4(in_v2.xyz, 1.f)).xyz, in_v2.w); }