diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
deleted file mode 100644
index 63fda17212cf97f2857ac1891dfad9dd052cbe6a..0000000000000000000000000000000000000000
--- a/.gitlab-ci.yml
+++ /dev/null
@@ -1,125 +0,0 @@
-variables:
-  RUN:
-    value: "all"
-    description: "The tests that should run. Possible values: ubuntu, win-msvc, win-mingw, mac, all."
-  GIT_DEPTH: 15
-
-stages:
-  - build
-  - deploy
-
-build_ubuntu_gcc:
-  only:
-    variables:
-      - $RUN =~ /\bubuntu.*/i || $RUN =~ /\ball.*/i
-  stage: build
-  tags: 
-    - ubuntu-gcc-cached
-  variables:
-    GIT_SUBMODULE_STRATEGY: recursive
-  timeout: 15m
-  retry: 1
-  script:
-    - mkdir debug
-    - cd debug
-    - cmake -DCMAKE_BUILD_TYPE=Debug ..
-    - cmake --build . -j 4
-  artifacts:
-    name: "Documentation - $CI_PIPELINE_ID"
-    paths:
-      - doc/html
-      - doc/latex
-    expire_in: never
-
-build_win10_msvc:
-  only:
-    variables:
-      - $RUN =~ /\bwin-msvc.*/i || $RUN =~ /\ball.*/i
-  stage: build
-  tags: 
-    - win10-msvc-cached
-  variables:
-    GIT_SUBMODULE_STRATEGY: recursive
-  timeout: 15m
-  retry: 0
-  script:
-    - cd 'C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\Tools\'
-    - .\Launch-VsDevShell.ps1
-    - cd $CI_PROJECT_DIR
-    - mkdir debug
-    - cd debug
-    - cmake -DCMAKE_BUILD_TYPE=Debug ..
-    - cmake --build . -j 4
-
-build_win10_mingw:
-  only:
-    variables:
-      - $RUN =~ /\bwin-mingw.*/i || $RUN =~ /\ball.*/i
-  stage: build
-  tags: 
-    - win10-mingw-cached
-  variables:
-    GIT_SUBMODULE_STRATEGY: recursive
-  timeout: 15m
-  retry: 0
-  script:
-    - mkdir debug
-    - cd debug
-    - cmake --no-warn-unused-cli -DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=TRUE -DCMAKE_BUILD_TYPE:STRING=Debug -DCMAKE_C_COMPILER:FILEPATH=C:\msys64\mingw64\bin\x86_64-w64-mingw32-gcc.exe -DCMAKE_CXX_COMPILER:FILEPATH=C:\msys64\mingw64\bin\x86_64-w64-mingw32-g++.exe .. -G "Unix Makefiles"
-    - cmake --build . -j 4
-
-build_mac_clang:
-  only:
-    variables:
-      - $RUN =~ /\bmac.*/i || $RUN =~ /\ball.*/i
-  stage: build
-  tags: 
-    - catalina-clang-cached
-  variables:
-    GIT_SUBMODULE_STRATEGY: recursive
-  timeout: 15m
-  retry: 1
-  script:
-    - mkdir debug
-    - cd debug
-    - export LDFLAGS="-L/usr/local/opt/llvm/lib"
-    - export CPPFLAGS="-I/usr/local/opt/llvm/include"
-    - cmake -DCMAKE_C_COMPILER="/usr/local/opt/llvm/bin/clang" -DCMAKE_CXX_COMPILER="/usr/local/opt/llvm/bin/clang++" -DCMAKE_BUILD_TYPE=Debug ..
-    - cmake --build . -j 4
-
-deploy_doc_develop:
-  only:
-    variables:
-      - $RUN =~ /\bubuntu.*/i || $RUN =~ /\ball.*/i
-    refs:
-      - develop
-  stage: deploy
-  needs: ["build_ubuntu_gcc"]
-  dependencies: 
-    - build_ubuntu_gcc
-  tags: 
-    - webserver
-  variables:
-    GIT_STRATEGY: none
-  script:
-    - rsync -avh doc/html/ /var/www/html/develop --delete
-    - echo "Check it out at https://vkcv.de/develop"
-
-deploy_doc_branch:
-  only:
-    variables:
-      - $RUN =~ /\bubuntu.*/i || $RUN =~ /\ball.*/i
-  except:
-    refs:
-      - develop
-  stage: deploy
-  needs: ["build_ubuntu_gcc"]
-  dependencies: 
-    - build_ubuntu_gcc
-  tags: 
-    - webserver
-  variables:
-    GIT_STRATEGY: none
-  script:
-    - rsync -avh  doc/html/ /var/www/html/branch/$CI_COMMIT_BRANCH --delete
-    - echo "Check it out at https://vkcv.de/branch/$CI_COMMIT_BRANCH"
\ No newline at end of file
diff --git a/include/vkcv/Core.hpp b/include/vkcv/Core.hpp
index 1e036f799d6769a88e4350afdf182ef397400907..77370fb39b1a5a300614fe9cfb0b505e5ef2dd67 100644
--- a/include/vkcv/Core.hpp
+++ b/include/vkcv/Core.hpp
@@ -207,7 +207,7 @@ namespace vkcv
         [[nodiscard]]
         SamplerHandle createSampler(SamplerFilterType magFilter, SamplerFilterType minFilter,
 									SamplerMipmapMode mipmapMode, SamplerAddressMode addressMode,
-									float mipLodBias = 0.0f);
+									float mipLodBias = 0.0f, SamplerBorderColor borderColor = SamplerBorderColor::INT_ZERO_OPAQUE);
 
         /**
          * Creates an #Image with a given format, width, height and depth.
diff --git a/include/vkcv/DrawcallRecording.hpp b/include/vkcv/DrawcallRecording.hpp
index 77e2a2e13bee3248e424d1d9c3d4cad7aaecd34b..70b23107c4605e6ac01aa80fa4a431c62d7a7219 100644
--- a/include/vkcv/DrawcallRecording.hpp
+++ b/include/vkcv/DrawcallRecording.hpp
@@ -1,11 +1,12 @@
 #pragma once
 #include <vulkan/vulkan.hpp>
-#include <vkcv/Handles.hpp>
-#include <vkcv/DescriptorConfig.hpp>
-#include <vkcv/PushConstants.hpp>
+#include "vkcv/Handles.hpp"
+#include "vkcv/DescriptorConfig.hpp"
+#include "vkcv/PushConstants.hpp"
 #include "Buffer.hpp"
 
 namespace vkcv {
+	
     struct VertexBufferBinding {
         inline VertexBufferBinding(vk::DeviceSize offset, vk::Buffer buffer) noexcept
             : offset(offset), buffer(buffer) {}
@@ -20,12 +21,12 @@ namespace vkcv {
     };
 
     struct DescriptorSetUsage {
-        inline DescriptorSetUsage(uint32_t setLocation, vk::DescriptorSet vulkanHandle,
+        inline DescriptorSetUsage(uint32_t setLocation, DescriptorSetHandle descriptorSet,
 								  const std::vector<uint32_t>& dynamicOffsets = {}) noexcept
-            : setLocation(setLocation), vulkanHandle(vulkanHandle), dynamicOffsets(dynamicOffsets) {}
+            : setLocation(setLocation), descriptorSet(descriptorSet), dynamicOffsets(dynamicOffsets) {}
 
         const uint32_t          	setLocation;
-        const vk::DescriptorSet 	vulkanHandle;
+        const DescriptorSetHandle 	descriptorSet;
         const std::vector<uint32_t> dynamicOffsets;
     };
 
@@ -51,8 +52,6 @@ namespace vkcv {
 
     };
 
-    vk::IndexType getIndexType(IndexBitCount indexByteCount);
-
     struct DrawcallInfo {
         inline DrawcallInfo(const Mesh& mesh, const std::vector<DescriptorSetUsage>& descriptorSets, const uint32_t instanceCount = 1)
             : mesh(mesh), descriptorSets(descriptorSets), instanceCount(instanceCount){}
@@ -62,22 +61,6 @@ namespace vkcv {
         uint32_t                        instanceCount;
     };
 
-    void recordDrawcall(
-        const DrawcallInfo      &drawcall,
-        vk::CommandBuffer       cmdBuffer,
-        vk::PipelineLayout      pipelineLayout,
-        const PushConstants     &pushConstants,
-        const size_t            drawcallIndex);
-
-    void recordIndirectDrawcall(
-            const DrawcallInfo                                  &drawcall,
-            vk::CommandBuffer                                   cmdBuffer,
-            const vkcv::Buffer<vk::DrawIndexedIndirectCommand>  &drawBuffer,
-            const uint32_t                                      drawCount,
-            vk::PipelineLayout                                  pipelineLayout,
-            const PushConstants                                 &pushConstants,
-            const size_t                                        drawcallIndex);
-
     void InitMeshShaderDrawFunctions(vk::Device device);
 
     struct MeshShaderDrawcall {
@@ -89,9 +72,10 @@ namespace vkcv {
     };
 
     void recordMeshShaderDrawcall(
+		const Core&								core,
         vk::CommandBuffer                       cmdBuffer,
         vk::PipelineLayout                      pipelineLayout,
-        const PushConstants&                 pushConstantData,
+        const PushConstants&                 	pushConstantData,
         const uint32_t                          pushConstantOffset,
         const MeshShaderDrawcall&               drawcall,
         const uint32_t                          firstTask);
diff --git a/include/vkcv/FeatureManager.hpp b/include/vkcv/FeatureManager.hpp
index 2f932f6e25c12e80f42d762f82a73f9a5265b793..5d5a2e9a7ca839c1cbd2cfa53d0f61d042ac3b2a 100644
--- a/include/vkcv/FeatureManager.hpp
+++ b/include/vkcv/FeatureManager.hpp
@@ -79,6 +79,12 @@ namespace vkcv {
 		[[nodiscard]]
 		bool checkSupport(const vk::PhysicalDeviceMeshShaderFeaturesNV& features, bool required) const;
 		
+		[[nodiscard]]
+		bool checkSupport(const vk::PhysicalDeviceShaderAtomicFloatFeaturesEXT& features, bool required) const;
+		
+		[[nodiscard]]
+		bool checkSupport(const vk::PhysicalDeviceShaderAtomicFloat2FeaturesEXT& features, bool required) const;
+		
 		/**
          * @brief Currently used for RTX. Checks support of the @p vk::PhysicalDeviceVulkan12Features.
          * @param features The features.
diff --git a/include/vkcv/Sampler.hpp b/include/vkcv/Sampler.hpp
index 007ed5ae4737275ddacbc5881a2c4c202b8806a4..e4f10cd9d3f1dd60021e62842acaa07d2aefb5ce 100644
--- a/include/vkcv/Sampler.hpp
+++ b/include/vkcv/Sampler.hpp
@@ -16,7 +16,19 @@ namespace vkcv {
 		REPEAT = 1,
 		MIRRORED_REPEAT = 2,
 		CLAMP_TO_EDGE = 3,
-		MIRROR_CLAMP_TO_EDGE = 4
+		MIRROR_CLAMP_TO_EDGE = 4,
+		CLAMP_TO_BORDER = 5
+	};
+	
+	enum class SamplerBorderColor {
+		INT_ZERO_OPAQUE = 1,
+		INT_ZERO_TRANSPARENT = 2,
+		
+		FLOAT_ZERO_OPAQUE = 3,
+		FLOAT_ZERO_TRANSPARENT = 4,
+		
+		INT_ONE_OPAQUE = 5,
+		FLOAT_ONE_OPAQUE = 6
 	};
 
 }
diff --git a/modules/scene/src/vkcv/scene/MeshPart.cpp b/modules/scene/src/vkcv/scene/MeshPart.cpp
index 46e79897719d5422151ec31837a41f7e58324a71..4aa450da4c11b23d686a2836f815d2c03c3675f7 100644
--- a/modules/scene/src/vkcv/scene/MeshPart.cpp
+++ b/modules/scene/src/vkcv/scene/MeshPart.cpp
@@ -70,11 +70,10 @@ namespace vkcv::scene {
 		
 		if (*this) {
 			const auto& material = getMaterial();
-			const auto& descriptorSet = core.getDescriptorSet(material.getDescriptorSet());
 			
 			drawcalls.push_back(DrawcallInfo(
 					vkcv::Mesh(m_vertexBindings, indexBuffer.getVulkanHandle(), m_indexCount),
-					{ DescriptorSetUsage(0, descriptorSet.vulkanHandle) }
+					{ DescriptorSetUsage(0, material.getDescriptorSet()) }
 			));
 		}
 	}
diff --git a/modules/shader_compiler/include/vkcv/shader/Compiler.hpp b/modules/shader_compiler/include/vkcv/shader/Compiler.hpp
index 5b119ca5c68f997bacfbea6c60d5c965f9a7a54e..d4be7384dfffeeb068a13660004be138e62722a1 100644
--- a/modules/shader_compiler/include/vkcv/shader/Compiler.hpp
+++ b/modules/shader_compiler/include/vkcv/shader/Compiler.hpp
@@ -6,10 +6,12 @@
 
 #include <vkcv/Event.hpp>
 #include <vkcv/ShaderStage.hpp>
+#include <vkcv/ShaderProgram.hpp>
 
 namespace vkcv::shader {
 	
 	typedef typename event_function<ShaderStage, const std::filesystem::path&>::type ShaderCompiledFunction;
+	typedef typename event_function<ShaderProgram&>::type ShaderProgramCompiledFunction;
 	
 	class Compiler {
 	private:
@@ -25,6 +27,11 @@ namespace vkcv::shader {
 							 const ShaderCompiledFunction& compiled,
 							 const std::filesystem::path& includePath, bool update) = 0;
 		
+		void compileProgram(ShaderProgram& program,
+							const std::unordered_map<ShaderStage, const std::filesystem::path>& stages,
+							const ShaderProgramCompiledFunction& compiled,
+							const std::filesystem::path& includePath = "", bool update = false);
+		
 		std::string getDefine(const std::string& name) const;
 		
 		void setDefine(const std::string& name, const std::string& value);
diff --git a/modules/shader_compiler/src/vkcv/shader/Compiler.cpp b/modules/shader_compiler/src/vkcv/shader/Compiler.cpp
index f5ec0435ca8b82dc5f328921f43a39338d1be456..1467bf22616764e00399ada69d5f866b929416d5 100644
--- a/modules/shader_compiler/src/vkcv/shader/Compiler.cpp
+++ b/modules/shader_compiler/src/vkcv/shader/Compiler.cpp
@@ -3,6 +3,27 @@
 
 namespace vkcv::shader {
 	
+	void Compiler::compileProgram(ShaderProgram& program,
+								  const std::unordered_map<ShaderStage, const std::filesystem::path>& stages,
+								  const ShaderProgramCompiledFunction& compiled,
+								  const std::filesystem::path& includePath, bool update) {
+		for (const auto& stage : stages) {
+			compile(
+				stage.first,
+				stage.second,
+				[&program](ShaderStage shaderStage, const std::filesystem::path& path) {
+					program.addShader(shaderStage, path);
+				},
+				includePath,
+				update
+			);
+		}
+		
+		if (compiled) {
+			compiled(program);
+		}
+	}
+	
 	std::string Compiler::getDefine(const std::string &name) const {
 		return m_defines.at(name);
 	}
diff --git a/modules/upscaling/src/vkcv/upscaling/FSRUpscaling.cpp b/modules/upscaling/src/vkcv/upscaling/FSRUpscaling.cpp
index 2045db9ea216d6810ec439114e93e06949c45cc0..0f6dfcdeaa3b1efc6f58c1c2d0eb61bbd2090a80 100644
--- a/modules/upscaling/src/vkcv/upscaling/FSRUpscaling.cpp
+++ b/modules/upscaling/src/vkcv/upscaling/FSRUpscaling.cpp
@@ -344,9 +344,7 @@ namespace vkcv::upscaling {
 					cmdStream,
 					m_easuPipeline,
 					dispatch,
-					{DescriptorSetUsage(0, m_core.getDescriptorSet(
-							m_easuDescriptorSet
-					).vulkanHandle, { 0 })},
+					{DescriptorSetUsage(0, m_easuDescriptorSet, { 0 })},
 					PushConstants(0)
 			);
 			
@@ -366,9 +364,7 @@ namespace vkcv::upscaling {
 					cmdStream,
 					m_rcasPipeline,
 					dispatch,
-					{DescriptorSetUsage(0, m_core.getDescriptorSet(
-							m_rcasDescriptorSet
-					).vulkanHandle, { 0 })},
+					{DescriptorSetUsage(0,m_rcasDescriptorSet, { 0 })},
 					PushConstants(0)
 			);
 			
@@ -386,9 +382,7 @@ namespace vkcv::upscaling {
 					cmdStream,
 					m_easuPipeline,
 					dispatch,
-					{DescriptorSetUsage(0, m_core.getDescriptorSet(
-							m_easuDescriptorSet
-					).vulkanHandle, { 0 })},
+					{DescriptorSetUsage(0, m_easuDescriptorSet, { 0 })},
 					PushConstants(0)
 			);
 		}
diff --git a/projects/CMakeLists.txt b/projects/CMakeLists.txt
index 621371fd4e20f942a775ac73300861ba2e15dd45..ffe8546c961697869354993539697b118fa8d6be 100644
--- a/projects/CMakeLists.txt
+++ b/projects/CMakeLists.txt
@@ -14,4 +14,5 @@ add_subdirectory(indirect_draw)
 add_subdirectory(bindless_textures)
 add_subdirectory(saf_r)
 add_subdirectory(indirect_dispatch)
-add_subdirectory(path_tracer)
\ No newline at end of file
+add_subdirectory(path_tracer)
+add_subdirectory(wobble_bobble)
\ No newline at end of file
diff --git a/projects/bindless_textures/src/main.cpp b/projects/bindless_textures/src/main.cpp
index 0ad6c1250c8a6e01838ae1b52fd52a6dd24fbbe3..6aa0a9d6106dd06c08dd5219c9570430b49ce229 100644
--- a/projects/bindless_textures/src/main.cpp
+++ b/projects/bindless_textures/src/main.cpp
@@ -123,18 +123,12 @@ int main(int argc, const char** argv) {
 	vkcv::ShaderProgram firstMeshProgram;
 	vkcv::shader::GLSLCompiler compiler;
 	
-	compiler.compile(vkcv::ShaderStage::VERTEX, std::filesystem::path("resources/shaders/shader.vert"),
-					 [&firstMeshProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
-		firstMeshProgram.addShader(shaderStage, path);
-	});
-	
-	compiler.compile(vkcv::ShaderStage::FRAGMENT, std::filesystem::path("resources/shaders/shader.frag"),
-					 [&firstMeshProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
-		firstMeshProgram.addShader(shaderStage, path);
-	});
+	compiler.compileProgram(firstMeshProgram, {
+		{ vkcv::ShaderStage::VERTEX, "resources/shaders/shader.vert" },
+		{ vkcv::ShaderStage::FRAGMENT, "resources/shaders/shader.frag" }
+	}, nullptr);
  
 	auto& attributes = mesh.vertexGroups[0].vertexBuffer.attributes;
-
 	
 	std::sort(attributes.begin(), attributes.end(), [](const vkcv::asset::VertexAttribute& x, const vkcv::asset::VertexAttribute& y) {
 		return static_cast<uint32_t>(x.type) < static_cast<uint32_t>(y.type);
@@ -218,7 +212,7 @@ int main(int argc, const char** argv) {
 
 	const vkcv::Mesh renderMesh(vertexBufferBindings, indexBuffer.getVulkanHandle(), mesh.vertexGroups[0].numIndices);
 
-	vkcv::DescriptorSetUsage    descriptorUsage(0, core.getDescriptorSet(descriptorSet).vulkanHandle);
+	vkcv::DescriptorSetUsage    descriptorUsage(0, descriptorSet);
 	vkcv::DrawcallInfo          drawcall(renderMesh, { descriptorUsage },1);
 
     vkcv::camera::CameraManager cameraManager(core.getWindow(windowHandle));
diff --git a/projects/first_mesh/src/main.cpp b/projects/first_mesh/src/main.cpp
index 55278b703b6a7c70d7d12e8b86ca5eea2b76cdf8..3f4378a6a2187ba33b7965fd6d008577541f7351 100644
--- a/projects/first_mesh/src/main.cpp
+++ b/projects/first_mesh/src/main.cpp
@@ -73,18 +73,12 @@ int main(int argc, const char** argv) {
 	vkcv::ShaderProgram firstMeshProgram;
 	vkcv::shader::GLSLCompiler compiler;
 	
-	compiler.compile(vkcv::ShaderStage::VERTEX, std::filesystem::path("assets/shaders/shader.vert"),
-					 [&firstMeshProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
-		firstMeshProgram.addShader(shaderStage, path);
-	});
-	
-	compiler.compile(vkcv::ShaderStage::FRAGMENT, std::filesystem::path("assets/shaders/shader.frag"),
-					 [&firstMeshProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
-		firstMeshProgram.addShader(shaderStage, path);
-	});
+	compiler.compileProgram(firstMeshProgram, {
+		{ vkcv::ShaderStage::VERTEX, "assets/shaders/shader.vert" },
+		{ vkcv::ShaderStage::FRAGMENT, "assets/shaders/shader.frag" }
+	}, nullptr);
  
 	auto& attributes = mesh.vertexGroups[0].vertexBuffer.attributes;
-
 	
 	std::sort(attributes.begin(), attributes.end(), [](const vkcv::asset::VertexAttribute& x, const vkcv::asset::VertexAttribute& y) {
 		return static_cast<uint32_t>(x.type) < static_cast<uint32_t>(y.type);
@@ -167,7 +161,7 @@ int main(int argc, const char** argv) {
 
 	const vkcv::Mesh renderMesh(vertexBufferBindings, indexBuffer.getVulkanHandle(), mesh.vertexGroups[0].numIndices);
 
-	vkcv::DescriptorSetUsage    descriptorUsage(0, core.getDescriptorSet(descriptorSet).vulkanHandle);
+	vkcv::DescriptorSetUsage    descriptorUsage(0, descriptorSet);
 	vkcv::DrawcallInfo          drawcall(renderMesh, { descriptorUsage },1);
 
     vkcv::camera::CameraManager cameraManager(core.getWindow(windowHandle));
diff --git a/projects/first_scene/src/main.cpp b/projects/first_scene/src/main.cpp
index 3ec5b9764f70efa8911d8e772d6287e5efc8f056..3dd6fc40d516c6f1ae3358d60c02fc4ed219245a 100644
--- a/projects/first_scene/src/main.cpp
+++ b/projects/first_scene/src/main.cpp
@@ -61,15 +61,10 @@ int main(int argc, const char** argv) {
 	vkcv::ShaderProgram sceneShaderProgram;
 	vkcv::shader::GLSLCompiler compiler;
 	
-	compiler.compile(vkcv::ShaderStage::VERTEX, std::filesystem::path("assets/shaders/shader.vert"),
-					 [&sceneShaderProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
-		sceneShaderProgram.addShader(shaderStage, path);
-	});
-	
-	compiler.compile(vkcv::ShaderStage::FRAGMENT, std::filesystem::path("assets/shaders/shader.frag"),
-					 [&sceneShaderProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
-		sceneShaderProgram.addShader(shaderStage, path);
-	});
+	compiler.compileProgram(sceneShaderProgram, {
+		{ vkcv::ShaderStage::VERTEX, "assets/shaders/shader.vert" },
+		{ vkcv::ShaderStage::FRAGMENT, "assets/shaders/shader.frag" }
+	}, nullptr);
 
 	const std::vector<vkcv::VertexAttachment> vertexAttachments = sceneShaderProgram.getVertexAttachments();
 	std::vector<vkcv::VertexBinding> bindings;
diff --git a/projects/first_triangle/src/main.cpp b/projects/first_triangle/src/main.cpp
index 1725b5c84dea931fe785880e3cd423cbb5f04a46..b4ba9046e07ecd3534844b493642098aa5847307 100644
--- a/projects/first_triangle/src/main.cpp
+++ b/projects/first_triangle/src/main.cpp
@@ -47,15 +47,10 @@ int main(int argc, const char** argv) {
 	vkcv::ShaderProgram triangleShaderProgram;
 	vkcv::shader::GLSLCompiler compiler;
 	
-	compiler.compile(vkcv::ShaderStage::VERTEX, std::filesystem::path("shaders/shader.vert"),
-					 [&triangleShaderProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
-		 triangleShaderProgram.addShader(shaderStage, path);
-	});
-	
-	compiler.compile(vkcv::ShaderStage::FRAGMENT, std::filesystem::path("shaders/shader.frag"),
-					 [&triangleShaderProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
-		triangleShaderProgram.addShader(shaderStage, path);
-	});
+	compiler.compileProgram(triangleShaderProgram, {
+		{vkcv::ShaderStage::VERTEX, "shaders/shader.vert"},
+		{ vkcv::ShaderStage::FRAGMENT, "shaders/shader.frag" }
+	}, nullptr);
 	
 	const auto swapchainExtent = core.getSwapchain(windowHandle).getExtent();
 
diff --git a/projects/indirect_dispatch/src/App.cpp b/projects/indirect_dispatch/src/App.cpp
index 532cef11db5b2bb8b5d741f6505507ff23fa4163..cc897e68651de65c7122188b52598535e6637335 100644
--- a/projects/indirect_dispatch/src/App.cpp
+++ b/projects/indirect_dispatch/src/App.cpp
@@ -261,7 +261,7 @@ void App::run() {
 		for (const Object& obj : sceneObjects) {
 			forwardSceneDrawcalls.push_back(vkcv::DrawcallInfo(
 				obj.meshResources.mesh, 
-				{ vkcv::DescriptorSetUsage(0, m_core.getDescriptorSet(m_meshPass.descriptorSet).vulkanHandle) }));
+				{ vkcv::DescriptorSetUsage(0, m_meshPass.descriptorSet) }));
 		}
 
 		m_core.recordDrawcallsToCmdStream(
@@ -338,7 +338,7 @@ void App::run() {
 			cmdStream,
 			m_gammaCorrectionPass.pipeline,
 			fullScreenImageDispatch,
-			{ vkcv::DescriptorSetUsage(0, m_core.getDescriptorSet(m_gammaCorrectionPass.descriptorSet).vulkanHandle) },
+			{ vkcv::DescriptorSetUsage(0, m_gammaCorrectionPass.descriptorSet) },
 			vkcv::PushConstants(0));
 
 		m_core.prepareSwapchainImageForPresent(cmdStream);
diff --git a/projects/indirect_dispatch/src/MotionBlur.cpp b/projects/indirect_dispatch/src/MotionBlur.cpp
index fea5b1d6f726851ad1d88e71301bffb0f6d71f4a..30116c84b79ae71c683ee865dbf93dd1a37923e9 100644
--- a/projects/indirect_dispatch/src/MotionBlur.cpp
+++ b/projects/indirect_dispatch/src/MotionBlur.cpp
@@ -120,7 +120,7 @@ vkcv::ImageHandle MotionBlur::render(
 		cmdStream,
 		m_tileResetPass.pipeline,
 		dispatchSizeOne,
-		{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_tileResetPass.descriptorSet).vulkanHandle) },
+		{ vkcv::DescriptorSetUsage(0, m_tileResetPass.descriptorSet) },
 		vkcv::PushConstants(0));
 
 	m_core->recordBufferMemoryBarrier(cmdStream, m_fullPathWorkTileBuffer);
@@ -166,7 +166,7 @@ vkcv::ImageHandle MotionBlur::render(
 		cmdStream,
 		m_tileClassificationPass.pipeline,
 		tileClassificationDispatch.data(),
-		{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_tileClassificationPass.descriptorSet).vulkanHandle) },
+		{ vkcv::DescriptorSetUsage(0, m_tileClassificationPass.descriptorSet) },
 		classificationPushConstants);
 
 	m_core->recordBufferMemoryBarrier(cmdStream, m_fullPathWorkTileBuffer);
@@ -254,7 +254,7 @@ vkcv::ImageHandle MotionBlur::render(
 			m_motionBlurPass.pipeline,
 			m_fullPathWorkTileBuffer,
 			0,
-			{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_motionBlurPass.descriptorSet).vulkanHandle) },
+			{ vkcv::DescriptorSetUsage(0, m_motionBlurPass.descriptorSet) },
 			motionBlurPushConstants);
 
 		m_core->recordComputeIndirectDispatchToCmdStream(
@@ -262,7 +262,7 @@ vkcv::ImageHandle MotionBlur::render(
 			m_colorCopyPass.pipeline,
 			m_copyPathWorkTileBuffer,
 			0,
-			{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_colorCopyPass.descriptorSet).vulkanHandle) },
+			{ vkcv::DescriptorSetUsage(0, m_colorCopyPass.descriptorSet) },
 			vkcv::PushConstants(0));
 
 		m_core->recordComputeIndirectDispatchToCmdStream(
@@ -270,7 +270,7 @@ vkcv::ImageHandle MotionBlur::render(
 			m_motionBlurFastPathPass.pipeline,
 			m_fastPathWorkTileBuffer,
 			0,
-			{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_motionBlurFastPathPass.descriptorSet).vulkanHandle) },
+			{ vkcv::DescriptorSetUsage(0, m_motionBlurFastPathPass.descriptorSet) },
 			fastPathPushConstants);
 	}
 	else if(mode == eMotionBlurMode::Disabled) {
@@ -305,7 +305,7 @@ vkcv::ImageHandle MotionBlur::render(
 			cmdStream,
 			m_tileVisualisationPass.pipeline,
 			dispatchCounts,
-			{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_tileVisualisationPass.descriptorSet).vulkanHandle) },
+			{ vkcv::DescriptorSetUsage(0, m_tileVisualisationPass.descriptorSet) },
 			vkcv::PushConstants(0));
 	}
 	else {
@@ -371,7 +371,7 @@ vkcv::ImageHandle MotionBlur::renderMotionVectorVisualisation(
 		cmdStream,
 		m_motionVectorVisualisationPass.pipeline,
 		dispatchSizes.data(),
-		{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_motionVectorVisualisationPass.descriptorSet).vulkanHandle) },
+		{ vkcv::DescriptorSetUsage(0, m_motionVectorVisualisationPass.descriptorSet) },
 		motionVectorVisualisationPushConstants);
 
 	return m_renderTargets.outputColor;
@@ -406,7 +406,7 @@ void MotionBlur::computeMotionTiles(
 		cmdStream,
 		m_motionVectorMinMaxPass.pipeline,
 		motionTileDispatchCounts.data(),
-		{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_motionVectorMinMaxPass.descriptorSet).vulkanHandle) },
+		{ vkcv::DescriptorSetUsage(0, m_motionVectorMinMaxPass.descriptorSet) },
 		vkcv::PushConstants(0));
 
 	// motion vector min max neighbourhood
@@ -432,6 +432,6 @@ void MotionBlur::computeMotionTiles(
 		cmdStream,
 		m_motionVectorMinMaxNeighbourhoodPass.pipeline,
 		motionTileDispatchCounts.data(),
-		{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_motionVectorMinMaxNeighbourhoodPass.descriptorSet).vulkanHandle) },
+		{ vkcv::DescriptorSetUsage(0, m_motionVectorMinMaxNeighbourhoodPass.descriptorSet) },
 		vkcv::PushConstants(0));
 }
\ No newline at end of file
diff --git a/projects/indirect_draw/src/main.cpp b/projects/indirect_draw/src/main.cpp
index 4841743a8e1462bfba572960986a3aba6ea355ab..9b062ab56245c44e5021f164b9e06fed8edd15c3 100644
--- a/projects/indirect_draw/src/main.cpp
+++ b/projects/indirect_draw/src/main.cpp
@@ -536,7 +536,7 @@ int main(int argc, const char** argv) {
     const uint32_t dispatchCount[3] = {static_cast<uint32_t>(ceiledDispatchCount), 1, 1};
 
 
-    vkcv::DescriptorSetUsage cullingUsage(0, core.getDescriptorSet(cullingDescSet).vulkanHandle, {});
+    vkcv::DescriptorSetUsage cullingUsage(0, cullingDescSet, {});
     vkcv::PushConstants emptyPushConstant(0);
 
     bool updateFrustumPlanes    = true;
diff --git a/projects/mesh_shader/src/main.cpp b/projects/mesh_shader/src/main.cpp
index dc7ca7caae5362e74fac82558b530cdce0357bb0..0a9914abf0a28f82eee06c5d2a67067faaff4109 100644
--- a/projects/mesh_shader/src/main.cpp
+++ b/projects/mesh_shader/src/main.cpp
@@ -354,7 +354,7 @@ int main(int argc, const char** argv) {
 
 		if (useMeshShader) {
 
-			vkcv::DescriptorSetUsage descriptorUsage(0, core.getDescriptorSet(meshShaderDescriptorSet).vulkanHandle);
+			vkcv::DescriptorSetUsage descriptorUsage(0, meshShaderDescriptorSet);
 			const uint32_t taskCount = (meshShaderModelData.meshlets.size() + 31) / 32;
 
 			core.recordMeshShaderDrawcalls(
@@ -368,7 +368,7 @@ int main(int argc, const char** argv) {
 		}
 		else {
 
-			vkcv::DescriptorSetUsage descriptorUsage(0, core.getDescriptorSet(vertexShaderDescriptorSet).vulkanHandle);
+			vkcv::DescriptorSetUsage descriptorUsage(0, vertexShaderDescriptorSet);
 
 			core.recordDrawcallsToCmdStream(
 				cmdStream,
diff --git a/projects/particle_simulation/src/BloomAndFlares.cpp b/projects/particle_simulation/src/BloomAndFlares.cpp
index a437afac25490e35547d53f432f7994d8e02717d..6ab0a8deff3d5fe906567562cb86d75a1cc2c09b 100644
--- a/projects/particle_simulation/src/BloomAndFlares.cpp
+++ b/projects/particle_simulation/src/BloomAndFlares.cpp
@@ -118,7 +118,7 @@ void BloomAndFlares::execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStre
             cmdStream,
             m_DownsamplePipe,
             initialDispatchCount,
-            {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_DownsampleDescSets[0]).vulkanHandle)},
+            {vkcv::DescriptorSetUsage(0, m_DownsampleDescSets[0])},
             vkcv::PushConstants(0));
 
     // downsample dispatches of blur buffer's mip maps
@@ -153,7 +153,7 @@ void BloomAndFlares::execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStre
                 cmdStream,
                 m_DownsamplePipe,
                 mipDispatchCount,
-                {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_DownsampleDescSets[mipLevel]).vulkanHandle)},
+                {vkcv::DescriptorSetUsage(0, m_DownsampleDescSets[mipLevel])},
                 vkcv::PushConstants(0));
 
         // image barrier between mips
@@ -198,7 +198,7 @@ void BloomAndFlares::execUpsamplePipe(const vkcv::CommandStreamHandle &cmdStream
                 cmdStream,
                 m_UpsamplePipe,
                 upsampleDispatchCount,
-                {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_UpsampleDescSets[mipLevel]).vulkanHandle)},
+                {vkcv::DescriptorSetUsage(0, m_UpsampleDescSets[mipLevel])},
                 vkcv::PushConstants(0)
         );
         // image barrier between mips
@@ -230,7 +230,7 @@ void BloomAndFlares::execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStr
             cmdStream,
             m_LensFlarePipe,
             lensFeatureDispatchCount,
-            {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_LensFlareDescSet).vulkanHandle)},
+            {vkcv::DescriptorSetUsage(0, m_LensFlareDescSet)},
             vkcv::PushConstants(0));
 }
 
@@ -263,7 +263,7 @@ void BloomAndFlares::execCompositePipe(const vkcv::CommandStreamHandle &cmdStrea
             cmdStream,
             m_CompositePipe,
             compositeDispatchCount,
-            {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_CompositeDescSet).vulkanHandle)},
+            {vkcv::DescriptorSetUsage(0, m_CompositeDescSet)},
             vkcv::PushConstants(0));
 }
 
diff --git a/projects/particle_simulation/src/main.cpp b/projects/particle_simulation/src/main.cpp
index 291b97f2fde4719530dcb26eb4baeb197f9b2a74..6637041e5ea9c8f1dd3dadf0303049d2a3b749e9 100644
--- a/projects/particle_simulation/src/main.cpp
+++ b/projects/particle_simulation/src/main.cpp
@@ -180,7 +180,7 @@ int main(int argc, const char **argv) {
 
     const vkcv::Mesh renderMesh({vertexBufferBindings}, particleIndexBuffer.getVulkanHandle(),
                                 particleIndexBuffer.getCount());
-    vkcv::DescriptorSetUsage descriptorUsage(0, core.getDescriptorSet(descriptorSet).vulkanHandle);
+    vkcv::DescriptorSetUsage descriptorUsage(0, descriptorSet);
 
     auto pos = glm::vec2(0.f);
     auto spawnPosition = glm::vec3(0.f);
@@ -283,7 +283,7 @@ int main(int argc, const char **argv) {
         core.recordComputeDispatchToCmdStream(cmdStream,
                                               computePipeline,
                                               computeDispatchCount,
-                                              {vkcv::DescriptorSetUsage(0,core.getDescriptorSet(computeDescriptorSet).vulkanHandle)},
+                                              {vkcv::DescriptorSetUsage(0, computeDescriptorSet)},
 											  pushConstantsCompute);
 
         core.recordBufferMemoryBarrier(cmdStream, particleBuffer.getHandle());
@@ -321,7 +321,7 @@ int main(int argc, const char **argv) {
             cmdStream, 
             tonemappingPipe, 
             tonemappingDispatchCount, 
-            {vkcv::DescriptorSetUsage(0, core.getDescriptorSet(tonemappingDescriptor).vulkanHandle) },
+            {vkcv::DescriptorSetUsage(0, tonemappingDescriptor) },
             vkcv::PushConstants(0));
 
         core.prepareSwapchainImageForPresent(cmdStream);
diff --git a/projects/path_tracer/src/main.cpp b/projects/path_tracer/src/main.cpp
index db25f239fd5845d2bd2ac69243ceb48b0b80a6ce..b7c495c070bd354dd58c0fad3f857489ad7408e7 100644
--- a/projects/path_tracer/src/main.cpp
+++ b/projects/path_tracer/src/main.cpp
@@ -342,7 +342,7 @@ int main(int argc, const char** argv) {
 			core.recordComputeDispatchToCmdStream(cmdStream,
 				imageClearPipeline,
 				fullscreenDispatchCount,
-				{ vkcv::DescriptorSetUsage(0, core.getDescriptorSet(imageClearDescriptorSet).vulkanHandle) },
+				{ vkcv::DescriptorSetUsage(0, imageClearDescriptorSet) },
 				vkcv::PushConstants(0));
 
 			clearMeanImage = false;
@@ -377,7 +377,7 @@ int main(int argc, const char** argv) {
 		core.recordComputeDispatchToCmdStream(cmdStream,
 			tracePipeline,
 			traceDispatchCount,
-			{ vkcv::DescriptorSetUsage(0,core.getDescriptorSet(traceDescriptorSet).vulkanHandle) },
+			{ vkcv::DescriptorSetUsage(0, traceDescriptorSet) },
 			pushConstantsCompute);
 
 		core.prepareImageForStorage(cmdStream, meanImage);
@@ -387,7 +387,7 @@ int main(int argc, const char** argv) {
 		core.recordComputeDispatchToCmdStream(cmdStream,
 			imageCombinePipeline,
 			fullscreenDispatchCount,
-			{ vkcv::DescriptorSetUsage(0,core.getDescriptorSet(imageCombineDescriptorSet).vulkanHandle) },
+			{ vkcv::DescriptorSetUsage(0, imageCombineDescriptorSet) },
 			vkcv::PushConstants(0));
 
 		core.recordImageMemoryBarrier(cmdStream, meanImage);
@@ -406,7 +406,7 @@ int main(int argc, const char** argv) {
 		core.recordComputeDispatchToCmdStream(cmdStream,
 			presentPipeline,
 			fullscreenDispatchCount,
-			{ vkcv::DescriptorSetUsage(0,core.getDescriptorSet(presentDescriptorSet).vulkanHandle) },
+			{ vkcv::DescriptorSetUsage(0, presentDescriptorSet) },
 			vkcv::PushConstants(0));
 
 		core.prepareSwapchainImageForPresent(cmdStream);
diff --git a/projects/rtx_ambient_occlusion/src/main.cpp b/projects/rtx_ambient_occlusion/src/main.cpp
index bac1fe367031e43cf2fc53d4be23c208f4fb682d..d4c8ec2cac8e26f70da346738da04de16a766a26 100644
--- a/projects/rtx_ambient_occlusion/src/main.cpp
+++ b/projects/rtx_ambient_occlusion/src/main.cpp
@@ -148,7 +148,7 @@ int main(int argc, const char** argv) {
 			rtxRegions.rmissRegion,
 			rtxRegions.rchitRegion,
 			rtxRegions.rcallRegion,
-			{	vkcv::DescriptorSetUsage(0, core.getDescriptorSet(rtxShaderDescriptorSet).vulkanHandle)},
+			{	vkcv::DescriptorSetUsage(0, rtxShaderDescriptorSet)},
 			pushConstantsRTX,
 			windowHandle);
 
diff --git a/projects/saf_r/src/main.cpp b/projects/saf_r/src/main.cpp
index 9ffd2845e1081d56e6026a10f19abd36e82d5828..a188c33d63f8c5d190ee02ce4aaef5adc2a8b967 100644
--- a/projects/saf_r/src/main.cpp
+++ b/projects/saf_r/src/main.cpp
@@ -198,7 +198,7 @@ int main(int argc, const char** argv) {
 	auto start = std::chrono::system_clock::now();
 
 	const vkcv::Mesh renderMesh({}, safrIndexBuffer.getVulkanHandle(), 3);
-	vkcv::DescriptorSetUsage descriptorUsage(0, core.getDescriptorSet(descriptorSet).vulkanHandle);
+	vkcv::DescriptorSetUsage descriptorUsage(0, descriptorSet);
 	vkcv::DrawcallInfo drawcall(renderMesh, { descriptorUsage }, 1);
 
 	//create the camera
@@ -280,7 +280,7 @@ int main(int argc, const char** argv) {
 		core.recordComputeDispatchToCmdStream(cmdStream,
 			computePipeline,
 			computeDispatchCount,
-			{ vkcv::DescriptorSetUsage(0,core.getDescriptorSet(computeDescriptorSet).vulkanHandle) },
+			{ vkcv::DescriptorSetUsage(0, computeDescriptorSet) },
 			pushConstantsCompute);
 
 		core.recordBufferMemoryBarrier(cmdStream, lightsBuffer.getHandle());
diff --git a/projects/sph/shaders/force.comp b/projects/sph/shaders/force.comp
index 9830a42f11a527e570bc77bc2f5a65b347e35819..694ddbdf7009d4e493a5f8dd8ba462e42d1e8b68 100644
--- a/projects/sph/shaders/force.comp
+++ b/projects/sph/shaders/force.comp
@@ -67,25 +67,29 @@ shared ParticleData particle_data [256];
 void main() {
     uint id = gl_GlobalInvocationID.x;
 
-    if(id >= int(particleCount))
-    {
-        return;
+    if (id >= int(particleCount)) {
+        particle_data[gl_LocalInvocationIndex].position = vec3(0.0f);
+        particle_data[gl_LocalInvocationIndex].density  = 0.0f;
+        particle_data[gl_LocalInvocationIndex].velocity = vec3(0.0f);
+        particle_data[gl_LocalInvocationIndex].pressure = 0.0f;
+    } else {
+        particle_data[gl_LocalInvocationIndex].position = inParticle[id].position;
+        particle_data[gl_LocalInvocationIndex].density  = inParticle[id].density;
+        particle_data[gl_LocalInvocationIndex].velocity = inParticle[id].velocity;
+        particle_data[gl_LocalInvocationIndex].pressure = inParticle[id].pressure;
     }
 
     uint index_offset = gl_WorkGroupID.x * gl_WorkGroupSize.x;
-
-    particle_data[gl_LocalInvocationIndex].position = inParticle[id].position;
-    particle_data[gl_LocalInvocationIndex].density  = inParticle[id].density;
-    particle_data[gl_LocalInvocationIndex].velocity = inParticle[id].velocity;
-    particle_data[gl_LocalInvocationIndex].pressure = inParticle[id].pressure;
+    uint group_size = min(index_offset + gl_WorkGroupSize.x, int(particleCount)) - index_offset;
 
     memoryBarrierShared();
+    barrier();
 
     const float h6 = pow(h, 6);
     externalForce = particle_data[gl_LocalInvocationIndex].density * gravity * vec3(-gravityDir.x,gravityDir.y,gravityDir.z);
 
-    for(uint j = 1; j < gl_WorkGroupSize.x; j++) {
-        uint i = (gl_LocalInvocationIndex + j) % gl_WorkGroupSize.x;
+    for(uint j = 1; j < group_size; j++) {
+        uint i = (gl_LocalInvocationIndex + j) % group_size;
 
         vec3 dir = particle_data[gl_LocalInvocationIndex].position - particle_data[i].position;
         float dist = length(dir);
@@ -119,7 +123,7 @@ void main() {
         }
     }
 
-    for(uint i = index_offset + gl_WorkGroupSize.x; i < int(particleCount); i++)
+    for(uint i = index_offset + group_size; i < int(particleCount); i++)
     {
         vec3 dir = particle_data[gl_LocalInvocationIndex].position - inParticle[i].position;
         float dist = length(dir);
@@ -138,9 +142,11 @@ void main() {
 
     viscosityForce *= viscosity;
 
-    outParticle[id].force = externalForce + pressureForce + viscosityForce;
-    outParticle[id].density = particle_data[gl_LocalInvocationIndex].density;
-    outParticle[id].pressure = particle_data[gl_LocalInvocationIndex].pressure;
-    outParticle[id].position = particle_data[gl_LocalInvocationIndex].position;
-    outParticle[id].velocity = particle_data[gl_LocalInvocationIndex].velocity;
+    if (id < int(particleCount)) {
+        outParticle[id].force = externalForce + pressureForce + viscosityForce;
+        outParticle[id].density = particle_data[gl_LocalInvocationIndex].density;
+        outParticle[id].pressure = particle_data[gl_LocalInvocationIndex].pressure;
+        outParticle[id].position = particle_data[gl_LocalInvocationIndex].position;
+        outParticle[id].velocity = particle_data[gl_LocalInvocationIndex].velocity;
+    }
 }
diff --git a/projects/sph/shaders/pressure.comp b/projects/sph/shaders/pressure.comp
index eb2029e35760d732ac8b4095d0ef41819a256736..8fa2e4762bddb3b9b28d8a3c184ceaaf7ab4421c 100644
--- a/projects/sph/shaders/pressure.comp
+++ b/projects/sph/shaders/pressure.comp
@@ -53,19 +53,20 @@ void main() {
     
     uint id = gl_GlobalInvocationID.x;
 
-    if(id >= int(particleCount))
-    {
-        return;
+    if (id >= int(particleCount)) {
+        position_data[gl_LocalInvocationIndex] = vec3(0.0f);
+    } else {
+        position_data[gl_LocalInvocationIndex] = inParticle[id].position;
     }
 
     uint index_offset = gl_WorkGroupID.x * gl_WorkGroupSize.x;
-
-    position_data[gl_LocalInvocationIndex] = inParticle[id].position;
+    uint group_size = min(index_offset + gl_WorkGroupSize.x, int(particleCount)) - index_offset;
 
     memoryBarrierShared();
+    barrier();
 
-    for(uint j = 1; j < gl_WorkGroupSize.x; j++) {
-        uint i = (gl_LocalInvocationIndex + j) % gl_WorkGroupSize.x;
+    for(uint j = 1; j < group_size; j++) {
+        uint i = (gl_LocalInvocationIndex + j) % group_size;
 
         float dist = distance(position_data[gl_LocalInvocationIndex], position_data[i]);
         densitySum += mass * poly6(dist);
@@ -77,15 +78,17 @@ void main() {
         densitySum += mass * poly6(dist);
     }
 
-    for(uint i = index_offset + gl_WorkGroupSize.x; i < int(particleCount); i++)
+    for(uint i = index_offset + group_size; i < int(particleCount); i++)
     {
         float dist = distance(position_data[gl_LocalInvocationIndex], inParticle[i].position);
         densitySum += mass * poly6(dist);
     }
 
-    outParticle[id].density = max(densitySum,0.0000001f);
-    outParticle[id].pressure = max((densitySum - offset), 0.0000001f) * gasConstant;
-    outParticle[id].position = position_data[gl_LocalInvocationIndex];
-    outParticle[id].velocity = inParticle[id].velocity;
-    outParticle[id].force = inParticle[id].force;
+    if (id < int(particleCount)) {
+        outParticle[id].density = max(densitySum, 0.0000001f);
+        outParticle[id].pressure = max((densitySum - offset), 0.0000001f) * gasConstant;
+        outParticle[id].position = position_data[gl_LocalInvocationIndex];
+        outParticle[id].velocity = inParticle[id].velocity;
+        outParticle[id].force = inParticle[id].force;
+    }
 }
diff --git a/projects/sph/src/BloomAndFlares.cpp b/projects/sph/src/BloomAndFlares.cpp
index 0af3bf8cc132db891e070a0068183a702061ee1d..200c0dea16a0b1483a8b20786902b38a43b5f825 100644
--- a/projects/sph/src/BloomAndFlares.cpp
+++ b/projects/sph/src/BloomAndFlares.cpp
@@ -114,7 +114,7 @@ void BloomAndFlares::execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStre
             cmdStream,
             m_DownsamplePipe,
             initialDispatchCount,
-            {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_DownsampleDescSets[0]).vulkanHandle)},
+            {vkcv::DescriptorSetUsage(0, m_DownsampleDescSets[0])},
             vkcv::PushConstants(0));
 
     // downsample dispatches of blur buffer's mip maps
@@ -149,7 +149,7 @@ void BloomAndFlares::execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStre
                 cmdStream,
                 m_DownsamplePipe,
                 mipDispatchCount,
-                {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_DownsampleDescSets[mipLevel]).vulkanHandle)},
+                {vkcv::DescriptorSetUsage(0, m_DownsampleDescSets[mipLevel])},
                 vkcv::PushConstants(0));
 
         // image barrier between mips
@@ -194,7 +194,7 @@ void BloomAndFlares::execUpsamplePipe(const vkcv::CommandStreamHandle &cmdStream
                 cmdStream,
                 m_UpsamplePipe,
                 upsampleDispatchCount,
-                {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_UpsampleDescSets[mipLevel]).vulkanHandle)},
+                {vkcv::DescriptorSetUsage(0, m_UpsampleDescSets[mipLevel])},
                 vkcv::PushConstants(0)
         );
         // image barrier between mips
@@ -226,7 +226,7 @@ void BloomAndFlares::execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStr
             cmdStream,
             m_LensFlarePipe,
             lensFeatureDispatchCount,
-            {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_LensFlareDescSet).vulkanHandle)},
+            {vkcv::DescriptorSetUsage(0, m_LensFlareDescSet)},
             vkcv::PushConstants(0));
 }
 
@@ -259,7 +259,7 @@ void BloomAndFlares::execCompositePipe(const vkcv::CommandStreamHandle &cmdStrea
             cmdStream,
             m_CompositePipe,
             compositeDispatchCount,
-            {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_CompositeDescSet).vulkanHandle)},
+            {vkcv::DescriptorSetUsage(0, m_CompositeDescSet)},
             vkcv::PushConstants(0));
 }
 
diff --git a/projects/sph/src/main.cpp b/projects/sph/src/main.cpp
index 96968776a6aea9aaa12abfea1bbc966a43548491..44d38850a134327e1bd4733d45e9e2aed295f8b3 100644
--- a/projects/sph/src/main.cpp
+++ b/projects/sph/src/main.cpp
@@ -204,7 +204,7 @@ int main(int argc, const char **argv) {
 
     const vkcv::Mesh renderMesh({vertexBufferBindings}, particleIndexBuffer.getVulkanHandle(),
                                 particleIndexBuffer.getCount());
-    vkcv::DescriptorSetUsage descriptorUsage(0, core.getDescriptorSet(descriptorSet).vulkanHandle);
+    vkcv::DescriptorSetUsage descriptorUsage(0, descriptorSet);
 
     auto pos = glm::vec2(0.f);
 
@@ -329,7 +329,7 @@ int main(int argc, const char **argv) {
         core.recordComputeDispatchToCmdStream(cmdStream,
                                               computePipeline1,
                                               computeDispatchCount,
-                                              {vkcv::DescriptorSetUsage(0,core.getDescriptorSet(computeDescriptorSet1).vulkanHandle)},
+                                              {vkcv::DescriptorSetUsage(0, computeDescriptorSet1)},
 											  pushConstantsCompute);
 
         core.recordBufferMemoryBarrier(cmdStream, particleBuffer1.getHandle());
@@ -338,7 +338,7 @@ int main(int argc, const char **argv) {
 		core.recordComputeDispatchToCmdStream(cmdStream,
 											  computePipeline2,
 											  computeDispatchCount,
-											  {vkcv::DescriptorSetUsage(0,core.getDescriptorSet(computeDescriptorSet2).vulkanHandle)},
+											  {vkcv::DescriptorSetUsage(0, computeDescriptorSet2)},
 											  pushConstantsCompute);
 
 		core.recordBufferMemoryBarrier(cmdStream, particleBuffer1.getHandle());
@@ -347,7 +347,7 @@ int main(int argc, const char **argv) {
         core.recordComputeDispatchToCmdStream(cmdStream,
                                               computePipeline3,
                                               computeDispatchCount,
-                                              { vkcv::DescriptorSetUsage(0,core.getDescriptorSet(computeDescriptorSet3).vulkanHandle) },
+                                              { vkcv::DescriptorSetUsage(0, computeDescriptorSet3) },
                                               pushConstantsCompute);
 
         core.recordBufferMemoryBarrier(cmdStream, particleBuffer1.getHandle());
@@ -356,7 +356,7 @@ int main(int argc, const char **argv) {
         core.recordComputeDispatchToCmdStream(cmdStream,
                                               computePipeline4,
                                               computeDispatchCount,
-                                              { vkcv::DescriptorSetUsage(0,core.getDescriptorSet(computeDescriptorSet4).vulkanHandle) },
+                                              { vkcv::DescriptorSetUsage(0, computeDescriptorSet4) },
                                               pushConstantsCompute);
 
         core.recordBufferMemoryBarrier(cmdStream, particleBuffer1.getHandle());
@@ -397,7 +397,7 @@ int main(int argc, const char **argv) {
             cmdStream, 
             tonemappingPipe, 
             tonemappingDispatchCount, 
-            {vkcv::DescriptorSetUsage(0, core.getDescriptorSet(tonemappingDescriptor).vulkanHandle) },
+            {vkcv::DescriptorSetUsage(0, tonemappingDescriptor) },
             vkcv::PushConstants(0));
 
         core.prepareSwapchainImageForPresent(cmdStream);
diff --git a/projects/voxelization/src/BloomAndFlares.cpp b/projects/voxelization/src/BloomAndFlares.cpp
index a7ac2904c173d84d45ffec1d03f3c37fef20c76a..ddb1326ae83c8bd596ce61dc1c47b81b5ddb17be 100644
--- a/projects/voxelization/src/BloomAndFlares.cpp
+++ b/projects/voxelization/src/BloomAndFlares.cpp
@@ -145,7 +145,7 @@ void BloomAndFlares::execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStre
             cmdStream,
             m_DownsamplePipe,
             initialDispatchCount,
-            {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_DownsampleDescSets[0]).vulkanHandle)},
+            {vkcv::DescriptorSetUsage(0, m_DownsampleDescSets[0])},
             vkcv::PushConstants(0));
 
     // downsample dispatches of blur buffer's mip maps
@@ -180,7 +180,7 @@ void BloomAndFlares::execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStre
                 cmdStream,
                 m_DownsamplePipe,
                 mipDispatchCount,
-                {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_DownsampleDescSets[mipLevel]).vulkanHandle)},
+                {vkcv::DescriptorSetUsage(0, m_DownsampleDescSets[mipLevel])},
                 vkcv::PushConstants(0));
 
         // image barrier between mips
@@ -229,7 +229,7 @@ void BloomAndFlares::execUpsamplePipe(const vkcv::CommandStreamHandle &cmdStream
                 cmdStream,
                 m_UpsamplePipe,
                 upsampleDispatchCount,
-                {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_UpsampleDescSets[mipLevel]).vulkanHandle)},
+                {vkcv::DescriptorSetUsage(0, m_UpsampleDescSets[mipLevel])},
                 vkcv::PushConstants(0)
         );
         // image barrier between mips
@@ -268,7 +268,7 @@ void BloomAndFlares::execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStr
             cmdStream,
             m_LensFlarePipe,
             lensFeatureDispatchCount,
-            {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_LensFlareDescSet).vulkanHandle)},
+            {vkcv::DescriptorSetUsage(0, m_LensFlareDescSet)},
             vkcv::PushConstants(0));
 
     // upsample dispatch
@@ -301,7 +301,7 @@ void BloomAndFlares::execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStr
             cmdStream,
             m_UpsamplePipe,
             upsampleDispatchCount,
-            { vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_UpsampleLensFlareDescSets[i]).vulkanHandle) },
+            { vkcv::DescriptorSetUsage(0, m_UpsampleLensFlareDescSets[i]) },
             vkcv::PushConstants(0)
         );
         // image barrier between mips
@@ -348,7 +348,7 @@ void BloomAndFlares::execCompositePipe(const vkcv::CommandStreamHandle &cmdStrea
         cmdStream,
         m_CompositePipe,
         compositeDispatchCount,
-        {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_CompositeDescSet).vulkanHandle)},
+        {vkcv::DescriptorSetUsage(0, m_CompositeDescSet)},
         pushConstants);
 
     p_Core->recordEndDebugLabel(cmdStream);
diff --git a/projects/voxelization/src/ShadowMapping.cpp b/projects/voxelization/src/ShadowMapping.cpp
index c2e1631098146c8ee82586995a5a9d21e6d85252..5ae7eb6047200b2cdb7a3ac38ce8512cdcaa3d53 100644
--- a/projects/voxelization/src/ShadowMapping.cpp
+++ b/projects/voxelization/src/ShadowMapping.cpp
@@ -296,7 +296,7 @@ void ShadowMapping::recordShadowMapRendering(
 		cmdStream,
 		m_depthToMomentsPipe,
 		dispatchCount,
-		{ vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_depthToMomentsDescriptorSet).vulkanHandle) },
+		{ vkcv::DescriptorSetUsage(0, m_depthToMomentsDescriptorSet) },
 		msaaPushConstants);
 	m_corePtr->prepareImageForSampling(cmdStream, m_shadowMap.getHandle());
 	m_corePtr->recordEndDebugLabel(cmdStream);
@@ -309,7 +309,7 @@ void ShadowMapping::recordShadowMapRendering(
 		cmdStream,
 		m_shadowBlurXPipe,
 		dispatchCount,
-		{ vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_shadowBlurXDescriptorSet).vulkanHandle) },
+		{ vkcv::DescriptorSetUsage(0, m_shadowBlurXDescriptorSet) },
 		vkcv::PushConstants(0));
 	m_corePtr->prepareImageForSampling(cmdStream, m_shadowMapIntermediate.getHandle());
 
@@ -319,7 +319,7 @@ void ShadowMapping::recordShadowMapRendering(
 		cmdStream,
 		m_shadowBlurYPipe,
 		dispatchCount,
-		{ vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_shadowBlurYDescriptorSet).vulkanHandle) },
+		{ vkcv::DescriptorSetUsage(0, m_shadowBlurYDescriptorSet) },
 		vkcv::PushConstants(0));
 	m_shadowMap.recordMipChainGeneration(cmdStream);
 	m_corePtr->prepareImageForSampling(cmdStream, m_shadowMap.getHandle());
diff --git a/projects/voxelization/src/Voxelization.cpp b/projects/voxelization/src/Voxelization.cpp
index c5d48fb350226896546c1e9ae23bc5bff656aca4..faa03d38127d5a23c931fdef0470c1e24131d206 100644
--- a/projects/voxelization/src/Voxelization.cpp
+++ b/projects/voxelization/src/Voxelization.cpp
@@ -168,7 +168,7 @@ Voxelization::Voxelization(
 		voxelIndexData.push_back(static_cast<uint16_t>(i));
 	}
 
-	const vkcv::DescriptorSetUsage voxelizationDescriptorUsage(0, m_corePtr->getDescriptorSet(m_visualisationDescriptorSet).vulkanHandle);
+	const vkcv::DescriptorSetUsage voxelizationDescriptorUsage(0, m_visualisationDescriptorSet);
 
 	vkcv::ShaderProgram resetVoxelShader = loadVoxelResetShader();
 
@@ -259,7 +259,7 @@ void Voxelization::voxelizeMeshes(
 		cmdStream,
 		m_voxelResetPipe,
 		resetVoxelDispatchCount,
-		{ vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_voxelResetDescriptorSet).vulkanHandle) },
+		{ vkcv::DescriptorSetUsage(0, m_voxelResetDescriptorSet) },
 		voxelCountPushConstants);
 	m_corePtr->recordBufferMemoryBarrier(cmdStream, m_voxelBuffer.getHandle());
 	m_corePtr->recordEndDebugLabel(cmdStream);
@@ -270,8 +270,8 @@ void Voxelization::voxelizeMeshes(
 		drawcalls.push_back(vkcv::DrawcallInfo(
 			meshes[i],
 			{ 
-				vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_voxelizationDescriptorSet).vulkanHandle),
-				vkcv::DescriptorSetUsage(1, m_corePtr->getDescriptorSet(perMeshDescriptorSets[i]).vulkanHandle) 
+				vkcv::DescriptorSetUsage(0, m_voxelizationDescriptorSet),
+				vkcv::DescriptorSetUsage(1, perMeshDescriptorSets[i])
 			},1));
 	}
 
@@ -299,7 +299,7 @@ void Voxelization::voxelizeMeshes(
 		cmdStream,
 		m_bufferToImagePipe,
 		bufferToImageDispatchCount,
-		{ vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_bufferToImageDescriptorSet).vulkanHandle) },
+		{ vkcv::DescriptorSetUsage(0, m_bufferToImageDescriptorSet) },
 		vkcv::PushConstants(0));
 
 	m_corePtr->recordImageMemoryBarrier(cmdStream, m_voxelImageIntermediate.getHandle());
@@ -318,7 +318,7 @@ void Voxelization::voxelizeMeshes(
 		cmdStream,
 		m_secondaryBouncePipe,
 		bufferToImageDispatchCount,
-		{ vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_secondaryBounceDescriptorSet).vulkanHandle) },
+		{ vkcv::DescriptorSetUsage(0, m_secondaryBounceDescriptorSet) },
 		vkcv::PushConstants(0));
 	m_voxelImage.recordMipChainGeneration(cmdStream);
 	m_corePtr->recordImageMemoryBarrier(cmdStream, m_voxelImage.getHandle());
@@ -355,7 +355,7 @@ void Voxelization::renderVoxelVisualisation(
 
 	const auto drawcall = vkcv::DrawcallInfo(
 		vkcv::Mesh({}, nullptr, drawVoxelCount),
-		{ vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_visualisationDescriptorSet).vulkanHandle) },1);
+		{ vkcv::DescriptorSetUsage(0, m_visualisationDescriptorSet) },1);
 
 	m_corePtr->recordBeginDebugLabel(cmdStream, "Voxel visualisation", { 1, 1, 1, 1 });
 	m_corePtr->prepareImageForStorage(cmdStream, m_voxelImage.getHandle());
diff --git a/projects/voxelization/src/main.cpp b/projects/voxelization/src/main.cpp
index 7157d2f625d5d86d0f916dc3c1d6e4f4c596b546..2245419f87d196e913ba27e8e78ffff73aab04b6 100644
--- a/projects/voxelization/src/main.cpp
+++ b/projects/voxelization/src/main.cpp
@@ -527,11 +527,11 @@ int main(int argc, const char** argv) {
 	for (size_t i = 0; i < meshes.size(); i++) {
 
 		drawcalls.push_back(vkcv::DrawcallInfo(meshes[i], { 
-			vkcv::DescriptorSetUsage(0, core.getDescriptorSet(forwardShadingDescriptorSet).vulkanHandle),
-			vkcv::DescriptorSetUsage(1, core.getDescriptorSet(perMeshDescriptorSets[i]).vulkanHandle) }));
+			vkcv::DescriptorSetUsage(0, forwardShadingDescriptorSet),
+			vkcv::DescriptorSetUsage(1, perMeshDescriptorSets[i]) }));
 		prepassDrawcalls.push_back(vkcv::DrawcallInfo(meshes[i], {
-			vkcv::DescriptorSetUsage(0, core.getDescriptorSet(prepassDescriptorSet).vulkanHandle),
-			vkcv::DescriptorSetUsage(1, core.getDescriptorSet(perMeshDescriptorSets[i]).vulkanHandle) }));
+			vkcv::DescriptorSetUsage(0, prepassDescriptorSet),
+			vkcv::DescriptorSetUsage(1, perMeshDescriptorSets[i]) }));
 	}
 
 	vkcv::SamplerHandle voxelSampler = core.createSampler(
@@ -859,7 +859,7 @@ int main(int argc, const char** argv) {
 					cmdStream,
 					resolvePipeline,
 					fulsscreenDispatchCount,
-					{ vkcv::DescriptorSetUsage(0, core.getDescriptorSet(resolveDescriptorSet).vulkanHandle) },
+					{ vkcv::DescriptorSetUsage(0, resolveDescriptorSet) },
 					vkcv::PushConstants(0));
 
 				core.recordImageMemoryBarrier(cmdStream, resolvedColorBuffer);
@@ -882,9 +882,7 @@ int main(int argc, const char** argv) {
 			cmdStream, 
 			tonemappingPipeline, 
 			fulsscreenDispatchCount,
-			{ vkcv::DescriptorSetUsage(0, core.getDescriptorSet(
-					tonemappingDescriptorSet
-			).vulkanHandle) },
+			{ vkcv::DescriptorSetUsage(0, tonemappingDescriptorSet) },
 			vkcv::PushConstants(0)
 		);
 		
@@ -920,9 +918,7 @@ int main(int argc, const char** argv) {
 				cmdStream,
 				postEffectsPipeline,
 				fulsscreenDispatchCount,
-				{ vkcv::DescriptorSetUsage(0, core.getDescriptorSet(
-						postEffectsDescriptorSet
-				).vulkanHandle) },
+				{ vkcv::DescriptorSetUsage(0, postEffectsDescriptorSet) },
 				timePushConstants
 		);
 		core.recordEndDebugLabel(cmdStream);
diff --git a/projects/wobble_bobble/.gitignore b/projects/wobble_bobble/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..7ed07ed409373206a95d74e6dbdc27a4cb391fea
--- /dev/null
+++ b/projects/wobble_bobble/.gitignore
@@ -0,0 +1 @@
+wobble_bobble
diff --git a/projects/wobble_bobble/CMakeLists.txt b/projects/wobble_bobble/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..9842b13b1038d61414de5c2cf2a9107604b6fe16
--- /dev/null
+++ b/projects/wobble_bobble/CMakeLists.txt
@@ -0,0 +1,21 @@
+cmake_minimum_required(VERSION 3.16)
+project(wobble_bobble)
+
+# setting c++ standard for the project
+set(CMAKE_CXX_STANDARD 20)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+# this should fix the execution path to load local files from the project
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
+
+# adding source files to the project
+add_executable(wobble_bobble
+		src/main.cpp)
+
+fix_project(wobble_bobble)
+
+# including headers of dependencies and the VkCV framework
+target_include_directories(wobble_bobble SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_camera_include} ${vkcv_gui_include} ${vkcv_shader_compiler_include})
+
+# linking with libraries from all dependencies and the VkCV framework
+target_link_libraries(wobble_bobble vkcv vkcv_camera vkcv_gui vkcv_shader_compiler)
diff --git a/projects/wobble_bobble/shaders/.gitignore b/projects/wobble_bobble/shaders/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/projects/wobble_bobble/shaders/grid.frag b/projects/wobble_bobble/shaders/grid.frag
new file mode 100644
index 0000000000000000000000000000000000000000..8eb2fbc2173bbddb5bdc44c52ce81d2e5d52d389
--- /dev/null
+++ b/projects/wobble_bobble/shaders/grid.frag
@@ -0,0 +1,30 @@
+#version 450
+
+layout(location = 0) in vec2 passPos;
+layout(location = 1) in vec3 passVelocity;
+layout(location = 2) in float passMass;
+
+layout(location = 0) out vec3 outColor;
+
+void main()	{
+    if (passMass <= 0.0f) {
+        discard;
+    }
+
+    const float value = length(passPos);
+
+    float z = sqrt(0.25 - value * value);
+
+    if (value < 0.5f) {
+        vec3 surface = vec3(passPos.x + 0.5f, passPos.y + 0.5f, z * 2.0f);
+        vec3 velocity = vec3(0.5f);
+
+        if (length(passVelocity) > 0.0f) {
+            velocity = vec3(0.5f) + 0.5f * normalize(passVelocity.xyz);
+        }
+
+        outColor = velocity;
+    } else {
+        discard;
+    }
+}
\ No newline at end of file
diff --git a/projects/wobble_bobble/shaders/grid.vert b/projects/wobble_bobble/shaders/grid.vert
new file mode 100644
index 0000000000000000000000000000000000000000..54de3f3e1da43e3ea3d018e5c54003b83e055c4c
--- /dev/null
+++ b/projects/wobble_bobble/shaders/grid.vert
@@ -0,0 +1,58 @@
+#version 450
+#extension GL_GOOGLE_include_directive : enable
+
+#include "particle.inc"
+
+layout(set=0, binding=0) uniform texture3D gridImage;
+layout(set=0, binding=1) uniform sampler gridSampler;
+
+layout(set=0, binding=2) uniform simulationBlock {
+    Simulation simulation;
+};
+
+layout(location = 0) in vec2 vertexPos;
+
+layout(location = 0) out vec2 passPos;
+layout(location = 1) out vec3 passVelocity;
+layout(location = 2) out float passMass;
+
+layout( push_constant ) uniform constants{
+    mat4 mvp;
+};
+
+ivec3 actual_mod(ivec3 x, ivec3 y) {
+    return x - y * (x/y);
+}
+
+void main()	{
+    ivec3 gridResolution = textureSize(sampler3D(gridImage, gridSampler), 0);
+
+    ivec3 gridID = ivec3(
+        gl_InstanceIndex,
+        gl_InstanceIndex / gridResolution.x,
+        gl_InstanceIndex / gridResolution.x / gridResolution.y
+    );
+
+    gridID = actual_mod(gridID, gridResolution);
+
+    vec3 position = (vec3(gridID) + vec3(0.5f)) / gridResolution;
+
+    vec3 size = vec3(1.0f) / vec3(gridResolution);
+    float volume = size.x * size.y * size.z;
+    float radius = cube_radius(volume);
+
+    vec4 gridData = texture(sampler3D(gridImage, gridSampler), position);
+
+    float mass = gridData.w;
+    float density = mass / volume;
+
+    float alpha = clamp(density / simulation.density, 0.0f, 1.0f);
+
+    passPos = vertexPos;
+    passVelocity = gridData.xyz;
+    passMass = mass;
+
+    // align voxel to face camera
+    gl_Position = mvp * vec4(position, 1);      // transform position into projected view space
+    gl_Position.xy += vertexPos * (radius * 2.0f) * alpha;  // move position directly in view space
+}
\ No newline at end of file
diff --git a/projects/wobble_bobble/shaders/init_particle_weights.comp b/projects/wobble_bobble/shaders/init_particle_weights.comp
new file mode 100644
index 0000000000000000000000000000000000000000..9b821e88fc7cc3fcea87b7eb6de0f8f6d629d911
--- /dev/null
+++ b/projects/wobble_bobble/shaders/init_particle_weights.comp
@@ -0,0 +1,43 @@
+#version 450
+#extension GL_GOOGLE_include_directive : enable
+
+layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in;
+
+#include "particle.inc"
+
+layout(set=0, binding=0, std430) restrict buffer particleBuffer {
+    Particle particles [];
+};
+
+layout(set=1, binding=0) uniform texture3D gridImage;
+layout(set=1, binding=1) uniform sampler gridSampler;
+
+void main()	{
+    if (gl_GlobalInvocationID.x < particles.length()) {
+        ParticleMinimal minimal = particles[gl_GlobalInvocationID.x].minimal;
+
+        minimal.weight_sum = 1.0f;
+
+        ivec3 gridResolution = textureSize(sampler3D(gridImage, gridSampler), 0);
+        ivec3 gridWindow = ivec3(minimal.size * 2.0f * gridResolution);
+
+        float weight_sum = 0.0f;
+
+        int i, j, k;
+
+        for (i = -gridWindow.x; i <= gridWindow.x; i++) {
+            for (j = -gridWindow.y; j <= gridWindow.y; j++) {
+                for (k = -gridWindow.z; k <= gridWindow.z; k++) {
+                    vec3 offset = vec3(i, j, k) / gridResolution;
+                    vec3 voxel = minimal.position + offset;
+
+                    weight_sum += voxel_particle_weight(voxel, minimal);
+                }
+            }
+        }
+
+        if (weight_sum > 0.0f) {
+            particles[gl_GlobalInvocationID.x].minimal.weight_sum = weight_sum;
+        }
+    }
+}
\ No newline at end of file
diff --git a/projects/wobble_bobble/shaders/lines.frag b/projects/wobble_bobble/shaders/lines.frag
new file mode 100644
index 0000000000000000000000000000000000000000..37d79d30d2ce751a59b1b467764b2fe464aa5c17
--- /dev/null
+++ b/projects/wobble_bobble/shaders/lines.frag
@@ -0,0 +1,7 @@
+#version 450
+
+layout(location = 0) out vec3 outColor;
+
+void main() {
+    outColor = vec3(1.0f);
+}
\ No newline at end of file
diff --git a/projects/wobble_bobble/shaders/lines.vert b/projects/wobble_bobble/shaders/lines.vert
new file mode 100644
index 0000000000000000000000000000000000000000..b8e3b01c67986156ad980899697ffea05409b752
--- /dev/null
+++ b/projects/wobble_bobble/shaders/lines.vert
@@ -0,0 +1,11 @@
+#version 450
+
+layout(location = 0) in vec3 vertexPos;
+
+layout( push_constant ) uniform constants{
+    mat4 mvp;
+};
+
+void main() {
+    gl_Position = mvp * vec4(vertexPos, 1);
+}
\ No newline at end of file
diff --git a/projects/wobble_bobble/shaders/particle.frag b/projects/wobble_bobble/shaders/particle.frag
new file mode 100644
index 0000000000000000000000000000000000000000..81c0a3594359e816a28b5e3f0301b47ce74a3cd7
--- /dev/null
+++ b/projects/wobble_bobble/shaders/particle.frag
@@ -0,0 +1,18 @@
+#version 450
+
+layout(location = 0) in vec2 passPos;
+layout(location = 1) in float passMass;
+
+layout(location = 0) out vec3 outColor;
+
+void main()	{
+    const float value = length(passPos);
+
+    float z = sqrt(0.25 - value * value);
+
+    if (value < 0.5f) {
+        outColor = vec3(passPos.x + 0.5f, passPos.y + 0.5f, z * 2.0f);
+    } else {
+        discard;
+    }
+}
\ No newline at end of file
diff --git a/projects/wobble_bobble/shaders/particle.inc b/projects/wobble_bobble/shaders/particle.inc
new file mode 100644
index 0000000000000000000000000000000000000000..a622485eab5bbcafdc51c030281e53b1cc1c5f11
--- /dev/null
+++ b/projects/wobble_bobble/shaders/particle.inc
@@ -0,0 +1,118 @@
+#ifndef PARTICLE_INC
+#define PARTICLE_INC
+
+#define EPSILON 0.00000001f
+
+struct ParticleMinimal {
+    vec3 position;
+    float size;
+    vec3 velocity;
+    float mass;
+	
+	vec3 pad;
+	float weight_sum;
+};
+
+struct Particle {
+    ParticleMinimal minimal;
+    mat4 deformation;
+	mat4 mls;
+};
+
+#define SIM_FORM_SPHERE 0
+#define SIM_FORM_CUBE 1
+
+#define SIM_TYPE_HYPERELASTIC 0
+#define SIM_TYPE_FLUID 1
+
+#define SIM_MODE_RANDOM 0
+#define SIM_MODE_ORDERED 1
+
+struct Simulation {
+	float density;
+	float size;
+	float lame1;
+	float lame2;
+	
+	int form;
+	int type;
+	float K;
+	float E;
+	
+	float gamma;
+	int mode;
+	float gravity;
+	int count;
+};
+
+const float PI = 3.1415926535897932384626433832795;
+
+float sphere_volume(float radius) {
+	return 4.0f * (radius * radius * radius) * PI / 3.0f;
+}
+
+float sphere_radius(float volume) {
+	return pow(volume * 3.0f / 4.0f / PI, 1.0f / 3.0f);
+}
+
+float cube_volume(float radius) {
+	return 8.0f * (radius * radius * radius);
+}
+
+float cube_radius(float volume) {
+	return pow(volume / 8.0f, 1.0f / 3.0f);
+}
+
+float weight_A(float x) {
+	return max(1.0f - x, 0.0f);
+}
+
+float weight_B(float x) {
+	if (x < 0.5f) {
+		return 0.75f - x * x;
+	} else
+	if (x < 1.5f) {
+		float y = (1.5f - x);
+		return 0.5f * y * y;
+	} else {
+		return 0.0f;
+	}
+}
+
+float weight_C(float x) {
+	if (x < 1.0f) {
+		return (0.5f * x - 1.0f) * x*x + 2.0f / 3.0f;
+	} else
+	if (x < 2.0f) {
+		float y = (2.0f - x);
+		return 0.5f / 3.0f * y * y * y;
+	} else {
+		return 0.0f;
+	}
+}
+
+float voxel_particle_weight(vec3 voxel, ParticleMinimal particle) {
+	vec3 delta = abs(particle.position - voxel) / particle.size;
+	
+	if (any(isnan(delta)) || any(isinf(delta))) {
+		return 0.0f;
+	}
+	
+	vec3 weight = vec3(
+		weight_C(delta.x),
+		weight_C(delta.y),
+		weight_C(delta.z)
+	);
+	
+	float result = (
+		weight.x * weight.y * weight.z
+	) / particle.weight_sum;
+	
+	if ((isnan(result)) || (isinf(result))) {
+		return 0.0f;
+	} else {
+		return result;
+	}
+}
+
+#endif // PARTICLE_INC
\ No newline at end of file
diff --git a/projects/wobble_bobble/shaders/particle.vert b/projects/wobble_bobble/shaders/particle.vert
new file mode 100644
index 0000000000000000000000000000000000000000..a8f697e79eacba361d80b5858405add675e761bb
--- /dev/null
+++ b/projects/wobble_bobble/shaders/particle.vert
@@ -0,0 +1,31 @@
+#version 450
+#extension GL_GOOGLE_include_directive : enable
+
+#include "particle.inc"
+
+layout(set=0, binding=0, std430) readonly buffer particleBuffer {
+    Particle particles [];
+};
+
+layout(location = 0) in vec2 vertexPos;
+
+layout(location = 0) out vec2 passPos;
+layout(location = 1) out float passMass;
+
+layout( push_constant ) uniform constants{
+    mat4 mvp;
+};
+
+void main()	{
+    vec3 position = particles[gl_InstanceIndex].minimal.position;
+    float size = particles[gl_InstanceIndex].minimal.size;
+
+    float mass = particles[gl_InstanceIndex].minimal.mass;
+
+    passPos = vertexPos;
+    passMass = mass;
+
+    // align particle to face camera
+    gl_Position = mvp * vec4(position, 1);      // transform position into projected view space
+    gl_Position.xy += vertexPos * size * 2.0f;  // move position directly in view space
+}
\ No newline at end of file
diff --git a/projects/wobble_bobble/shaders/transform_particles_to_grid.comp b/projects/wobble_bobble/shaders/transform_particles_to_grid.comp
new file mode 100644
index 0000000000000000000000000000000000000000..1be18c41303ab2208c8cb4a8c33f41b350315d63
--- /dev/null
+++ b/projects/wobble_bobble/shaders/transform_particles_to_grid.comp
@@ -0,0 +1,101 @@
+#version 450
+#extension GL_GOOGLE_include_directive : enable
+
+layout(local_size_x = 4, local_size_y = 4, local_size_z = 4) in;
+
+#include "particle.inc"
+
+layout(set=0, binding=0, std430) readonly buffer particleBuffer {
+    Particle particles [];
+};
+
+layout(set=1, binding=0) uniform simulationBlock {
+    Simulation simulation;
+};
+
+layout(set=2, binding=0, rgba16f) restrict writeonly uniform image3D gridImage;
+
+layout( push_constant ) uniform constants {
+    float t;
+    float dt;
+    float speedfactor;
+};
+
+#define SHARED_PARTICLES_BATCH_SIZE 64
+
+shared ParticleMinimal shared_particles [SHARED_PARTICLES_BATCH_SIZE];
+
+void main()	{
+    const vec3 position = (vec3(gl_GlobalInvocationID) + vec3(0.5f)) / imageSize(gridImage);
+
+    float dts = dt * speedfactor;
+
+    vec4 gridValue = vec4(0.0f);
+
+    uint offset = 0;
+
+    for (offset = 0; offset < particles.length(); offset += SHARED_PARTICLES_BATCH_SIZE) {
+        uint localOffset = offset + gl_LocalInvocationIndex;
+
+        if (localOffset < particles.length()) {
+            shared_particles[gl_LocalInvocationIndex] = particles[localOffset].minimal;
+
+            shared_particles[gl_LocalInvocationIndex].pad = (
+                mat3(particles[localOffset].mls) *
+                (position - shared_particles[gl_LocalInvocationIndex].position)
+            ) + (
+                shared_particles[gl_LocalInvocationIndex].velocity *
+                shared_particles[gl_LocalInvocationIndex].mass
+            );
+        } else {
+            shared_particles[gl_LocalInvocationIndex].position = vec3(0.0f);
+            shared_particles[gl_LocalInvocationIndex].size = 0.0f;
+            shared_particles[gl_LocalInvocationIndex].velocity = vec3(0.0f);
+            shared_particles[gl_LocalInvocationIndex].mass = 0.0f;
+
+            shared_particles[gl_LocalInvocationIndex].pad = vec3(0.0f);
+            shared_particles[gl_LocalInvocationIndex].weight_sum = 1.0f;
+        }
+
+        barrier();
+        memoryBarrierShared();
+
+        for (uint i = 0; i < SHARED_PARTICLES_BATCH_SIZE; i++) {
+            float weight = voxel_particle_weight(position, shared_particles[i]);
+
+            gridValue += vec4(
+                shared_particles[i].pad * weight,
+                shared_particles[i].mass * weight
+            );
+        }
+
+        barrier();
+        memoryBarrierShared();
+    }
+
+    if (any(isnan(gridValue.xyz)) || any(isinf(gridValue.xyz))) {
+        gridValue.xyz = vec3(0.0f);
+    }
+
+    gridValue.xyz += vec3(0.0f, -simulation.gravity * dts * gridValue.w, 0.0f);
+
+    bvec3 lowerID = lessThanEqual(gl_GlobalInvocationID, ivec3(0));
+    bvec3 negativeVelocity = lessThan(gridValue.xyz, vec3(0.0f));
+
+    bvec3 greaterID = greaterThanEqual(gl_GlobalInvocationID + ivec3(1), imageSize(gridImage));
+    bvec3 positiveVelocity = greaterThan(gridValue.xyz, vec3(0.0f));
+
+    bvec3 collision = bvec3(
+        (lowerID.x && negativeVelocity.x) || (greaterID.x && positiveVelocity.x),
+        (lowerID.y && negativeVelocity.y) || (greaterID.y && positiveVelocity.y),
+        (lowerID.z && negativeVelocity.z) || (greaterID.z && positiveVelocity.z)
+    );
+
+    gridValue.xyz = mix(gridValue.xyz, -gridValue.xyz, collision);
+
+    imageStore(
+        gridImage,
+        ivec3(gl_GlobalInvocationID),
+        gridValue
+    );
+}
\ No newline at end of file
diff --git a/projects/wobble_bobble/shaders/update_particle_velocities.comp b/projects/wobble_bobble/shaders/update_particle_velocities.comp
new file mode 100644
index 0000000000000000000000000000000000000000..4420bc9575bcde4d04d7d4f5531d0091362285c2
--- /dev/null
+++ b/projects/wobble_bobble/shaders/update_particle_velocities.comp
@@ -0,0 +1,161 @@
+#version 450
+#extension GL_GOOGLE_include_directive : enable
+#extension GL_EXT_control_flow_attributes : enable
+
+layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in;
+
+#include "particle.inc"
+
+layout(set=0, binding=0, std430) restrict buffer particleBuffer {
+    Particle particles [];
+};
+
+layout(set=1, binding=0) uniform simulationBlock {
+    Simulation simulation;
+};
+
+layout(set=2, binding=0) uniform texture3D gridImage;
+layout(set=2, binding=1) uniform sampler gridSampler;
+
+layout( push_constant ) uniform constants {
+    float t;
+    float dt;
+    float speedfactor;
+};
+
+void main()	{
+    float dts = dt * speedfactor;
+
+    if (gl_GlobalInvocationID.x < particles.length()) {
+        Particle particle = particles[gl_GlobalInvocationID.x];
+
+        vec3 position = particle.minimal.position;
+        float size = particle.minimal.size;
+        float mass = particle.minimal.mass;
+
+        ivec3 gridResolution = textureSize(sampler3D(gridImage, gridSampler), 0);
+        ivec3 gridWindow = ivec3(size * 2.0f * gridResolution);
+
+        mat3 affine_D = mat3(0.0f);
+        mat3 affine_B = mat3(0.0f);
+
+        vec3 velocity_pic = vec3(0.0f);
+        vec3 velocity_flip = vec3(particle.minimal.velocity);
+
+        int i, j, k;
+
+        for (i = -gridWindow.x; i <= gridWindow.x; i++) {
+            for (j = -gridWindow.y; j <= gridWindow.y; j++) {
+                for (k = -gridWindow.z; k <= gridWindow.z; k++) {
+                    vec3 offset = vec3(i, j, k) / gridResolution;
+                    vec3 voxel = position + offset;
+
+                    vec4 gridSample = texture(sampler3D(gridImage, gridSampler), voxel);
+
+                    float weight = voxel_particle_weight(voxel, particle.minimal);
+                    vec3 velocity = gridSample.xyz * weight / gridSample.w;
+
+                    if (any(isnan(velocity)) || any(isinf(velocity))) {
+                        velocity = vec3(0.0f);
+                    }
+
+                    affine_D += outerProduct(weight * offset, offset);
+                    affine_B += outerProduct(velocity, offset);
+
+                    velocity_pic += velocity;
+                }
+            }
+        }
+
+        mat3 mls_Q = mat3(0.0f);
+        mat3 affine_C = mat3(0.0f);
+
+        mat3 F = mat3(particle.deformation);
+
+        mat3 D_inv = inverse(affine_D);
+        float D_det = determinant(D_inv);
+
+        if ((isnan(D_det)) || (isinf(D_det))) {
+            D_inv = mat3(0.0f);
+        } else {
+            D_inv *= min(abs(D_det), 1.0f / EPSILON) / abs(D_det);
+        }
+
+        float J = max(determinant(F), EPSILON);
+        float volume = sphere_volume(size);
+
+        mat3 stress = mat3(0.0f);
+
+        switch (simulation.type) {
+            case SIM_TYPE_HYPERELASTIC:
+                mat3 F_T = transpose(F);
+                mat3 F_T_inv = inverse(F_T);
+
+                mat3 P_term_0 = simulation.lame2 * (F - F_T_inv);
+                mat3 P_term_1 = simulation.lame1 * log(J) * F_T_inv;
+
+                mat3 P = P_term_0 + P_term_1;
+
+                stress = P * F_T;
+                break;
+            case SIM_TYPE_FLUID:
+                float pressure = simulation.K * (1.0f / pow(J, simulation.gamma) - 1.0f);
+
+                stress = mat3(-pressure * J);
+                break;
+            default:
+                break;
+        }
+
+        mls_Q -= dts * volume * stress * D_inv;
+
+        affine_C = affine_B * D_inv;
+        mls_Q += affine_C * mass;
+
+        F = (mat3(1.0f) + dts * affine_C) * F;
+
+        position = position + velocity_pic * dts;
+
+        const float gridRange = (1.0f - 2.0f * size);
+
+        for (uint i = 0; i < 3; i++) {
+            if (position[i] - size < 0.0f) {
+                float a = (size - position[i]) / gridRange;
+                int b = int(floor(a));
+
+                a = (a - b) * gridRange;
+
+                if (b % 2 == 0) {
+                    position[i] = size + a;
+                } else {
+                    position[i] = 1.0f - size - a;
+                }
+
+                if ((velocity_pic[i] < 0.0f) == (b % 2 == 0)) {
+                    velocity_pic[i] *= -1.0f;
+                }
+            } else
+            if (position[i] + size > 1.0f) {
+                float a = (position[i] + size - 1.0f) / gridRange;
+                int b = int(floor(a));
+
+                a = (a - b) * gridRange;
+
+                if (b % 2 == 0) {
+                    position[i] = 1.0f - size - a;
+                } else {
+                    position[i] = size + a;
+                }
+
+                if ((velocity_pic[i] > 0.0f) == (b % 2 == 0)) {
+                    velocity_pic[i] *= -1.0f;
+                }
+            }
+        }
+
+        particles[gl_GlobalInvocationID.x].minimal.position = position;
+        particles[gl_GlobalInvocationID.x].minimal.velocity = velocity_pic;
+        particles[gl_GlobalInvocationID.x].deformation = mat4(F);
+        particles[gl_GlobalInvocationID.x].mls = mat4(mls_Q);
+    }
+}
\ No newline at end of file
diff --git a/projects/wobble_bobble/src/main.cpp b/projects/wobble_bobble/src/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..96c367f2f8e22ac65bf6ccf6a5c09eb238b5604d
--- /dev/null
+++ b/projects/wobble_bobble/src/main.cpp
@@ -0,0 +1,905 @@
+
+#include <vkcv/Core.hpp>
+#include <vkcv/camera/CameraManager.hpp>
+#include <vkcv/gui/GUI.hpp>
+#include <vkcv/shader/GLSLCompiler.hpp>
+
+#include <random>
+
+struct Particle {
+	glm::vec3 position;
+	float size;
+	glm::vec3 velocity;
+	float mass;
+	
+	glm::vec3 pad;
+	float weight_sum;
+	
+	glm::mat4 deformation;
+	glm::mat4 mls;
+};
+
+#define SIM_FORM_SPHERE 0
+#define SIM_FORM_CUBE 1
+
+#define SIM_TYPE_HYPERELASTIC 0
+#define SIM_TYPE_FLUID 1
+
+#define SIM_MODE_RANDOM 0
+#define SIM_MODE_ORDERED 1
+
+struct Simulation {
+	float density;
+	float size;
+	float lame1;
+	float lame2;
+	
+	int form;
+	int type;
+	float K;
+	float E;
+	
+	float gamma;
+	int mode;
+	float gravity;
+	int count;
+};
+
+struct Physics {
+	float t;
+	float dt;
+	float speedfactor;
+};
+
+float sphere_volume(float radius) {
+	return 4.0f * (radius * radius * radius) * M_PI / 3.0f;
+}
+
+float sphere_radius(float volume) {
+	return std::pow(volume * 3.0f / 4.0f / M_PI, 1.0f / 3.0f);
+}
+
+float cube_volume(float radius) {
+	return 8.0f * (radius * radius * radius);
+}
+
+float cube_radius(float volume) {
+	return std::pow(volume / 8.0f, 1.0f / 3.0f);
+}
+
+std::random_device random_dev;
+std::uniform_int_distribution<int> dist(0, RAND_MAX);
+
+float randomFloat(float min, float max) {
+	return min + (max - min) * dist(random_dev) / static_cast<float>(RAND_MAX);
+}
+
+float mod(float x, float y) {
+	return x - std::floor(x / y) * y;
+}
+
+void distributeParticlesCube(Particle *particles, size_t count, const glm::vec3& center, float radius,
+							 float mass, const glm::vec3& velocity, bool random) {
+	const float side = cube_radius(static_cast<float>(count)) * 2.0f;
+	
+	float volume = 0.0f;
+	
+	for (size_t i = 0; i < count; i++) {
+		glm::vec3 offset;
+		
+		if (random) {
+			offset.x = randomFloat(-1.0f, +1.0f);
+			offset.y = randomFloat(-1.0f, +1.0f);
+			offset.z = randomFloat(-1.0f, +1.0f);
+		} else {
+			const float s = static_cast<float>(i) + 0.5f;
+			
+			offset.x = 2.0f * mod(s, side) / side - 1.0f;
+			offset.y = 2.0f * mod(s / side, side) / side - 1.0f;
+			offset.z = 2.0f * mod(s / side / side, side) / side - 1.0f;
+		}
+		
+		offset *= radius;
+		
+		float size = 0.0f;
+		
+		if (random) {
+			const float ax = std::abs(offset.x);
+			const float ay = std::abs(offset.y);
+			const float az = std::abs(offset.z);
+			
+			const float a = std::max(std::max(ax, ay), az);
+			
+			size = (radius - a);
+		} else {
+			size = 2.0f * radius / side;
+		}
+		
+		particles[i].position = center + offset;
+		particles[i].size = size;
+		particles[i].velocity = velocity;
+		
+		volume += cube_volume(size);
+	}
+	
+	for (size_t i = 0; i < count; i++) {
+		particles[i].mass = (mass * cube_volume(particles[i].size) / volume);
+		particles[i].deformation = glm::mat4(1.0f);
+		
+		particles[i].pad = glm::vec3(0.0f);
+		particles[i].weight_sum = 1.0f;
+		
+		particles[i].mls = glm::mat4(0.0f);
+	}
+}
+
+void distributeParticlesSphere(Particle *particles, size_t count, const glm::vec3& center, float radius,
+							   float mass, const glm::vec3& velocity, bool random) {
+	const float side = sphere_radius(static_cast<float>(count)) * 2.0f;
+	
+	float volume = 0.0f;
+	
+	for (size_t i = 0; i < count; i++) {
+		glm::vec3 offset;
+		
+		if (random) {
+			offset.x = randomFloat(-1.0f, +1.0f);
+			offset.y = randomFloat(-1.0f, +1.0f);
+			offset.z = randomFloat(-1.0f, +1.0f);
+			
+			if (glm::length(offset) > 0.0f)
+				offset = glm::normalize(offset);
+			
+			offset *= randomFloat(0.0f, 1.0f);
+		} else {
+			const float range = 0.5f * side;
+			const float s = static_cast<float>(i) + 0.5f;
+			
+			float a = mod(s, range) / range;
+			float b = mod(s / range, M_PI * range);
+			float c = mod(s / range / M_PI / range, M_PI * range * 2.0f);
+			
+			offset.x = a * std::sin(c) * std::sin(b);
+			offset.y = a * std::cos(b);
+			offset.z = a * std::cos(c) * std::sin(b);
+		}
+		
+		offset *= radius;
+		
+		float size = 0.0f;
+		
+		if (random) {
+			size = (radius - glm::length(offset));
+		} else {
+			size = 2.0f * radius / side;
+		}
+		
+		particles[i].position = center + offset;
+		particles[i].size = size;
+		particles[i].velocity = velocity;
+		
+		volume += sphere_volume(size);
+	}
+	
+	// Keep the same densitiy as planned!
+	mass *= (volume / sphere_volume(radius));
+	
+	for (size_t i = 0; i < count; i++) {
+		particles[i].mass = (mass * sphere_volume(particles[i].size) / volume);
+		particles[i].deformation = glm::mat4(1.0f);
+		
+		particles[i].pad = glm::vec3(0.0f);
+		particles[i].weight_sum = 1.0f;
+		
+		particles[i].mls = glm::mat4(0.0f);
+	}
+}
+
+vkcv::ComputePipelineHandle createComputePipeline(vkcv::Core& core, vkcv::shader::GLSLCompiler& compiler,
+												  const std::string& path,
+												  std::vector<vkcv::DescriptorSetHandle>& descriptorSets) {
+	vkcv::ShaderProgram shaderProgram;
+	
+	compiler.compile(
+			vkcv::ShaderStage::COMPUTE,
+			path,
+			[&shaderProgram](vkcv::ShaderStage stage, const std::filesystem::path& path) {
+				shaderProgram.addShader(stage, path);
+			}
+	);
+	
+	const auto& descriptors = shaderProgram.getReflectedDescriptors();
+	
+	size_t count = 0;
+	
+	for (const auto& descriptor : descriptors) {
+		if (descriptor.first >= count) {
+			count = (descriptor.first + 1);
+		}
+	}
+	
+	std::vector<vkcv::DescriptorSetLayoutHandle> descriptorSetLayouts;
+	
+	descriptorSetLayouts.resize(count);
+	descriptorSets.resize(count);
+	
+	for (const auto& descriptor : descriptors) {
+		descriptorSetLayouts[descriptor.first] = core.createDescriptorSetLayout(descriptor.second);
+		descriptorSets[descriptor.first] = core.createDescriptorSet(descriptorSetLayouts[descriptor.first]);
+	}
+	
+	vkcv::ComputePipelineConfig config {
+			shaderProgram,
+			descriptorSetLayouts
+	};
+	
+	return core.createComputePipeline(config);
+}
+
+vkcv::BufferHandle resetParticles(vkcv::Core& core, size_t count, const glm::vec3& velocity,
+					float density, float size, int form, int mode) {
+	vkcv::Buffer<Particle> particles = core.createBuffer<Particle>(
+			vkcv::BufferType::STORAGE,
+			count
+	);
+	
+	std::vector<Particle> particles_vec (particles.getCount());
+	
+	switch (form) {
+		case SIM_FORM_SPHERE:
+			distributeParticlesSphere(
+					particles_vec.data(),
+					particles_vec.size(),
+					glm::vec3(0.5f),
+					size,
+					density * sphere_volume(size),
+					velocity,
+					(mode == 0)
+			);
+			break;
+		case SIM_FORM_CUBE:
+			distributeParticlesCube(
+					particles_vec.data(),
+					particles_vec.size(),
+					glm::vec3(0.5f),
+					size,
+					density * sphere_volume(size),
+					velocity,
+					(mode == 0)
+			);
+			break;
+		default:
+			break;
+	}
+	
+	particles.fill(particles_vec);
+	return particles.getHandle();
+}
+
+int main(int argc, const char **argv) {
+	const char* applicationName = "Wobble Bobble";
+	
+	uint32_t windowWidth = 800;
+	uint32_t windowHeight = 600;
+	
+	vkcv::Features features;
+	features.requireExtension(VK_KHR_SWAPCHAIN_EXTENSION_NAME);
+	
+	vkcv::Core core = vkcv::Core::create(
+			applicationName,
+			VK_MAKE_VERSION(0, 0, 1),
+			{vk::QueueFlagBits::eTransfer, vk::QueueFlagBits::eGraphics, vk::QueueFlagBits::eCompute},
+			features
+	);
+	vkcv::WindowHandle windowHandle = core.createWindow(applicationName, windowWidth, windowHeight, true);
+	vkcv::Window& window = core.getWindow(windowHandle);
+	vkcv::camera::CameraManager cameraManager(window);
+	
+	vkcv::gui::GUI gui (core, windowHandle);
+	
+	uint32_t trackballIdx = cameraManager.addCamera(vkcv::camera::ControllerType::TRACKBALL);
+	cameraManager.getCamera(trackballIdx).setCenter(glm::vec3(0.5f, 0.5f, 0.5f));   // set camera to look at the center of the particle volume
+	cameraManager.addCamera(vkcv::camera::ControllerType::PILOT);
+	
+	auto swapchainExtent = core.getSwapchain(windowHandle).getExtent();
+	
+	vkcv::ImageHandle depthBuffer = core.createImage(
+			vk::Format::eD32Sfloat,
+			swapchainExtent.width,
+			swapchainExtent.height
+	).getHandle();
+	
+	vkcv::Image grid = core.createImage(
+			vk::Format::eR16G16B16A16Sfloat,
+			32,
+			32,
+			32,
+			false,
+			true
+	);
+	
+	vkcv::SamplerHandle gridSampler = core.createSampler(
+			vkcv::SamplerFilterType::LINEAR,
+			vkcv::SamplerFilterType::LINEAR,
+			vkcv::SamplerMipmapMode::NEAREST,
+			vkcv::SamplerAddressMode::CLAMP_TO_BORDER,
+			0.0f,
+			vkcv::SamplerBorderColor::FLOAT_ZERO_TRANSPARENT
+	);
+	
+	vkcv::Buffer<Simulation> simulation = core.createBuffer<Simulation>(
+			vkcv::BufferType::UNIFORM, 1, vkcv::BufferMemoryType::HOST_VISIBLE
+	);
+	
+	Simulation* sim = simulation.map();
+	
+	glm::vec3 initialVelocity (0.0f, 0.1f, 0.0f);
+	
+	sim->density = 2500.0f;
+	sim->size = 0.1f;
+	sim->lame1 = 10.0f;
+	sim->lame2 = 20.0f;
+	sim->form = SIM_FORM_SPHERE;
+	sim->type = SIM_TYPE_HYPERELASTIC;
+	sim->K = 2.2f;
+	sim->E = 35.0f;
+	sim->gamma = 1.330f;
+	sim->mode = SIM_MODE_RANDOM;
+	sim->gravity = 9.81f;
+	sim->count = 1024;
+	
+	vkcv::BufferHandle particlesHandle = resetParticles(
+			core,
+			sim->count,
+			initialVelocity,
+			sim->density,
+			sim->size,
+			sim->form,
+			sim->mode
+	);
+	
+	vkcv::shader::GLSLCompiler compiler;
+	
+	std::vector<vkcv::DescriptorSetHandle> initParticleWeightsSets;
+	vkcv::ComputePipelineHandle initParticleWeightsPipeline = createComputePipeline(
+			core, compiler,
+			"shaders/init_particle_weights.comp",
+			initParticleWeightsSets
+	);
+	
+	{
+		vkcv::DescriptorWrites writes;
+		writes.storageBufferWrites.push_back(vkcv::BufferDescriptorWrite(0, particlesHandle));
+		core.writeDescriptorSet(initParticleWeightsSets[0], writes);
+	}
+	
+	{
+		vkcv::DescriptorWrites writes;
+		writes.sampledImageWrites.push_back(vkcv::SampledImageDescriptorWrite(0, grid.getHandle()));
+		writes.samplerWrites.push_back(vkcv::SamplerDescriptorWrite(1, gridSampler));
+		core.writeDescriptorSet(initParticleWeightsSets[1], writes);
+	}
+	
+	std::vector<vkcv::DescriptorSetHandle> transformParticlesToGridSets;
+	vkcv::ComputePipelineHandle transformParticlesToGridPipeline = createComputePipeline(
+			core, compiler,
+			"shaders/transform_particles_to_grid.comp",
+			transformParticlesToGridSets
+	);
+	
+	{
+		vkcv::DescriptorWrites writes;
+		writes.storageBufferWrites.push_back(vkcv::BufferDescriptorWrite(0, particlesHandle));
+		core.writeDescriptorSet(transformParticlesToGridSets[0], writes);
+	}
+	
+	{
+		vkcv::DescriptorWrites writes;
+		writes.uniformBufferWrites.push_back(vkcv::BufferDescriptorWrite(0, simulation.getHandle()));
+		core.writeDescriptorSet(transformParticlesToGridSets[1], writes);
+	}
+	
+	{
+		vkcv::DescriptorWrites writes;
+		writes.storageImageWrites.push_back(vkcv::StorageImageDescriptorWrite(0, grid.getHandle()));
+		core.writeDescriptorSet(transformParticlesToGridSets[2], writes);
+	}
+	
+	std::vector<vkcv::DescriptorSetHandle> updateParticleVelocitiesSets;
+	vkcv::ComputePipelineHandle updateParticleVelocitiesPipeline = createComputePipeline(
+			core, compiler,
+			"shaders/update_particle_velocities.comp",
+			updateParticleVelocitiesSets
+	);
+	
+	{
+		vkcv::DescriptorWrites writes;
+		writes.storageBufferWrites.push_back(vkcv::BufferDescriptorWrite(0, particlesHandle));
+		core.writeDescriptorSet(updateParticleVelocitiesSets[0], writes);
+	}
+	
+	{
+		vkcv::DescriptorWrites writes;
+		writes.uniformBufferWrites.push_back(vkcv::BufferDescriptorWrite(0, simulation.getHandle()));
+		core.writeDescriptorSet(updateParticleVelocitiesSets[1], writes);
+	}
+	
+	{
+		vkcv::DescriptorWrites writes;
+		writes.sampledImageWrites.push_back(vkcv::SampledImageDescriptorWrite(0, grid.getHandle()));
+		writes.samplerWrites.push_back(vkcv::SamplerDescriptorWrite(1, gridSampler));
+		core.writeDescriptorSet(updateParticleVelocitiesSets[2], writes);
+	}
+	
+	vkcv::ShaderProgram gfxProgramGrid;
+	
+	compiler.compileProgram(gfxProgramGrid, {
+			{ vkcv::ShaderStage::VERTEX, "shaders/grid.vert" },
+			{ vkcv::ShaderStage::FRAGMENT, "shaders/grid.frag" }
+	}, nullptr);
+	
+	vkcv::ShaderProgram gfxProgramParticles;
+	
+	compiler.compileProgram(gfxProgramParticles, {
+		{ vkcv::ShaderStage::VERTEX, "shaders/particle.vert" },
+		{ vkcv::ShaderStage::FRAGMENT, "shaders/particle.frag" }
+	}, nullptr);
+	
+	vkcv::ShaderProgram gfxProgramLines;
+	
+	compiler.compileProgram(gfxProgramLines, {
+			{ vkcv::ShaderStage::VERTEX, "shaders/lines.vert" },
+			{ vkcv::ShaderStage::FRAGMENT, "shaders/lines.frag" }
+	}, nullptr);
+	
+	vkcv::PassConfig passConfigGrid ({
+		vkcv::AttachmentDescription(
+				vkcv::AttachmentOperation::STORE,
+				vkcv::AttachmentOperation::CLEAR,
+				core.getSwapchain(windowHandle).getFormat()
+		),
+		vkcv::AttachmentDescription(
+				vkcv::AttachmentOperation::STORE,
+				vkcv::AttachmentOperation::CLEAR,
+				vk::Format::eD32Sfloat
+		)
+	});
+	
+	vkcv::PassConfig passConfigParticles ({
+		vkcv::AttachmentDescription(
+				vkcv::AttachmentOperation::STORE,
+				vkcv::AttachmentOperation::CLEAR,
+				core.getSwapchain(windowHandle).getFormat()
+		),
+		vkcv::AttachmentDescription(
+				vkcv::AttachmentOperation::STORE,
+				vkcv::AttachmentOperation::CLEAR,
+				vk::Format::eD32Sfloat
+		)
+	});
+	
+	vkcv::PassConfig passConfigLines ({
+		vkcv::AttachmentDescription(
+				vkcv::AttachmentOperation::STORE,
+				vkcv::AttachmentOperation::LOAD,
+				core.getSwapchain(windowHandle).getFormat()
+		),
+		vkcv::AttachmentDescription(
+				vkcv::AttachmentOperation::STORE,
+				vkcv::AttachmentOperation::LOAD,
+				vk::Format::eD32Sfloat
+		)
+	});
+	
+	vkcv::DescriptorSetLayoutHandle gfxSetLayoutGrid = core.createDescriptorSetLayout(
+			gfxProgramGrid.getReflectedDescriptors().at(0)
+	);
+	
+	vkcv::DescriptorSetHandle gfxSetGrid = core.createDescriptorSet(gfxSetLayoutGrid);
+	
+	{
+		vkcv::DescriptorWrites writes;
+		writes.sampledImageWrites.push_back(vkcv::SampledImageDescriptorWrite(0, grid.getHandle()));
+		writes.samplerWrites.push_back(vkcv::SamplerDescriptorWrite(1, gridSampler));
+		writes.uniformBufferWrites.push_back(vkcv::BufferDescriptorWrite(2, simulation.getHandle()));
+		core.writeDescriptorSet(gfxSetGrid, writes);
+	}
+	
+	vkcv::DescriptorSetLayoutHandle gfxSetLayoutParticles = core.createDescriptorSetLayout(
+			gfxProgramParticles.getReflectedDescriptors().at(0)
+	);
+	
+	vkcv::DescriptorSetHandle gfxSetParticles = core.createDescriptorSet(gfxSetLayoutParticles);
+	
+	{
+		vkcv::DescriptorWrites writes;
+		writes.storageBufferWrites.push_back(vkcv::BufferDescriptorWrite(0, particlesHandle));
+		core.writeDescriptorSet(gfxSetParticles, writes);
+	}
+	
+	vkcv::PassHandle gfxPassGrid = core.createPass(passConfigGrid);
+	vkcv::PassHandle gfxPassParticles = core.createPass(passConfigParticles);
+	vkcv::PassHandle gfxPassLines = core.createPass(passConfigLines);
+	
+	vkcv::VertexLayout vertexLayoutGrid ({
+		vkcv::VertexBinding(0, gfxProgramGrid.getVertexAttachments())
+	});
+	
+	vkcv::GraphicsPipelineConfig gfxPipelineConfigGrid;
+	gfxPipelineConfigGrid.m_ShaderProgram = gfxProgramGrid;
+	gfxPipelineConfigGrid.m_Width = windowWidth;
+	gfxPipelineConfigGrid.m_Height = windowHeight;
+	gfxPipelineConfigGrid.m_PassHandle = gfxPassGrid;
+	gfxPipelineConfigGrid.m_VertexLayout = vertexLayoutGrid;
+	gfxPipelineConfigGrid.m_DescriptorLayouts = { gfxSetLayoutGrid };
+	gfxPipelineConfigGrid.m_UseDynamicViewport = true;
+	
+	vkcv::VertexLayout vertexLayoutParticles ({
+		vkcv::VertexBinding(0, gfxProgramParticles.getVertexAttachments())
+	});
+	
+	vkcv::GraphicsPipelineConfig gfxPipelineConfigParticles;
+	gfxPipelineConfigParticles.m_ShaderProgram = gfxProgramParticles;
+	gfxPipelineConfigParticles.m_Width = windowWidth;
+	gfxPipelineConfigParticles.m_Height = windowHeight;
+	gfxPipelineConfigParticles.m_PassHandle = gfxPassParticles;
+	gfxPipelineConfigParticles.m_VertexLayout = vertexLayoutParticles;
+	gfxPipelineConfigParticles.m_DescriptorLayouts = { gfxSetLayoutParticles };
+	gfxPipelineConfigParticles.m_UseDynamicViewport = true;
+	
+	vkcv::VertexLayout vertexLayoutLines ({
+		vkcv::VertexBinding(0, gfxProgramLines.getVertexAttachments())
+	});
+	
+	vkcv::GraphicsPipelineConfig gfxPipelineConfigLines;
+	gfxPipelineConfigLines.m_ShaderProgram = gfxProgramLines;
+	gfxPipelineConfigLines.m_Width = windowWidth;
+	gfxPipelineConfigLines.m_Height = windowHeight;
+	gfxPipelineConfigLines.m_PassHandle = gfxPassLines;
+	gfxPipelineConfigLines.m_VertexLayout = vertexLayoutLines;
+	gfxPipelineConfigLines.m_DescriptorLayouts = {};
+	gfxPipelineConfigLines.m_UseDynamicViewport = true;
+	gfxPipelineConfigLines.m_PrimitiveTopology = vkcv::PrimitiveTopology::LineList;
+	
+	vkcv::GraphicsPipelineHandle gfxPipelineGrid = core.createGraphicsPipeline(gfxPipelineConfigGrid);
+	vkcv::GraphicsPipelineHandle gfxPipelineParticles = core.createGraphicsPipeline(gfxPipelineConfigParticles);
+	vkcv::GraphicsPipelineHandle gfxPipelineLines = core.createGraphicsPipeline(gfxPipelineConfigLines);
+	
+	vkcv::Buffer<glm::vec2> trianglePositions = core.createBuffer<glm::vec2>(vkcv::BufferType::VERTEX, 3);
+	trianglePositions.fill({
+		glm::vec2(-1.0f, -1.0f),
+		glm::vec2(+0.0f, +1.5f),
+		glm::vec2(+1.0f, -1.0f)
+	});
+	
+	vkcv::Buffer<uint16_t> triangleIndices = core.createBuffer<uint16_t>(vkcv::BufferType::INDEX, 3);
+	triangleIndices.fill({
+		0, 1, 2
+	});
+	
+	vkcv::Mesh triangleMesh (
+			{ vkcv::VertexBufferBinding(0, trianglePositions.getVulkanHandle()) },
+			triangleIndices.getVulkanHandle(),
+			triangleIndices.getCount()
+	);
+	
+	vkcv::Buffer<glm::vec3> linesPositions = core.createBuffer<glm::vec3>(vkcv::BufferType::VERTEX, 8);
+	linesPositions.fill({
+		glm::vec3(0.0f, 0.0f, 0.0f),
+		glm::vec3(1.0f, 0.0f, 0.0f),
+		glm::vec3(0.0f, 1.0f, 0.0f),
+		glm::vec3(1.0f, 1.0f, 0.0f),
+		glm::vec3(0.0f, 0.0f, 1.0f),
+		glm::vec3(1.0f, 0.0f, +1.0f),
+		glm::vec3(0.0f, 1.0f, 1.0f),
+		glm::vec3(1.0f, 1.0f, 1.0f)
+	});
+	
+	vkcv::Buffer<uint16_t> linesIndices = core.createBuffer<uint16_t>(vkcv::BufferType::INDEX, 24);
+	linesIndices.fill({
+		0, 1,
+		1, 3,
+		3, 2,
+		2, 0,
+		
+		4, 5,
+		5, 7,
+		7, 6,
+		6, 4,
+		
+		0, 4,
+		1, 5,
+		2, 6,
+		3, 7
+	});
+	
+	vkcv::Mesh linesMesh (
+			{ vkcv::VertexBufferBinding(0, linesPositions.getVulkanHandle()) },
+			linesIndices.getVulkanHandle(),
+			linesIndices.getCount()
+	);
+	
+	std::vector<vkcv::DrawcallInfo> drawcallsGrid;
+	
+	drawcallsGrid.push_back(vkcv::DrawcallInfo(
+			triangleMesh,
+			{ vkcv::DescriptorSetUsage(0, gfxSetGrid) },
+			grid.getWidth() * grid.getHeight() * grid.getDepth()
+	));
+	
+	std::vector<vkcv::DrawcallInfo> drawcallsParticles;
+	
+	drawcallsParticles.push_back(vkcv::DrawcallInfo(
+			triangleMesh,
+			{ vkcv::DescriptorSetUsage(0, gfxSetParticles) },
+			sim->count
+	));
+	
+	std::vector<vkcv::DrawcallInfo> drawcallsLines;
+	
+	drawcallsLines.push_back(vkcv::DrawcallInfo(
+			linesMesh,
+			{},
+			1
+	));
+	
+	bool renderGrid = true;
+	
+	float speed_factor = 1.0f;
+	
+	auto start = std::chrono::system_clock::now();
+	auto current = start;
+	
+	while (vkcv::Window::hasOpenWindow()) {
+		vkcv::Window::pollEvents();
+		
+		if (window.getHeight() == 0 || window.getWidth() == 0)
+			continue;
+		
+		uint32_t swapchainWidth, swapchainHeight;
+		if (!core.beginFrame(swapchainWidth, swapchainHeight, windowHandle)) {
+			continue;
+		}
+		
+		if ((swapchainWidth != swapchainExtent.width) || ((swapchainHeight != swapchainExtent.height))) {
+			depthBuffer = core.createImage(
+					vk::Format::eD32Sfloat,
+					swapchainWidth,
+					swapchainHeight
+			).getHandle();
+			
+			swapchainExtent.width = swapchainWidth;
+			swapchainExtent.height = swapchainHeight;
+		}
+		
+		auto next = std::chrono::system_clock::now();
+		
+		auto time = std::chrono::duration_cast<std::chrono::microseconds>(next - start);
+		auto deltatime = std::chrono::duration_cast<std::chrono::microseconds>(next - current);
+		
+		current = next;
+		
+		Physics physics;
+		physics.t = static_cast<float>(0.000001 * static_cast<double>(time.count()));
+		physics.dt = static_cast<float>(0.000001 * static_cast<double>(deltatime.count()));
+		physics.speedfactor = speed_factor;
+		
+		vkcv::PushConstants physicsPushConstants(sizeof(physics));
+		physicsPushConstants.appendDrawcall(physics);
+		
+		cameraManager.update(physics.dt);
+		
+		glm::mat4 mvp = cameraManager.getActiveCamera().getMVP();
+		vkcv::PushConstants cameraPushConstants(sizeof(glm::mat4));
+		cameraPushConstants.appendDrawcall(mvp);
+		
+		auto cmdStream = core.createCommandStream(vkcv::QueueType::Graphics);
+		
+		const uint32_t dispatchSizeGrid[3] = {grid.getWidth() / 4, grid.getHeight() / 4, grid.getDepth() / 4};
+		const uint32_t dispatchSizeParticles[3] = {static_cast<uint32_t>(sim->count + 63) / 64, 1, 1};
+		
+		for (int step = 0; step < 1; step++) {
+			core.recordBeginDebugLabel(cmdStream, "INIT PARTICLE WEIGHTS", {0.78f, 0.89f, 0.94f, 1.0f});
+			core.recordBufferMemoryBarrier(cmdStream, particlesHandle);
+			core.prepareImageForSampling(cmdStream, grid.getHandle());
+			
+			core.recordComputeDispatchToCmdStream(
+					cmdStream,
+					initParticleWeightsPipeline,
+					dispatchSizeParticles,
+					{
+						vkcv::DescriptorSetUsage(
+								0, initParticleWeightsSets[0]
+						),
+						vkcv::DescriptorSetUsage(
+								1, initParticleWeightsSets[1]
+						)
+					},
+					vkcv::PushConstants(0)
+			);
+			
+			core.recordBufferMemoryBarrier(cmdStream, particlesHandle);
+			core.recordEndDebugLabel(cmdStream);
+			
+			core.recordBeginDebugLabel(cmdStream, "TRANSFORM PARTICLES TO GRID", {0.47f, 0.77f, 0.85f, 1.0f});
+			core.recordBufferMemoryBarrier(cmdStream, particlesHandle);
+			core.prepareImageForStorage(cmdStream, grid.getHandle());
+			
+			core.recordComputeDispatchToCmdStream(
+					cmdStream,
+					transformParticlesToGridPipeline,
+					dispatchSizeGrid,
+					{
+						vkcv::DescriptorSetUsage(
+								0, transformParticlesToGridSets[0]
+						),
+						vkcv::DescriptorSetUsage(
+								1, transformParticlesToGridSets[1]
+						),
+						vkcv::DescriptorSetUsage(
+								2, transformParticlesToGridSets[2]
+						)
+					},
+					physicsPushConstants
+			);
+			
+			core.recordImageMemoryBarrier(cmdStream, grid.getHandle());
+			core.recordEndDebugLabel(cmdStream);
+			
+			core.recordBeginDebugLabel(cmdStream, "UPDATE PARTICLE VELOCITIES", {0.78f, 0.89f, 0.94f, 1.0f});
+			core.recordBufferMemoryBarrier(cmdStream, particlesHandle);
+			core.recordBufferMemoryBarrier(cmdStream, simulation.getHandle());
+			core.prepareImageForSampling(cmdStream, grid.getHandle());
+			
+			core.recordComputeDispatchToCmdStream(
+					cmdStream,
+					updateParticleVelocitiesPipeline,
+					dispatchSizeParticles,
+					{
+						vkcv::DescriptorSetUsage(
+								0, updateParticleVelocitiesSets[0]
+						),
+						vkcv::DescriptorSetUsage(
+								1, updateParticleVelocitiesSets[1]
+						),
+						vkcv::DescriptorSetUsage(
+								2, updateParticleVelocitiesSets[2]
+						)
+					},
+					physicsPushConstants
+			);
+			
+			core.recordBufferMemoryBarrier(cmdStream, particlesHandle);
+			core.recordEndDebugLabel(cmdStream);
+		}
+		
+		std::vector<vkcv::ImageHandle> renderTargets {
+				vkcv::ImageHandle::createSwapchainImageHandle(),
+				depthBuffer
+		};
+		
+		if (renderGrid) {
+			core.recordBeginDebugLabel(cmdStream, "RENDER GRID", { 0.13f, 0.20f, 0.22f, 1.0f });
+			core.recordBufferMemoryBarrier(cmdStream, simulation.getHandle());
+			core.prepareImageForSampling(cmdStream, grid.getHandle());
+			
+			core.recordDrawcallsToCmdStream(
+					cmdStream,
+					gfxPassGrid,
+					gfxPipelineGrid,
+					cameraPushConstants,
+					drawcallsGrid,
+					renderTargets,
+					windowHandle
+			);
+			
+			core.recordEndDebugLabel(cmdStream);
+		} else {
+			core.recordBeginDebugLabel(cmdStream, "RENDER PARTICLES", { 0.13f, 0.20f, 0.22f, 1.0f });
+			core.recordBufferMemoryBarrier(cmdStream, particlesHandle);
+			
+			core.recordDrawcallsToCmdStream(
+					cmdStream,
+					gfxPassParticles,
+					gfxPipelineParticles,
+					cameraPushConstants,
+					drawcallsParticles,
+					renderTargets,
+					windowHandle
+			);
+			
+			core.recordEndDebugLabel(cmdStream);
+		}
+		
+		core.recordBeginDebugLabel(cmdStream, "RENDER LINES", { 0.13f, 0.20f, 0.22f, 1.0f });
+		
+		core.recordDrawcallsToCmdStream(
+				cmdStream,
+				gfxPassLines,
+				gfxPipelineLines,
+				cameraPushConstants,
+				drawcallsLines,
+				renderTargets,
+				windowHandle
+		);
+		
+		core.recordEndDebugLabel(cmdStream);
+		
+		core.prepareSwapchainImageForPresent(cmdStream);
+		core.submitCommandStream(cmdStream);
+		
+		gui.beginGUI();
+		ImGui::Begin("Settings");
+		
+		ImGui::BeginGroup();
+		ImGui::Combo("Mode", &(sim->mode), "Random\0Ordered", 2);
+		ImGui::Combo("Form", &(sim->form), "Sphere\0Cube", 2);
+		ImGui::Combo("Type", &(sim->type), "Hyperelastic\0Fluid", 2);
+		ImGui::EndGroup();
+		
+		ImGui::Spacing();
+		
+		ImGui::SliderInt("Particle Count", &(sim->count), 1, 100000);
+		ImGui::SliderFloat("Density", &(sim->density), std::numeric_limits<float>::epsilon(), 5000.0f);
+		ImGui::SameLine(0.0f, 10.0f);
+		if (ImGui::SmallButton("Reset##density")) {
+			sim->density = 2500.0f;
+		}
+		
+		ImGui::SliderFloat("Radius", &(sim->size), 0.0f, 0.5f);
+		ImGui::SameLine(0.0f, 10.0f);
+		if (ImGui::SmallButton("Reset##radius")) {
+			sim->size = 0.1f;
+		}
+		
+		ImGui::Spacing();
+		
+		ImGui::BeginGroup();
+		ImGui::SliderFloat("Bulk Modulus", &(sim->K), 0.0f, 1000.0f);
+		ImGui::SliderFloat("Young's Modulus", &(sim->E), 0.0f, 1000.0f);
+		ImGui::SliderFloat("Heat Capacity Ratio", &(sim->gamma), 1.0f, 2.0f);
+		ImGui::SliderFloat("Lame1", &(sim->lame1), 0.0f, 1000.0f);
+		ImGui::SliderFloat("Lame2", &(sim->lame2), 0.0f, 1000.0f);
+		ImGui::EndGroup();
+
+		ImGui::Spacing();
+
+		ImGui::SliderFloat("Simulation Speed", &speed_factor, 0.0f, 2.0f);
+		
+		ImGui::Spacing();
+		ImGui::Checkbox("Render Grid", &renderGrid);
+		
+		ImGui::DragFloat3("Initial Velocity", reinterpret_cast<float*>(&initialVelocity), 0.001f);
+		ImGui::SameLine(0.0f, 10.0f);
+		if (ImGui::Button("Reset##particle_velocity")) {
+			particlesHandle = resetParticles(
+					core,
+					sim->count,
+					initialVelocity,
+					sim->density,
+					sim->size,
+					sim->form,
+					sim->mode
+			);
+			
+			vkcv::DescriptorWrites writes;
+			writes.storageBufferWrites.push_back(vkcv::BufferDescriptorWrite(0, particlesHandle));
+			
+			core.writeDescriptorSet(initParticleWeightsSets[0], writes);
+			core.writeDescriptorSet(transformParticlesToGridSets[0], writes);
+			core.writeDescriptorSet(updateParticleVelocitiesSets[0], writes);
+			
+			core.writeDescriptorSet(gfxSetParticles, writes);
+		}
+		
+		ImGui::SliderFloat("Gravity", &(sim->gravity), 0.0f, 10.0f);
+		
+		ImGui::End();
+		gui.endGUI();
+		
+		core.endFrame(windowHandle);
+	}
+	
+	simulation.unmap();
+	return 0;
+}
diff --git a/src/vkcv/Core.cpp b/src/vkcv/Core.cpp
index 12d2090de47e5d002734c9a44773affdb4f99b3e..ba69cab5fcd2a1ce78e5a3491e24ab7f273e0474 100644
--- a/src/vkcv/Core.cpp
+++ b/src/vkcv/Core.cpp
@@ -268,6 +268,56 @@ namespace vkcv
 		cmdBuffer.setViewport(0, 1, &dynamicViewport);
 		cmdBuffer.setScissor(0, 1, &dynamicScissor);
 	}
+	
+	vk::IndexType getIndexType(IndexBitCount indexByteCount){
+		switch (indexByteCount) {
+			case IndexBitCount::Bit16: return vk::IndexType::eUint16;
+			case IndexBitCount::Bit32: return vk::IndexType::eUint32;
+			default:
+			vkcv_log(LogLevel::ERROR, "unknown Enum");
+				return vk::IndexType::eUint16;
+		}
+	}
+	
+	void recordDrawcall(
+			const Core				&core,
+			const DrawcallInfo      &drawcall,
+			vk::CommandBuffer       cmdBuffer,
+			vk::PipelineLayout      pipelineLayout,
+			const PushConstants     &pushConstants,
+			const size_t            drawcallIndex) {
+		
+		for (uint32_t i = 0; i < drawcall.mesh.vertexBufferBindings.size(); i++) {
+			const auto& vertexBinding = drawcall.mesh.vertexBufferBindings[i];
+			cmdBuffer.bindVertexBuffers(i, vertexBinding.buffer, vertexBinding.offset);
+		}
+		
+		for (const auto& descriptorUsage : drawcall.descriptorSets) {
+			cmdBuffer.bindDescriptorSets(
+					vk::PipelineBindPoint::eGraphics,
+					pipelineLayout,
+					descriptorUsage.setLocation,
+					core.getDescriptorSet(descriptorUsage.descriptorSet).vulkanHandle,
+					nullptr);
+		}
+		
+		if (pushConstants.getSizePerDrawcall() > 0) {
+			cmdBuffer.pushConstants(
+					pipelineLayout,
+					vk::ShaderStageFlagBits::eAll,
+					0,
+					pushConstants.getSizePerDrawcall(),
+					pushConstants.getDrawcallData(drawcallIndex));
+		}
+		
+		if (drawcall.mesh.indexBuffer) {
+			cmdBuffer.bindIndexBuffer(drawcall.mesh.indexBuffer, 0, getIndexType(drawcall.mesh.indexBitCount));
+			cmdBuffer.drawIndexed(drawcall.mesh.indexCount, drawcall.instanceCount, 0, 0, {});
+		}
+		else {
+			cmdBuffer.draw(drawcall.mesh.indexCount, drawcall.instanceCount, 0, 0, {});
+		}
+	}
 
 	void Core::recordDrawcallsToCmdStream(
 		const CommandStreamHandle&      cmdStreamHandle,
@@ -323,7 +373,7 @@ namespace vkcv
 			}
 
 			for (size_t i = 0; i < drawcalls.size(); i++) {
-				recordDrawcall(drawcalls[i], cmdBuffer, pipelineLayout, pushConstantData, i);
+				recordDrawcall(*this, drawcalls[i], cmdBuffer, pipelineLayout, pushConstantData, i);
 			}
 
 			cmdBuffer.endRenderPass();
@@ -490,6 +540,7 @@ namespace vkcv
 			for (size_t i = 0; i < drawcalls.size(); i++) {
                 const uint32_t pushConstantOffset = i * pushConstantData.getSizePerDrawcall();
                 recordMeshShaderDrawcall(
+					*this,
                     cmdBuffer,
                     pipelineLayout,
                     pushConstantData,
@@ -529,7 +580,7 @@ namespace vkcv
 					vk::PipelineBindPoint::eRayTracingKHR,
 					rtxPipelineLayout,
 					usage.setLocation,
-					{ usage.vulkanHandle },
+					{ getDescriptorSet(usage.descriptorSet).vulkanHandle },
 					usage.dynamicOffsets
 				);
 			}
@@ -570,7 +621,7 @@ namespace vkcv
 					vk::PipelineBindPoint::eCompute,
 					pipelineLayout,
 					usage.setLocation,
-					{ usage.vulkanHandle },
+					{ getDescriptorSet(usage.descriptorSet).vulkanHandle },
 					usage.dynamicOffsets
 				);
 			}
@@ -649,7 +700,7 @@ namespace vkcv
 					vk::PipelineBindPoint::eCompute,
 					pipelineLayout,
 					usage.setLocation,
-					{ usage.vulkanHandle },
+					{ getDescriptorSet(usage.descriptorSet).vulkanHandle },
 					usage.dynamicOffsets
 				);
 			}
@@ -763,8 +814,8 @@ namespace vkcv
 
 	SamplerHandle Core::createSampler(SamplerFilterType magFilter, SamplerFilterType minFilter,
 									  SamplerMipmapMode mipmapMode, SamplerAddressMode addressMode,
-									  float mipLodBias) {
-		return m_SamplerManager->createSampler(magFilter, minFilter, mipmapMode, addressMode, mipLodBias);
+									  float mipLodBias, SamplerBorderColor borderColor) {
+		return m_SamplerManager->createSampler(magFilter, minFilter, mipmapMode, addressMode, mipLodBias, borderColor);
 	}
 
 	Image Core::createImage(
diff --git a/src/vkcv/DrawcallRecording.cpp b/src/vkcv/DrawcallRecording.cpp
index 638df388178f79f680c3b9f797d9d435e752fe10..e13b4d24148e9d76d8be4ad7fd3db0db87249c59 100644
--- a/src/vkcv/DrawcallRecording.cpp
+++ b/src/vkcv/DrawcallRecording.cpp
@@ -1,67 +1,9 @@
-#include <vkcv/DrawcallRecording.hpp>
-#include <vkcv/Logger.hpp>
 
-namespace vkcv {
-
-    vk::IndexType getIndexType(IndexBitCount indexByteCount){
-        switch (indexByteCount) {
-            case IndexBitCount::Bit16: return vk::IndexType::eUint16;
-            case IndexBitCount::Bit32: return vk::IndexType::eUint32;
-            default:
-                vkcv_log(LogLevel::ERROR, "unknown Enum");
-                return vk::IndexType::eUint16;
-        }
-    }
-
-    void recordDrawcall(
-        const DrawcallInfo      &drawcall,
-        vk::CommandBuffer       cmdBuffer,
-        vk::PipelineLayout      pipelineLayout,
-        const PushConstants     &pushConstants,
-        const size_t            drawcallIndex) {
-
-        for (uint32_t i = 0; i < drawcall.mesh.vertexBufferBindings.size(); i++) {
-            const auto& vertexBinding = drawcall.mesh.vertexBufferBindings[i];
-            cmdBuffer.bindVertexBuffers(i, vertexBinding.buffer, vertexBinding.offset);
-        }
+#include "vkcv/DrawcallRecording.hpp"
+#include "vkcv/Logger.hpp"
+#include "vkcv/Core.hpp"
 
-        for (const auto& descriptorUsage : drawcall.descriptorSets) {
-            cmdBuffer.bindDescriptorSets(
-                vk::PipelineBindPoint::eGraphics,
-                pipelineLayout,
-                descriptorUsage.setLocation,
-                descriptorUsage.vulkanHandle,
-                nullptr);
-        }
-
-        if (pushConstants.getSizePerDrawcall() > 0) {
-            cmdBuffer.pushConstants(
-                pipelineLayout,
-                vk::ShaderStageFlagBits::eAll,
-                0,
-				pushConstants.getSizePerDrawcall(),
-                pushConstants.getDrawcallData(drawcallIndex));
-        }
-
-        if (drawcall.mesh.indexBuffer) {
-            cmdBuffer.bindIndexBuffer(drawcall.mesh.indexBuffer, 0, getIndexType(drawcall.mesh.indexBitCount));
-            cmdBuffer.drawIndexed(drawcall.mesh.indexCount, drawcall.instanceCount, 0, 0, {});
-        }
-        else {
-            cmdBuffer.draw(drawcall.mesh.indexCount, drawcall.instanceCount, 0, 0, {});
-        }
-    }
-
-    void recordIndirectDrawcall(
-            const DrawcallInfo                              &drawcall,
-            vk::CommandBuffer                               cmdBuffer,
-            const Buffer <vk::DrawIndexedIndirectCommand>   &drawBuffer,
-            const uint32_t                                  drawCount,
-            vk::PipelineLayout                              pipelineLayout,
-            const PushConstants                             &pushConstants,
-            const size_t                                    drawcallIndex) {
-        return;
-    }
+namespace vkcv {
 
     struct MeshShaderFunctions
     {
@@ -78,6 +20,7 @@ namespace vkcv {
     }
 
     void recordMeshShaderDrawcall(
+		const Core&								core,
         vk::CommandBuffer                       cmdBuffer,
         vk::PipelineLayout                      pipelineLayout,
         const PushConstants&                    pushConstantData,
@@ -90,7 +33,7 @@ namespace vkcv {
                 vk::PipelineBindPoint::eGraphics,
                 pipelineLayout,
                 descriptorUsage.setLocation,
-                descriptorUsage.vulkanHandle,
+				core.getDescriptorSet(descriptorUsage.descriptorSet).vulkanHandle,
                 nullptr);
         }
 
diff --git a/src/vkcv/FeatureManager.cpp b/src/vkcv/FeatureManager.cpp
index c25745fcdd007df86316e0f72bc3d654377baa70..f89eb0717b82e40b10f98673e6d42fc07e67a8fd 100644
--- a/src/vkcv/FeatureManager.cpp
+++ b/src/vkcv/FeatureManager.cpp
@@ -293,6 +293,44 @@ m_physicalDevice.getFeatures2(&query)
 		return true;
 	}
 	
+	bool FeatureManager::checkSupport(const vk::PhysicalDeviceShaderAtomicFloatFeaturesEXT& features, bool required) const {
+		vkcv_check_init_features2(vk::PhysicalDeviceShaderAtomicFloatFeaturesEXT);
+		
+		vkcv_check_feature(shaderBufferFloat32Atomics);
+		vkcv_check_feature(shaderBufferFloat32AtomicAdd);
+		vkcv_check_feature(shaderBufferFloat64Atomics);
+		vkcv_check_feature(shaderBufferFloat64AtomicAdd);
+		vkcv_check_feature(shaderSharedFloat32Atomics);
+		vkcv_check_feature(shaderSharedFloat32AtomicAdd);
+		vkcv_check_feature(shaderSharedFloat64Atomics);
+		vkcv_check_feature(shaderSharedFloat64AtomicAdd);
+		vkcv_check_feature(shaderImageFloat32Atomics);
+		vkcv_check_feature(shaderImageFloat32AtomicAdd);
+		vkcv_check_feature(sparseImageFloat32Atomics);
+		vkcv_check_feature(sparseImageFloat32AtomicAdd);
+		
+		return true;
+	}
+	
+	bool FeatureManager::checkSupport(const vk::PhysicalDeviceShaderAtomicFloat2FeaturesEXT& features, bool required) const {
+		vkcv_check_init_features2(vk::PhysicalDeviceShaderAtomicFloat2FeaturesEXT);
+		
+		vkcv_check_feature(shaderBufferFloat16Atomics);
+		vkcv_check_feature(shaderBufferFloat16AtomicAdd);
+		vkcv_check_feature(shaderBufferFloat16AtomicMinMax);
+		vkcv_check_feature(shaderBufferFloat32AtomicMinMax);
+		vkcv_check_feature(shaderBufferFloat64AtomicMinMax);
+		vkcv_check_feature(shaderSharedFloat16Atomics);
+		vkcv_check_feature(shaderSharedFloat16AtomicAdd);
+		vkcv_check_feature(shaderSharedFloat16AtomicMinMax);
+		vkcv_check_feature(shaderSharedFloat32AtomicMinMax);
+		vkcv_check_feature(shaderSharedFloat64AtomicMinMax);
+		vkcv_check_feature(shaderImageFloat32AtomicMinMax);
+		vkcv_check_feature(sparseImageFloat32AtomicMinMax);
+		
+		return true;
+	}
+	
 	bool FeatureManager::checkSupport(const vk::PhysicalDeviceVulkan12Features &features, bool required) const {
 	    vkcv_check_init_features2(vk::PhysicalDeviceVulkan12Features);
 
diff --git a/src/vkcv/ImageManager.cpp b/src/vkcv/ImageManager.cpp
index bde020498e19e3f9bf0667c7182ca13d11f9044f..bfee504f599d941e625e6ef5c6045805c33a29a1 100644
--- a/src/vkcv/ImageManager.cpp
+++ b/src/vkcv/ImageManager.cpp
@@ -345,7 +345,7 @@ namespace vkcv {
 		recordImageBarrier(cmdBuffer, transitionBarrier);
 	}
 	
-	constexpr uint32_t getChannelsByFormat(vk::Format format) {
+	constexpr uint32_t getBytesPerPixel(vk::Format format) {
 		switch (format) {
 			case vk::Format::eR8Unorm:
 				return 1;
@@ -353,6 +353,10 @@ namespace vkcv {
 				return 4;
 			case vk::Format::eR8G8B8A8Unorm:
 				return 4;
+			case vk::Format::eR16G16B16A16Sfloat:
+				return 8;
+			case vk::Format::eR32G32B32A32Sfloat:
+				return 16;
 			default:
 				std::cerr << "Unknown image format" << std::endl;
 				return 4;
@@ -379,15 +383,15 @@ namespace vkcv {
 				handle,
 				vk::ImageLayout::eTransferDstOptimal);
 		
-		uint32_t channels = getChannelsByFormat(image.m_format);
 		const size_t image_size = (
-				image.m_width * image.m_height * image.m_depth * channels
+				image.m_width * image.m_height * image.m_depth *
+				getBytesPerPixel(image.m_format)
 		);
 		
 		const size_t max_size = std::min(size, image_size);
 		
 		BufferHandle bufferHandle = m_bufferManager.createBuffer(
-				BufferType::STAGING, max_size, BufferMemoryType::HOST_VISIBLE, false
+				BufferType::STAGING, max_size, BufferMemoryType::DEVICE_LOCAL, false
 		);
 		
 		m_bufferManager.fillBuffer(bufferHandle, data, max_size, 0);
diff --git a/src/vkcv/PassConfig.cpp b/src/vkcv/PassConfig.cpp
index 78bd5808b63fee7333243db4fca640047f76eae9..b203fab4f02afcc8f51b468ab4504480a8e60e4e 100644
--- a/src/vkcv/PassConfig.cpp
+++ b/src/vkcv/PassConfig.cpp
@@ -11,7 +11,7 @@ namespace vkcv
 	store_operation{store_op},
 	load_operation{load_op},
 	format(format)
-    {};
+    {}
 
     PassConfig::PassConfig(std::vector<AttachmentDescription> attachments, Multisampling msaa) noexcept :
     attachments{std::move(attachments) }, msaa(msaa)
diff --git a/src/vkcv/SamplerManager.cpp b/src/vkcv/SamplerManager.cpp
index 792e6f16b4a05af41a164a1eda9dd7423594857e..9a80635744e5a3dd0b6bd8db476cec841b1c317d 100644
--- a/src/vkcv/SamplerManager.cpp
+++ b/src/vkcv/SamplerManager.cpp
@@ -18,11 +18,13 @@ namespace vkcv {
 												SamplerFilterType minFilter,
 												SamplerMipmapMode mipmapMode,
 												SamplerAddressMode addressMode,
-												float mipLodBias) {
+												float mipLodBias,
+												SamplerBorderColor borderColor) {
 		vk::Filter vkMagFilter;
 		vk::Filter vkMinFilter;
 		vk::SamplerMipmapMode vkMipmapMode;
 		vk::SamplerAddressMode vkAddressMode;
+		vk::BorderColor vkBorderColor;
 		
 		switch (magFilter) {
 			case SamplerFilterType::NEAREST:
@@ -70,6 +72,32 @@ namespace vkcv {
 			case SamplerAddressMode::MIRROR_CLAMP_TO_EDGE:
 				vkAddressMode = vk::SamplerAddressMode::eMirrorClampToEdge;
 				break;
+			case SamplerAddressMode::CLAMP_TO_BORDER:
+				vkAddressMode = vk::SamplerAddressMode::eClampToBorder;
+				break;
+			default:
+				return SamplerHandle();
+		}
+		
+		switch (borderColor) {
+			case SamplerBorderColor::INT_ZERO_OPAQUE:
+				vkBorderColor = vk::BorderColor::eIntOpaqueBlack;
+				break;
+			case SamplerBorderColor::INT_ZERO_TRANSPARENT:
+				vkBorderColor = vk::BorderColor::eIntTransparentBlack;
+				break;
+			case SamplerBorderColor::FLOAT_ZERO_OPAQUE:
+				vkBorderColor = vk::BorderColor::eFloatOpaqueBlack;
+				break;
+			case SamplerBorderColor::FLOAT_ZERO_TRANSPARENT:
+				vkBorderColor = vk::BorderColor::eFloatTransparentBlack;
+				break;
+			case SamplerBorderColor::INT_ONE_OPAQUE:
+				vkBorderColor = vk::BorderColor::eIntOpaqueWhite;
+				break;
+			case SamplerBorderColor::FLOAT_ONE_OPAQUE:
+				vkBorderColor = vk::BorderColor::eFloatOpaqueWhite;
+				break;
 			default:
 				return SamplerHandle();
 		}
@@ -89,7 +117,7 @@ namespace vkcv {
 				vk::CompareOp::eAlways,
 				-1000.0f,
 				1000.0f,
-				vk::BorderColor::eIntOpaqueBlack,
+				vkBorderColor,
 				false
 		);
 		
diff --git a/src/vkcv/SamplerManager.hpp b/src/vkcv/SamplerManager.hpp
index aea47a03714b417314a09dfc0be855df31fbb557..128faa711993ac052cf774a1d31144d19362658f 100644
--- a/src/vkcv/SamplerManager.hpp
+++ b/src/vkcv/SamplerManager.hpp
@@ -30,10 +30,11 @@ namespace vkcv {
 		SamplerManager& operator=(SamplerManager&& other) = delete;
 		
 		SamplerHandle createSampler(SamplerFilterType magFilter,
-							  		SamplerFilterType minFilter,
-							  		SamplerMipmapMode mipmapMode,
-							  		SamplerAddressMode addressMode,
-							  		float mipLodBias);
+									SamplerFilterType minFilter,
+									SamplerMipmapMode mipmapMode,
+									SamplerAddressMode addressMode,
+									float mipLodBias,
+									SamplerBorderColor borderColor);
 		
 		[[nodiscard]]
 		vk::Sampler getVulkanSampler(const SamplerHandle& handle) const;