diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000000000000000000000000000000000000..540b00747eee74a94be3c987d1ac4c8595738a09
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,20 @@
+Alexander Gauggel
+Artur Wasmut
+Josch Morgenstern
+Katharina Krämer
+Lars Hoerttrich
+Leonie Franken
+Mara Vogt
+Mark O. Mints
+Sebastian Gaida
+Simeon Hermann
+Susanne Dötsch
+Tobias Frisch
+Trevor Hollmann
+Vanessa Karolek
+
+
+
+
+
+
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2a0858b970dead0233261c4f3c126a0bf64732f7..5f82b94af2a98dfb6700fb098a6b2a84adf7b9a8 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -45,6 +45,9 @@ endif()
 # configure everything to use the required dependencies
 include(${vkcv_config}/Libraries.cmake)
 
+# set macro to enable vulkan debug labels
+list(APPEND vkcv_definitions VULKAN_DEBUG_LABELS)
+
 # set the compile definitions aka preprocessor variables
 add_compile_definitions(${vkcv_definitions})
 
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..c1f3c5685b29d78b26dcc8c9e454b74c33f73ba5
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Universität Koblenz
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/include/vkcv/Core.hpp b/include/vkcv/Core.hpp
index 01d1455c20bc99d1956bd73a8a65eaef3350523c..028f8bcc10c3483c417462bc9382e45ae24e74d6 100644
--- a/include/vkcv/Core.hpp
+++ b/include/vkcv/Core.hpp
@@ -418,6 +418,16 @@ namespace vkcv
 		void prepareSwapchainImageForPresent(const CommandStreamHandle& handle);
 		void prepareImageForSampling(const CommandStreamHandle& cmdStream, const ImageHandle& image);
 		void prepareImageForStorage(const CommandStreamHandle& cmdStream, const ImageHandle& image);
+
+		// normally layout transitions for attachments are handled by the core
+		// however for manual vulkan use, e.g. ImGui integration, this function is exposed
+		// this is also why the command buffer is passed directly, instead of the command stream handle
+		void prepareImageForAttachmentManually(const vk::CommandBuffer& cmdBuffer, const ImageHandle& image);
+
+		// if manual vulkan work, e.g. ImGui integration, changes an image layout this function must be used
+		// to update the internal image state
+		void updateImageLayoutManual(const vkcv::ImageHandle& image, const vk::ImageLayout layout);
+
 		void recordImageMemoryBarrier(const CommandStreamHandle& cmdStream, const ImageHandle& image);
 		void recordBufferMemoryBarrier(const CommandStreamHandle& cmdStream, const BufferHandle& buffer);
 		void resolveMSAAImage(const CommandStreamHandle& cmdStream, const ImageHandle& src, const ImageHandle& dst);
diff --git a/modules/gui/src/vkcv/gui/GUI.cpp b/modules/gui/src/vkcv/gui/GUI.cpp
index 22c40d2937c69525c04ffd79f26107f829e42f4d..7ee335379603b3e21ab4d95f0738097bd954cf71 100644
--- a/modules/gui/src/vkcv/gui/GUI.cpp
+++ b/modules/gui/src/vkcv/gui/GUI.cpp
@@ -6,6 +6,9 @@
 
 namespace vkcv::gui {
 	
+	const static vk::ImageLayout initialImageLayout = vk::ImageLayout::eColorAttachmentOptimal;
+	const static vk::ImageLayout finalImageLayout   = vk::ImageLayout::ePresentSrcKHR;
+
 	static void checkVulkanResult(VkResult resultCode) {
 		if (resultCode == 0)
 			return;
@@ -95,8 +98,8 @@ namespace vkcv::gui {
 				vk::AttachmentStoreOp::eStore,
 				vk::AttachmentLoadOp::eDontCare,
 				vk::AttachmentStoreOp::eDontCare,
-				vk::ImageLayout::eUndefined,
-				vk::ImageLayout::ePresentSrcKHR
+				initialImageLayout,
+				finalImageLayout
 		);
 		
 		const vk::AttachmentReference attachmentReference (
@@ -199,7 +202,7 @@ namespace vkcv::gui {
 		
 		const Swapchain& swapchain = m_core.getSwapchain(m_windowHandle);
 		const auto extent = swapchain.getExtent();
-		
+
 		const vk::ImageView swapchainImageView = m_core.getSwapchainImageView();
 
 		const vk::FramebufferCreateInfo framebufferCreateInfo (
@@ -218,6 +221,10 @@ namespace vkcv::gui {
 		submitInfo.queueType = QueueType::Graphics;
 		
 		m_core.recordAndSubmitCommandsImmediate(submitInfo, [&](const vk::CommandBuffer& commandBuffer) {
+
+			assert(initialImageLayout == vk::ImageLayout::eColorAttachmentOptimal);
+			m_core.prepareImageForAttachmentManually(commandBuffer, vkcv::ImageHandle::createSwapchainImageHandle());
+
 			const vk::Rect2D renderArea (
 					vk::Offset2D(0, 0),
 					extent
@@ -230,12 +237,17 @@ namespace vkcv::gui {
 					0,
 					nullptr
 			);
-			
+
 			commandBuffer.beginRenderPass(beginInfo, vk::SubpassContents::eInline);
 			
 			ImGui_ImplVulkan_RenderDrawData(drawData, static_cast<VkCommandBuffer>(commandBuffer));
 			
 			commandBuffer.endRenderPass();
+
+			// executing the renderpass changed the image layout without going through the image manager
+			// therefore the layout must be updated manually
+			m_core.updateImageLayoutManual(vkcv::ImageHandle::createSwapchainImageHandle(), finalImageLayout);
+
 		}, [&]() {
 			m_context.getDevice().destroyFramebuffer(framebuffer);
 		});
diff --git a/projects/CMakeLists.txt b/projects/CMakeLists.txt
index 07623300db36f655c9ed94136c12b604d7713ca0..52d4a4a21f001b9feaab917fcdac4d52c6bf7e63 100644
--- a/projects/CMakeLists.txt
+++ b/projects/CMakeLists.txt
@@ -5,6 +5,9 @@ add_subdirectory(first_mesh)
 add_subdirectory(first_scene)
 add_subdirectory(particle_simulation)
 add_subdirectory(rtx_ambient_occlusion)
+add_subdirectory(sph)
 add_subdirectory(voxelization)
 add_subdirectory(mesh_shader)
+add_subdirectory(saf_r)
 add_subdirectory(indirect_dispatch)
+add_subdirectory(path_tracer)
\ No newline at end of file
diff --git a/projects/first_mesh/src/main.cpp b/projects/first_mesh/src/main.cpp
index 5ef277dfedbfa823a2b6fa55c5a6303117ddaa52..0871631827b87539bbe9b0050420088e199a39af 100644
--- a/projects/first_mesh/src/main.cpp
+++ b/projects/first_mesh/src/main.cpp
@@ -101,7 +101,7 @@ int main(int argc, const char** argv) {
 
 	// since we only use one descriptor set (namely, desc set 0), directly address it
 	// recreate copies of the bindings and the handles (to check whether they are properly reused instead of actually recreated)
-	std::unordered_map<uint32_t, vkcv::DescriptorBinding> set0Bindings = firstMeshProgram.getReflectedDescriptors().at(0);
+	const vkcv::DescriptorBindings& set0Bindings = firstMeshProgram.getReflectedDescriptors().at(0);
     auto set0BindingsExplicitCopy = set0Bindings;
 
 	vkcv::DescriptorSetLayoutHandle setLayoutHandle = core.createDescriptorSetLayout(set0Bindings);
diff --git a/projects/indirect_dispatch/assets/shaders/motionVectorMinMax.comp b/projects/indirect_dispatch/assets/shaders/motionVectorMinMax.comp
index 4ad350b0d5300aa63a66d7aceb00ea0b642d07ee..06b1b98f37579ae33406691bf19999d42ab7eb83 100644
--- a/projects/indirect_dispatch/assets/shaders/motionVectorMinMax.comp
+++ b/projects/indirect_dispatch/assets/shaders/motionVectorMinMax.comp
@@ -12,6 +12,7 @@ layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
 void main(){
     
     ivec2 outImageRes       = imageSize(outMotionMax);
+    ivec2 inImageRes        = textureSize(sampler2D(inMotion, textureSampler), 0);
     ivec2 motionTileCoord   = ivec2(gl_GlobalInvocationID.xy);
     
     if(any(greaterThanEqual(motionTileCoord, outImageRes)))
@@ -28,6 +29,14 @@ void main(){
     for(int x = 0; x < motionTileSize; x++){
         for(int y = 0; y < motionTileSize; y++){
             ivec2   sampleCoord     = motionBufferBaseCoord + ivec2(x, y);
+            
+            bool sampleIsOutsideImage = false;
+            sampleIsOutsideImage = sampleIsOutsideImage || any(greaterThanEqual(sampleCoord, inImageRes));
+            sampleIsOutsideImage = sampleIsOutsideImage || any(lessThan(sampleCoord, ivec2(0)));
+            
+            if(sampleIsOutsideImage)
+                continue;
+            
             vec2    motionSample    = texelFetch(sampler2D(inMotion, textureSampler), sampleCoord, 0).rg;
             float   velocitySample  = length(motionSample);
             
diff --git a/projects/indirect_dispatch/assets/shaders/motionVectorMinMaxNeighbourhood.comp b/projects/indirect_dispatch/assets/shaders/motionVectorMinMaxNeighbourhood.comp
index 4d6e7c0af6115e816ba087570e5585ffde23b1e6..3f836341a97a683efe88f41416d541624be03a0e 100644
--- a/projects/indirect_dispatch/assets/shaders/motionVectorMinMaxNeighbourhood.comp
+++ b/projects/indirect_dispatch/assets/shaders/motionVectorMinMaxNeighbourhood.comp
@@ -12,6 +12,7 @@ layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
 void main(){
     
     ivec2 outImageRes       = imageSize(outMotionMaxNeighbourhood);
+    ivec2 inImageRes        = textureSize(sampler2D(inMotionMax, textureSampler), 0);
     ivec2 motionTileCoord   = ivec2(gl_GlobalInvocationID.xy);
     
     if(any(greaterThanEqual(motionTileCoord, outImageRes)))
@@ -27,6 +28,13 @@ void main(){
         for(int y = -1; y <= 1; y++){
             ivec2   sampleCoord         = motionTileCoord + ivec2(x, y);
             
+            bool sampleIsOutsideImage = false;
+            sampleIsOutsideImage = sampleIsOutsideImage || any(greaterThanEqual(sampleCoord, inImageRes));
+            sampleIsOutsideImage = sampleIsOutsideImage || any(lessThan(sampleCoord, ivec2(0)));
+            
+            if(sampleIsOutsideImage)
+                continue;
+            
             vec2    motionSampleMax     = texelFetch(sampler2D(inMotionMax, textureSampler), sampleCoord, 0).rg;
             float   velocitySampleMax   = length(motionSampleMax);
             
diff --git a/projects/indirect_dispatch/assets/shaders/motionVectorVisualisation.comp b/projects/indirect_dispatch/assets/shaders/motionVectorVisualisation.comp
index 1cfb09c87e8288b8ea80c6ddfbe5f0d4918b7f2e..fdceb575feaf24e7114bbcf223585a28955f45b8 100644
--- a/projects/indirect_dispatch/assets/shaders/motionVectorVisualisation.comp
+++ b/projects/indirect_dispatch/assets/shaders/motionVectorVisualisation.comp
@@ -21,7 +21,10 @@ void main(){
     if(any(greaterThanEqual(coord, outImageRes)))
         return;
 
-    vec2 motionVector           = texelFetch(sampler2D(inMotion, textureSampler), coord / motionTileSize, 0).rg;
+    vec2    uv              = (coord + 0.5) / vec2(outImageRes);
+    ivec2   inTextureRes    = textureSize(sampler2D(inMotion, textureSampler), 0);
+    
+    vec2 motionVector           = texelFetch(sampler2D(inMotion, textureSampler), ivec2(uv * inTextureRes), 0).rg;
     vec2 motionVectorNormalized = clamp(motionVector / range, -1, 1);
     
     vec2 color  = motionVectorNormalized * 0.5 + 0.5;
diff --git a/projects/indirect_dispatch/src/App.cpp b/projects/indirect_dispatch/src/App.cpp
index d4afc61c7421bd45c773bbdbc3da796b868869d2..532cef11db5b2bb8b5d741f6505507ff23fa4163 100644
--- a/projects/indirect_dispatch/src/App.cpp
+++ b/projects/indirect_dispatch/src/App.cpp
@@ -28,7 +28,7 @@ App::App() :
 		VK_MAKE_VERSION(0, 0, 1),
 		{ vk::QueueFlagBits::eGraphics ,vk::QueueFlagBits::eCompute , vk::QueueFlagBits::eTransfer },
 		{ VK_KHR_SWAPCHAIN_EXTENSION_NAME })),
-	m_windowHandle(m_core.createWindow(m_applicationName, m_windowWidth, m_windowHeight, false)),
+	m_windowHandle(m_core.createWindow(m_applicationName, m_windowWidth, m_windowHeight, true)),
 	m_cameraManager(m_core.getWindow(m_windowHandle)){}
 
 bool App::initialize() {
diff --git a/projects/particle_simulation/src/main.cpp b/projects/particle_simulation/src/main.cpp
index ddbe6195e66e78504d4bccb32b3b09ff680ab414..ae3c6795a66cdc81297986acb224a63055d02c44 100644
--- a/projects/particle_simulation/src/main.cpp
+++ b/projects/particle_simulation/src/main.cpp
@@ -22,7 +22,7 @@ int main(int argc, const char **argv) {
             {vk::QueueFlagBits::eTransfer, vk::QueueFlagBits::eGraphics, vk::QueueFlagBits::eCompute},
 			{ VK_KHR_SWAPCHAIN_EXTENSION_NAME }
     );
-	vkcv::WindowHandle windowHandle = core.createWindow(applicationName, windowWidth, windowHeight, true);
+	vkcv::WindowHandle windowHandle = core.createWindow(applicationName, windowWidth, windowHeight, false);
     vkcv::Window& window = core.getWindow(windowHandle);
 	vkcv::camera::CameraManager cameraManager(window);
 
@@ -61,7 +61,7 @@ int main(int argc, const char **argv) {
 			shaderPathFragment = "shaders/shader_space.frag";
     	} else
 		if (strcmp(argv[i], "--water") == 0) {
-			shaderPathCompute = "shaders/shader_water.comp";
+			shaderPathCompute = "shaders/shader_water1.comp";
 			shaderPathFragment = "shaders/shader_water.frag";
 		} else
 		if (strcmp(argv[i], "--gravity") == 0) {
diff --git a/projects/path_tracer/.gitignore b/projects/path_tracer/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..ff3dff30031efafa24269a9ac0ef93f64f63ded1
--- /dev/null
+++ b/projects/path_tracer/.gitignore
@@ -0,0 +1 @@
+saf_r
\ No newline at end of file
diff --git a/projects/path_tracer/CMakeLists.txt b/projects/path_tracer/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..2b8edc208c70a5e8e74c6c28221f783a68a3ec6c
--- /dev/null
+++ b/projects/path_tracer/CMakeLists.txt
@@ -0,0 +1,29 @@
+cmake_minimum_required(VERSION 3.16)
+project(path_tracer)
+
+# setting c++ standard for the project
+set(CMAKE_CXX_STANDARD 17)
+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(path_tracer
+		src/main.cpp)
+
+# this should fix the execution path to load local files from the project (for MSVC)
+if(MSVC)
+	set_target_properties(path_tracer PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+	set_target_properties(path_tracer PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+
+	# in addition to setting the output directory, the working directory has to be set
+	# by default visual studio sets the working directory to the build directory, when using the debugger
+	set_target_properties(path_tracer PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+endif()
+
+# including headers of dependencies and the VkCV framework
+target_include_directories(path_tracer SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_testing_include} ${vkcv_asset_loader_include} ${vkcv_camera_include} ${vkcv_shader_compiler_include} ${vkcv_gui_include})
+
+# linking with libraries from all dependencies and the VkCV framework
+target_link_libraries(path_tracer vkcv vkcv_testing vkcv_asset_loader ${vkcv_asset_loader_libraries} vkcv_camera vkcv_shader_compiler vkcv_gui)
diff --git a/projects/path_tracer/shaders/clearImage.comp b/projects/path_tracer/shaders/clearImage.comp
new file mode 100644
index 0000000000000000000000000000000000000000..97998e945112d166be7d00df98ee44ea8322a633
--- /dev/null
+++ b/projects/path_tracer/shaders/clearImage.comp
@@ -0,0 +1,17 @@
+#version 440
+#extension GL_GOOGLE_include_directive : enable
+
+layout(set=0, binding=0, rgba32f) 	uniform image2D outImage;
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+void main(){
+
+    ivec2 outImageRes = imageSize(outImage);
+    ivec2 coord       = ivec2(gl_GlobalInvocationID.xy);
+
+    if(any(greaterThanEqual(coord, outImageRes)))
+        return;
+
+    imageStore(outImage, coord, vec4(0));
+}
\ No newline at end of file
diff --git a/projects/path_tracer/shaders/combineImages.comp b/projects/path_tracer/shaders/combineImages.comp
new file mode 100644
index 0000000000000000000000000000000000000000..d1a4e85caf175dfc3125afd847d7458ddec2fef1
--- /dev/null
+++ b/projects/path_tracer/shaders/combineImages.comp
@@ -0,0 +1,21 @@
+#version 440
+#extension GL_GOOGLE_include_directive : enable
+
+layout(set=0, binding=0, rgba32f) uniform image2D newImage;
+layout(set=0, binding=1, rgba32f) uniform image2D meanImage;
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+void main(){
+
+    ivec2 outImageRes = imageSize(meanImage);
+    ivec2 coord       = ivec2(gl_GlobalInvocationID.xy);
+
+    if(any(greaterThanEqual(coord, outImageRes)))
+        return;
+   
+    vec4 colorNew 	= imageLoad(newImage,  coord);
+	vec4 colorMean 	= imageLoad(meanImage, coord);
+
+    imageStore(meanImage, coord, colorNew + colorMean);
+}
\ No newline at end of file
diff --git a/projects/path_tracer/shaders/path_tracer.comp b/projects/path_tracer/shaders/path_tracer.comp
new file mode 100644
index 0000000000000000000000000000000000000000..f08bdfd123ede964befe5feed4ba9f438dc0a498
--- /dev/null
+++ b/projects/path_tracer/shaders/path_tracer.comp
@@ -0,0 +1,430 @@
+#version 450 core
+#extension GL_ARB_separate_shader_objects : enable
+
+const float pi      	= 3.1415926535897932384626433832795;
+const float hitBias 	= 0.0001;   // used to offset hits to avoid self intersection
+const float denomMin 	= 0.001;
+
+layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in;
+
+struct Material {
+    vec3 	emission;
+	float 	ks;		// specular percentage
+    vec3 	albedo;
+    float 	r;		// roughness
+	vec3 	f0;
+	float 	padding;
+};
+
+struct Sphere{
+    vec3 	center;
+    float 	radius;
+    int 	materialIndex;
+	float 	padding[3];
+};
+
+struct Plane{
+	vec3 	center;
+	int 	materialIndex;
+	vec3 	N;
+	float 	padding1;
+	vec2 	extent;
+	vec2 	padding2;
+};
+
+layout(std430, binding = 0) buffer spheres{
+    Sphere inSpheres[];
+};
+
+layout(std430, binding = 1) buffer planes{
+    Plane inPlanes[];
+};
+
+layout(std430, binding = 2) buffer materials{
+    Material inMaterials[];
+};
+
+layout(set=0, binding = 3, rgba32f) uniform image2D outImage;
+
+layout( push_constant ) uniform constants{
+    mat4 	viewToWorld;
+	vec3 	skyColor;
+    int 	sphereCount;
+	int 	planeCount;
+	int 	frameIndex;
+};
+
+// ---- Intersection functions ----
+
+struct Ray{
+	vec3 origin;
+	vec3 direction;
+};
+
+struct Intersection{
+    bool 		hit;
+	float 		distance;
+    vec3 		pos;
+    vec3 		N;
+    Material 	material;
+};
+
+// https://www.scratchapixel.com/lessons/3d-basic-rendering/minimal-ray-tracer-rendering-simple-shapes/ray-sphere-intersection
+Intersection raySphereIntersect(Ray ray, Sphere sphere){
+
+	Intersection intersection;
+	intersection.hit = false;
+	
+    vec3 	L 	= sphere.center - ray.origin;
+    float 	tca = dot(L, ray.direction);
+    float 	d2 	= dot(L, L) - tca * tca;
+	
+    if (d2 > sphere.radius * sphere.radius){
+        return intersection;
+    }
+    float thc 	= float(sqrt(sphere.radius * sphere.radius - d2));
+    float t0 	= tca - thc;
+    float t1 	= tca + thc;
+	
+    if (t0 < 0)
+        t0 = t1;
+    
+    if (t0 < 0)
+        return intersection;
+	
+	intersection.hit 		= true;
+	intersection.distance 	= t0;
+	intersection.pos        = ray.origin + ray.direction * intersection.distance;
+	intersection.N          = normalize(intersection.pos - sphere.center);
+	intersection.material 	= inMaterials[sphere.materialIndex];
+	
+    return intersection;
+}
+
+struct Basis{
+	vec3 right;
+	vec3 up;
+	vec3 forward;
+};
+
+Basis buildBasisAroundNormal(vec3 N){
+	Basis 	basis;
+	basis.up 		= N;
+	basis.right 	= abs(basis.up.x) < 0.99 ?  vec3(1, 0, 0) : vec3(0, 0, 1);
+	basis.forward 	= normalize(cross(basis.up, basis.right));
+	basis.right 	= cross(basis.up, basis.forward);
+	return basis;
+}
+
+// see: https://www.scratchapixel.com/lessons/3d-basic-rendering/minimal-ray-tracer-rendering-simple-shapes/ray-plane-and-ray-disk-intersection
+Intersection rayPlaneIntersect(Ray ray, Plane plane){
+
+	Intersection intersection;
+	intersection.hit = false;
+
+	vec3 	toPlane = plane.center - ray.origin;
+	float 	denom 	= dot(ray.direction, plane.N);
+	if(abs(denom) < 0.001)
+		return intersection;
+		
+	intersection.distance = dot(toPlane, plane.N) / denom;
+	
+	if(intersection.distance < 0)
+		return intersection;
+	
+	intersection.pos 				= ray.origin + ray.direction * intersection.distance;
+	
+	vec3 	centerToIntersection	= intersection.pos - plane.center;
+	Basis 	planeBasis 				= buildBasisAroundNormal(plane.N);
+	float 	projectedRight			= dot(centerToIntersection, planeBasis.right);
+	float 	projectedUp				= dot(centerToIntersection, planeBasis.forward);
+
+	intersection.hit 		= abs(projectedRight) <= plane.extent.x && abs(projectedUp) <= plane.extent.y;
+	intersection.N 			= plane.N;
+	intersection.material 	= inMaterials[plane.materialIndex];
+	
+	return intersection;
+}
+
+Intersection sceneIntersect(Ray ray) {
+    float minDistance = 100000;  // lets start with something big
+    
+    Intersection intersection;
+    intersection.hit = false;
+    
+    for (int i = 0; i < sphereCount; i++) {
+		Intersection sphereIntersection = raySphereIntersect(ray, inSpheres[i]);
+        if (sphereIntersection.hit && sphereIntersection.distance < minDistance) {            
+            intersection 	= sphereIntersection;
+			minDistance 	= intersection.distance;
+        }
+    }
+	for (int i = 0; i < planeCount; i++){
+		Intersection planeIntersection = rayPlaneIntersect(ray, inPlanes[i]);
+        if (planeIntersection.hit && planeIntersection.distance < minDistance) {
+            intersection 	= planeIntersection;
+			minDistance 	= intersection.distance;
+        }
+	}
+    return intersection;
+}
+
+vec3 biasHitPosition(vec3 hitPos, vec3 rayDirection, vec3 N){
+    // return hitPos + N * hitBias; // works as long as no refraction/transmission is used and camera is outside sphere
+    return hitPos + sign(dot(rayDirection, N)) * N * hitBias;
+}
+
+// ---- noise/hash functions for pseudorandom variables ----
+
+// extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences
+vec2 r2Sequence(uint n){
+	n = n % 42000;
+	const float g = 1.32471795724474602596;
+	return fract(vec2(
+		n /  g,
+		n / (g*g)));
+}
+
+// random() and helpers from: https://www.shadertoy.com/view/XlycWh
+float g_seed = 0;
+
+uint base_hash(uvec2 p) {
+    p = 1103515245U*((p >> 1U)^(p.yx));
+    uint h32 = 1103515245U*((p.x)^(p.y>>3U));
+    return h32^(h32 >> 16);
+}
+
+vec2 hash2(inout float seed) {
+    uint n = base_hash(floatBitsToUint(vec2(seed+=.1,seed+=.1)));
+    uvec2 rz = uvec2(n, n*48271U);
+    return vec2(rz.xy & uvec2(0x7fffffffU))/float(0x7fffffff);
+}
+
+void initRandom(ivec2 coord){
+	g_seed = float(base_hash(coord)/float(0xffffffffU)+frameIndex);
+}
+
+vec2 random(){
+	return hash2(g_seed);
+}
+
+// ---- shading ----
+
+vec3 lambertBRDF(vec3 albedo){
+	return albedo / pi;
+}
+
+vec3 computeDiffuseBRDF(Material material){
+    return lambertBRDF(material.albedo);
+}
+
+float distributionGGX(float r, float NoH){
+	float r2 	= r*r;
+	float denom = pi * pow(NoH*NoH * (r2-1) + 1, 2);
+	return r2 / max(denom, denomMin);
+}
+
+float geometryGGXSmith(float r, float NoL){
+	float r2 	= r*r;
+	float denom = NoL + sqrt(r2 + (1-r2) * NoL*NoL);
+	return 2 * NoL / max(denom, denomMin);
+}
+
+float geometryGGX(float r, float NoV, float NoL){
+	return geometryGGXSmith(r, NoV) * geometryGGXSmith(r, NoL);
+}
+
+vec3 fresnelSchlick(vec3 f0, float NoH){
+	return f0 + (1 - f0) * pow(1 - NoH, 5);
+}
+
+vec3 computeSpecularBRDF(vec3 f0, float r, float NoV, float NoL, float NoH){
+	float 	denom 	= 4 * NoV * NoL;
+	float 	D 		= distributionGGX(r, NoH);
+	float 	G 		= geometryGGX(r, NoV, NoL);
+	vec3	F 		= fresnelSchlick(f0, NoH);
+	return D * F * G / max(denom, denomMin);
+}
+
+// ---- pathtracing and main ----
+
+// distributions: https://link.springer.com/content/pdf/10.1007/978-1-4842-4427-2_16.pdf
+float cosineDistributionPDF(float NoL){
+	return NoL / pi;
+}
+
+vec3 sampleCosineDistribution(vec2 xi){
+	float phi = 2 * pi * xi.y;
+	return vec3(
+		sqrt(xi.x) * cos(phi),
+		sqrt(1 - xi.x),
+		sqrt(xi.x) * sin(phi));
+}
+
+float uniformDistributionPDF(){
+	return 1.f / (2 * pi);
+}
+
+vec3 sampleUniformDistribution(vec2 xi){
+	float phi = 2 * pi * xi.y;
+	return vec3(
+		sqrt(xi.x) * cos(phi),
+		1 - xi.x,
+		sqrt(xi.x) * sin(phi));
+}
+
+float ggxDistributionPDF(float r, float NoH){
+	return distributionGGX(r, NoH) * NoH;
+}
+
+float ggxDistributionPDFReflected(float r, float NoH, float NoV){
+	float jacobian = 0.25 / max(NoV, denomMin);
+	return ggxDistributionPDF(r, NoH) * jacobian;
+}
+
+vec3 sampleGGXDistribution(vec2 xi, float r){
+	float phi 		= 2 * pi * xi.y;
+	float cosTheta 	= sqrt((1 - xi.x) / ((r*r - 1) * xi.x + 1));
+	float sinTheta	= sqrt(1 - cosTheta*cosTheta);
+	return vec3(
+		cos(phi) * sinTheta,
+		cosTheta,
+		sin(phi) * sinTheta);
+}
+
+vec3 sampleTangentToWorldSpace(vec3 tangentSpaceSample, vec3 N){
+	Basis tangentBasis = buildBasisAroundNormal(N);
+	return
+		tangentBasis.right		* tangentSpaceSample.x +
+		tangentBasis.up			* tangentSpaceSample.y +
+		tangentBasis.forward 	* tangentSpaceSample.z;
+}
+
+vec3 castRay(Ray ray) {
+    
+    vec3   	throughput 	= vec3(1);
+    vec3	color   	= vec3(0);
+    
+	const int maxDepth = 10;
+    for(int i = 0; i < maxDepth; i++){
+
+        Intersection intersection = sceneIntersect(ray);
+        
+        vec3 hitLighting 	= vec3(0);
+        vec3 brdf 			= vec3(1);
+		
+		// V is where the ray came from and will lead back to the camera (over multiple bounces)
+		vec3 	V 				= -normalize(ray.direction);
+		vec3 	R 				= reflect(-V, intersection.N);
+		float 	NoV				= max(dot(intersection.N, V), 0);
+		
+		intersection.material.r *= intersection.material.r;	// remapping for perceuptual linearity
+		intersection.material.r = max(intersection.material.r, 0.01);
+		
+		float 	kd 				= 1 - intersection.material.ks;
+		bool 	sampleDiffuse 	= random().x < kd;
+		
+		vec3 	sampleTangentSpace;
+		float 	pdf;
+		if(sampleDiffuse){
+			sampleTangentSpace 	= sampleCosineDistribution(random());
+			ray.direction   	= sampleTangentToWorldSpace(sampleTangentSpace, intersection.N);
+			
+			float NoL 			= max(dot(intersection.N, ray.direction), 0);
+			pdf 				= cosineDistributionPDF(NoL);
+		}
+		else{			
+			#define IMPORTANCE
+			
+			#ifdef IMPORTANCE
+			sampleTangentSpace 	= sampleGGXDistribution(random(), intersection.material.r);
+			ray.direction   	= sampleTangentToWorldSpace(sampleTangentSpace, R);		
+			vec3 	L 			= normalize(ray.direction);
+			pdf 				= ggxDistributionPDFReflected(intersection.material.r, max(sampleTangentSpace.y, 0.01), max(dot(intersection.N, V), 0.01));
+			#else
+			sampleTangentSpace 	= sampleUniformDistribution(random());
+			ray.direction   	= sampleTangentToWorldSpace(sampleTangentSpace, intersection.N);		
+			pdf 				= uniformDistributionPDF();
+			#endif
+		}
+        
+        ray.origin				= biasHitPosition(intersection.pos, ray.direction, intersection.N);
+		
+		// L is where the ray is going, as that is the direction where light will from
+		vec3 	L 			= normalize(ray.direction);
+		vec3 	H			= normalize(L + V);
+		
+        float 	NoL 		= max(dot(intersection.N, L), 0);
+		float	NoH			= max(dot(intersection.N, H), 0);
+		
+        if(intersection.hit){
+            vec3 	diffuseBRDF 	= computeDiffuseBRDF(intersection.material);
+
+			vec3 	specularBRDF 	= computeSpecularBRDF(intersection.material.f0, intersection.material.r, NoV, NoL, NoH);
+			brdf 					= mix(diffuseBRDF, specularBRDF, intersection.material.ks);
+			
+			
+			hitLighting = intersection.material.emission * max(sign(NoV), 0);	// objects only emit in direction of normal
+        }
+        else{
+            hitLighting = skyColor;
+        }
+        
+        color       	+= hitLighting * throughput;
+        throughput  	*= brdf * NoL / max(pdf, denomMin);
+        
+        if(!intersection.hit)
+            break;
+    }
+
+    return color;
+}
+
+// coord must be in pixel coordinates, but already shifted to pixel center
+vec3 computeCameraRay(vec2 coord){
+
+    ivec2 outImageRes   = imageSize(outImage);
+    float fovDegree     = 45;
+    float fov           = fovDegree * pi / 180;
+    
+    vec2 uv     		= coord / vec2(outImageRes);
+    vec2 ndc    		= 2 * uv - 1;
+    
+    float tanFovHalf    = tan(fov / 2.f);
+    float aspectRatio   = outImageRes.x / float(outImageRes.y);
+    float x             =  ndc.x * tanFovHalf * aspectRatio;
+    float y             = -ndc.y * tanFovHalf;
+    
+    // view direction goes through pixel on image plane with z=1
+    vec3 directionViewSpace     = normalize(vec3(x, y, 1));
+    vec3 directionWorldSpace    = mat3(viewToWorld) * directionViewSpace;
+    return directionWorldSpace;
+}
+
+void main(){
+    ivec2 	coord     = ivec2(gl_GlobalInvocationID.xy);
+	vec2 	pixelSize = 1.f / coord;
+	initRandom(coord);
+	
+	Ray cameraRay;
+    cameraRay.origin  	= viewToWorld[3].xyz;
+	vec2 coordCentered 	= coord + 0.5;
+	
+	vec3 color = vec3(0);
+	
+	const int samplesPerPixel = 1;
+	for(int i = 0; i < samplesPerPixel; i++){
+		vec2 jitter 		= r2Sequence(i + frameIndex) - 0.5;
+		cameraRay.direction = computeCameraRay(coordCentered + jitter);
+		color 				+= castRay(cameraRay);
+	}
+	color /= samplesPerPixel;
+		
+	vec4 final = vec4(color, 1);
+	
+	// occasional NaNs in reflection, should be fixed properly
+	if(any(isnan(color)))
+		final = vec4(0);
+	
+    imageStore(outImage, coord, final);
+}
\ No newline at end of file
diff --git a/projects/path_tracer/shaders/presentImage.comp b/projects/path_tracer/shaders/presentImage.comp
new file mode 100644
index 0000000000000000000000000000000000000000..a52159c0c6173779b091e5d4153b15b0a6361780
--- /dev/null
+++ b/projects/path_tracer/shaders/presentImage.comp
@@ -0,0 +1,23 @@
+#version 440
+#extension GL_GOOGLE_include_directive : enable
+
+layout(set=0, binding=0, rgba32f) 	uniform image2D inImage;
+layout(set=0, binding=1, rgba8) 	uniform image2D outImage;
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+void main(){
+
+    ivec2 outImageRes = imageSize(outImage);
+    ivec2 coord       = ivec2(gl_GlobalInvocationID.xy);
+
+    if(any(greaterThanEqual(coord, outImageRes)))
+        return;
+   
+    vec4 colorRaw 				= imageLoad(inImage, coord);
+	vec3 colorNormalized 		= colorRaw.rgb / colorRaw.a;
+	vec3 colorTonemapped		= colorNormalized / (1 + dot(colorNormalized, vec3(0.71, 0.21, 0.08)));	// reinhard tonemapping
+	vec3 colorGammaCorrected 	= pow(colorTonemapped, vec3(1.f / 2.2));
+
+    imageStore(outImage, coord, vec4(colorGammaCorrected, 0));
+}
\ No newline at end of file
diff --git a/projects/path_tracer/src/main.cpp b/projects/path_tracer/src/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c3b21f78cea479a463fb6224605a202d35d8e581
--- /dev/null
+++ b/projects/path_tracer/src/main.cpp
@@ -0,0 +1,451 @@
+#include <vkcv/Core.hpp>
+#include <vkcv/camera/CameraManager.hpp>
+#include <vkcv/asset/asset_loader.hpp>
+#include <vkcv/shader/GLSLCompiler.hpp>
+#include "vkcv/gui/GUI.hpp"
+#include <chrono>
+#include <vector>
+
+int main(int argc, const char** argv) {
+
+	// structs must match shader version
+	struct Material {
+		Material(const glm::vec3& emission, const glm::vec3& albedo, float ks, float roughness, const glm::vec3& f0)
+			: emission(emission), albedo(albedo), ks(ks), roughness(roughness), f0(f0){}
+
+		glm::vec3   emission;
+		float       ks;
+		glm::vec3   albedo;
+		float       roughness;
+		glm::vec3   f0;
+		float       padding;
+	};
+
+	struct Sphere {
+		Sphere(const glm::vec3& c, const float& r, const int m) : center(c), radius(r), materialIndex(m) {}
+
+		glm::vec3   center;
+		float       radius;
+		uint32_t    materialIndex;
+		float       padding[3];
+	};
+
+	struct Plane {
+		Plane(const glm::vec3& c, const glm::vec3& n, const glm::vec2 e, int m)
+			: center(c), normal(n), extent(e), materialIndex(m) {}
+
+		glm::vec3   center;
+		uint32_t    materialIndex;
+		glm::vec3   normal;
+		float       padding1;
+		glm::vec2   extent;
+		glm::vec2   padding3;
+	};
+
+	const char* applicationName = "Path Tracer";
+
+	const int initialWidth = 1280;
+	const int initialHeight = 720;
+	vkcv::Window window = vkcv::Window::create(
+		applicationName,
+		initialWidth,
+		initialHeight,
+		true);
+
+	vkcv::Core core = vkcv::Core::create(
+		window,
+		applicationName,
+		VK_MAKE_VERSION(0, 0, 1),
+		{ vk::QueueFlagBits::eTransfer,vk::QueueFlagBits::eGraphics, vk::QueueFlagBits::eCompute },
+		{ "VK_KHR_swapchain" }
+	);
+
+	// images
+	vkcv::ImageHandle outputImage = core.createImage(
+		vk::Format::eR32G32B32A32Sfloat,
+		initialWidth,
+		initialHeight,
+		1,
+		false,
+		true).getHandle();
+
+	vkcv::ImageHandle meanImage = core.createImage(
+		vk::Format::eR32G32B32A32Sfloat,
+		initialWidth,
+		initialHeight,
+		1,
+		false,
+		true).getHandle();
+
+	vkcv::shader::GLSLCompiler compiler;
+
+	// path tracing shader
+	vkcv::ShaderProgram traceShaderProgram{};
+
+	compiler.compile(vkcv::ShaderStage::COMPUTE, "shaders/path_tracer.comp", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		traceShaderProgram.addShader(shaderStage, path);
+	});
+
+	const vkcv::DescriptorBindings& traceDescriptorBindings     = traceShaderProgram.getReflectedDescriptors().at(0);
+	vkcv::DescriptorSetLayoutHandle traceDescriptorSetLayout    = core.createDescriptorSetLayout(traceDescriptorBindings);
+	vkcv::DescriptorSetHandle       traceDescriptorSet          = core.createDescriptorSet(traceDescriptorSetLayout);
+
+	// image combine shader
+	vkcv::ShaderProgram imageCombineShaderProgram{};
+
+	compiler.compile(vkcv::ShaderStage::COMPUTE, "shaders/combineImages.comp", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		imageCombineShaderProgram.addShader(shaderStage, path);
+	});
+
+	const vkcv::DescriptorBindings& imageCombineDescriptorBindings  = imageCombineShaderProgram.getReflectedDescriptors().at(0);
+	vkcv::DescriptorSetLayoutHandle imageCombineDescriptorSetLayout = core.createDescriptorSetLayout(imageCombineDescriptorBindings);
+	vkcv::DescriptorSetHandle       imageCombineDescriptorSet       = core.createDescriptorSet(imageCombineDescriptorSetLayout);
+	vkcv::PipelineHandle            imageCombinePipeline            = core.createComputePipeline(
+		imageCombineShaderProgram, 
+		{ core.getDescriptorSetLayout(imageCombineDescriptorSetLayout).vulkanHandle });
+
+	vkcv::DescriptorWrites imageCombineDescriptorWrites;
+	imageCombineDescriptorWrites.storageImageWrites = {
+		vkcv::StorageImageDescriptorWrite(0, outputImage),
+		vkcv::StorageImageDescriptorWrite(1, meanImage)
+	};
+	core.writeDescriptorSet(imageCombineDescriptorSet, imageCombineDescriptorWrites);
+
+	// image present shader
+	vkcv::ShaderProgram presentShaderProgram{};
+
+	compiler.compile(vkcv::ShaderStage::COMPUTE, "shaders/presentImage.comp", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		presentShaderProgram.addShader(shaderStage, path);
+	});
+
+	const vkcv::DescriptorBindings& presentDescriptorBindings   = presentShaderProgram.getReflectedDescriptors().at(0);
+	vkcv::DescriptorSetLayoutHandle presentDescriptorSetLayout  = core.createDescriptorSetLayout(presentDescriptorBindings);
+	vkcv::DescriptorSetHandle       presentDescriptorSet        = core.createDescriptorSet(presentDescriptorSetLayout);
+	vkcv::PipelineHandle            presentPipeline             = core.createComputePipeline(
+		presentShaderProgram,
+		{ core.getDescriptorSetLayout(presentDescriptorSetLayout).vulkanHandle });
+
+	// clear shader
+	vkcv::ShaderProgram clearShaderProgram{};
+
+	compiler.compile(vkcv::ShaderStage::COMPUTE, "shaders/clearImage.comp", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		clearShaderProgram.addShader(shaderStage, path);
+	});
+
+	const vkcv::DescriptorBindings& imageClearDescriptorBindings    = clearShaderProgram.getReflectedDescriptors().at(0);
+	vkcv::DescriptorSetLayoutHandle imageClearDescriptorSetLayout   = core.createDescriptorSetLayout(imageClearDescriptorBindings);
+	vkcv::DescriptorSetHandle       imageClearDescriptorSet         = core.createDescriptorSet(imageClearDescriptorSetLayout);
+	vkcv::PipelineHandle            imageClearPipeline              = core.createComputePipeline(
+		clearShaderProgram,
+		{ core.getDescriptorSetLayout(imageClearDescriptorSetLayout).vulkanHandle });
+
+	vkcv::DescriptorWrites imageClearDescriptorWrites;
+	imageClearDescriptorWrites.storageImageWrites = {
+		vkcv::StorageImageDescriptorWrite(0, meanImage)
+	};
+	core.writeDescriptorSet(imageClearDescriptorSet, imageClearDescriptorWrites);
+
+	// buffers
+	typedef std::pair<std::string, Material> MaterialSetting;
+
+	std::vector<MaterialSetting> materialSettings;
+	materialSettings.emplace_back(MaterialSetting("white",  Material(glm::vec3(0),    glm::vec3(0.65),          0, 0.25, glm::vec3(0.04))));
+	materialSettings.emplace_back(MaterialSetting("red",    Material(glm::vec3(0),    glm::vec3(0.5, 0.0, 0.0), 0, 0.25, glm::vec3(0.04))));
+	materialSettings.emplace_back(MaterialSetting("green",  Material(glm::vec3(0),    glm::vec3(0.0, 0.5, 0.0), 0, 0.25, glm::vec3(0.04))));
+	materialSettings.emplace_back(MaterialSetting("light",  Material(glm::vec3(20),   glm::vec3(0),             0, 0.25, glm::vec3(0.04))));
+	materialSettings.emplace_back(MaterialSetting("sphere", Material(glm::vec3(0),    glm::vec3(0.65),          1, 0.25, glm::vec3(0.04))));
+	materialSettings.emplace_back(MaterialSetting("ground", Material(glm::vec3(0),    glm::vec3(0.65),          0, 0.25, glm::vec3(0.04))));
+
+	const uint32_t whiteMaterialIndex   = 0;
+	const uint32_t redMaterialIndex     = 1;
+	const uint32_t greenMaterialIndex   = 2;
+	const uint32_t lightMaterialIndex   = 3;
+	const uint32_t sphereMaterialIndex  = 4;
+	const uint32_t groundMaterialIndex  = 5;
+
+	std::vector<Sphere> spheres;
+	spheres.emplace_back(Sphere(glm::vec3(0, -1.5, 0), 0.5, sphereMaterialIndex));
+
+	std::vector<Plane> planes;
+	planes.emplace_back(Plane(glm::vec3( 0, -2,     0), glm::vec3( 0,  1,  0), glm::vec2(2), groundMaterialIndex));
+	planes.emplace_back(Plane(glm::vec3( 0,  2,     0), glm::vec3( 0, -1,  0), glm::vec2(2), whiteMaterialIndex));
+	planes.emplace_back(Plane(glm::vec3( 2,  0,     0), glm::vec3(-1,  0,  0), glm::vec2(2), redMaterialIndex));
+	planes.emplace_back(Plane(glm::vec3(-2,  0,     0), glm::vec3( 1,  0,  0), glm::vec2(2), greenMaterialIndex));
+	planes.emplace_back(Plane(glm::vec3( 0,  0,     2), glm::vec3( 0,  0, -1), glm::vec2(2), whiteMaterialIndex));
+	planes.emplace_back(Plane(glm::vec3( 0,  1.9,   0), glm::vec3( 0, -1,  0), glm::vec2(1), lightMaterialIndex));
+
+	vkcv::Buffer<Sphere> sphereBuffer = core.createBuffer<Sphere>(
+		vkcv::BufferType::STORAGE,
+		spheres.size());
+	sphereBuffer.fill(spheres);
+
+	vkcv::Buffer<Plane> planeBuffer = core.createBuffer<Plane>(
+		vkcv::BufferType::STORAGE,
+		planes.size());
+	planeBuffer.fill(planes);
+
+	vkcv::Buffer<Material> materialBuffer = core.createBuffer<Material>(
+		vkcv::BufferType::STORAGE,
+		materialSettings.size());
+
+	vkcv::DescriptorWrites traceDescriptorWrites;
+	traceDescriptorWrites.storageBufferWrites = { 
+		vkcv::BufferDescriptorWrite(0, sphereBuffer.getHandle()),
+		vkcv::BufferDescriptorWrite(1, planeBuffer.getHandle()),
+		vkcv::BufferDescriptorWrite(2, materialBuffer.getHandle())};
+	traceDescriptorWrites.storageImageWrites = {
+		vkcv::StorageImageDescriptorWrite(3, outputImage)};
+	core.writeDescriptorSet(traceDescriptorSet, traceDescriptorWrites);
+
+	vkcv::PipelineHandle tracePipeline = core.createComputePipeline(
+		traceShaderProgram, 
+		{ core.getDescriptorSetLayout(traceDescriptorSetLayout).vulkanHandle });
+
+	if (!tracePipeline)
+	{
+		vkcv_log(vkcv::LogLevel::ERROR, "Could not create graphics pipeline. Exiting.");
+		return EXIT_FAILURE;
+	}
+
+	vkcv::camera::CameraManager cameraManager(window);
+	uint32_t camIndex0 = cameraManager.addCamera(vkcv::camera::ControllerType::PILOT);
+
+	cameraManager.getCamera(camIndex0).setPosition(glm::vec3(0, 0, -2));
+
+	auto    startTime       = std::chrono::system_clock::now();
+	float   time            = 0;
+	int     frameIndex      = 0;
+	bool    clearMeanImage  = true;
+	bool    updateMaterials = true;
+
+	float cameraPitchPrevious   = 0;
+	float cameraYawPrevious     = 0;
+	glm::vec3 cameraPositionPrevious = glm::vec3(0);
+
+	uint32_t widthPrevious  = initialWidth;
+	uint32_t heightPrevious = initialHeight;
+
+	vkcv::gui::GUI gui(core, window);
+
+	bool renderUI = true;
+	window.e_key.add([&renderUI](int key, int scancode, int action, int mods) {
+		if (key == GLFW_KEY_I && action == GLFW_PRESS) {
+			renderUI = !renderUI;
+		}
+	});
+
+	glm::vec3   skyColor            = glm::vec3(0.2, 0.7, 0.8);
+	float       skyColorMultiplier  = 1;
+
+	while (window.isWindowOpen())
+	{
+		vkcv::Window::pollEvents();
+
+		uint32_t swapchainWidth, swapchainHeight; // No resizing = No problem
+		if (!core.beginFrame(swapchainWidth, swapchainHeight)) {
+			continue;
+		}
+
+		if (swapchainWidth != widthPrevious || swapchainHeight != heightPrevious) {
+
+			// resize images
+			outputImage = core.createImage(
+				vk::Format::eR32G32B32A32Sfloat,
+				swapchainWidth,
+				swapchainHeight,
+				1,
+				false,
+				true).getHandle();
+
+			meanImage = core.createImage(
+				vk::Format::eR32G32B32A32Sfloat,
+				swapchainWidth,
+				swapchainHeight,
+				1,
+				false,
+				true).getHandle();
+
+			// update descriptor sets
+			traceDescriptorWrites.storageImageWrites = {
+			vkcv::StorageImageDescriptorWrite(3, outputImage) };
+			core.writeDescriptorSet(traceDescriptorSet, traceDescriptorWrites);
+
+			vkcv::DescriptorWrites imageCombineDescriptorWrites;
+			imageCombineDescriptorWrites.storageImageWrites = {
+				vkcv::StorageImageDescriptorWrite(0, outputImage),
+				vkcv::StorageImageDescriptorWrite(1, meanImage)
+			};
+			core.writeDescriptorSet(imageCombineDescriptorSet, imageCombineDescriptorWrites);
+
+			vkcv::DescriptorWrites imageClearDescriptorWrites;
+			imageClearDescriptorWrites.storageImageWrites = {
+				vkcv::StorageImageDescriptorWrite(0, meanImage)
+			};
+			core.writeDescriptorSet(imageClearDescriptorSet, imageClearDescriptorWrites);
+
+			widthPrevious  = swapchainWidth;
+			heightPrevious = swapchainHeight;
+
+			clearMeanImage = true;
+		}
+
+		auto end = std::chrono::system_clock::now();
+		auto deltatime = std::chrono::duration_cast<std::chrono::microseconds>(end - startTime);
+		startTime = end;
+
+		time += 0.000001f * static_cast<float>(deltatime.count());
+
+		cameraManager.update(0.000001 * static_cast<double>(deltatime.count()));
+
+		const vkcv::CommandStreamHandle cmdStream = core.createCommandStream(vkcv::QueueType::Graphics);
+
+		uint32_t fullscreenDispatchCount[3] = {
+			static_cast<uint32_t> (std::ceil(swapchainWidth  / 8.f)),
+			static_cast<uint32_t> (std::ceil(swapchainHeight / 8.f)),
+			1 };
+
+		if (updateMaterials) {
+			std::vector<Material> materials;
+			for (const auto& settings : materialSettings) {
+				materials.push_back(settings.second);
+			}
+			materialBuffer.fill(materials);
+			updateMaterials = false;
+			clearMeanImage  = true;
+		}
+
+		float cameraPitch;
+		float cameraYaw;
+		cameraManager.getActiveCamera().getAngles(cameraPitch, cameraYaw);
+
+		if (glm::abs(cameraPitch - cameraPitchPrevious) > 0.01 || glm::abs(cameraYaw - cameraYawPrevious) > 0.01)
+			clearMeanImage = true;	// camera rotated
+
+		cameraPitchPrevious = cameraPitch;
+		cameraYawPrevious   = cameraYaw;
+
+		glm::vec3 cameraPosition = cameraManager.getActiveCamera().getPosition();
+
+		if(glm::distance(cameraPosition, cameraPositionPrevious) > 0.0001)
+			clearMeanImage = true;	// camera moved
+
+		cameraPositionPrevious = cameraPosition;
+
+		if (clearMeanImage) {
+			core.prepareImageForStorage(cmdStream, meanImage);
+
+			core.recordComputeDispatchToCmdStream(cmdStream,
+				imageClearPipeline,
+				fullscreenDispatchCount,
+				{ vkcv::DescriptorSetUsage(0, core.getDescriptorSet(imageClearDescriptorSet).vulkanHandle) },
+				vkcv::PushConstants(0));
+
+			clearMeanImage = false;
+		}
+
+		// path tracing
+		struct RaytracingPushConstantData {
+			glm::mat4   viewToWorld;
+			glm::vec3   skyColor;
+			int32_t     sphereCount;
+			int32_t     planeCount;
+			int32_t     frameIndex;
+		};
+
+		RaytracingPushConstantData raytracingPushData;
+		raytracingPushData.viewToWorld  = glm::inverse(cameraManager.getActiveCamera().getView());
+		raytracingPushData.skyColor     = skyColor * skyColorMultiplier;
+		raytracingPushData.sphereCount  = spheres.size();
+		raytracingPushData.planeCount   = planes.size();
+		raytracingPushData.frameIndex   = frameIndex;
+
+		vkcv::PushConstants pushConstantsCompute(sizeof(RaytracingPushConstantData));
+		pushConstantsCompute.appendDrawcall(raytracingPushData);
+
+		uint32_t traceDispatchCount[3] = { 
+			static_cast<uint32_t> (std::ceil(swapchainWidth  / 16.f)),
+			static_cast<uint32_t> (std::ceil(swapchainHeight / 16.f)),
+			1 };
+
+		core.prepareImageForStorage(cmdStream, outputImage);
+
+		core.recordComputeDispatchToCmdStream(cmdStream,
+			tracePipeline,
+			traceDispatchCount,
+			{ vkcv::DescriptorSetUsage(0,core.getDescriptorSet(traceDescriptorSet).vulkanHandle) },
+			pushConstantsCompute);
+
+		core.prepareImageForStorage(cmdStream, meanImage);
+		core.recordImageMemoryBarrier(cmdStream, outputImage);
+
+		// combine images
+		core.recordComputeDispatchToCmdStream(cmdStream,
+			imageCombinePipeline,
+			fullscreenDispatchCount,
+			{ vkcv::DescriptorSetUsage(0,core.getDescriptorSet(imageCombineDescriptorSet).vulkanHandle) },
+			vkcv::PushConstants(0));
+
+		core.recordImageMemoryBarrier(cmdStream, meanImage);
+
+		// present image
+		const vkcv::ImageHandle swapchainInput = vkcv::ImageHandle::createSwapchainImageHandle();
+
+		vkcv::DescriptorWrites presentDescriptorWrites;
+		presentDescriptorWrites.storageImageWrites = {
+			vkcv::StorageImageDescriptorWrite(0, meanImage),
+			vkcv::StorageImageDescriptorWrite(1, swapchainInput) };
+		core.writeDescriptorSet(presentDescriptorSet, presentDescriptorWrites);
+
+		core.prepareImageForStorage(cmdStream, swapchainInput);
+
+		core.recordComputeDispatchToCmdStream(cmdStream,
+			presentPipeline,
+			fullscreenDispatchCount,
+			{ vkcv::DescriptorSetUsage(0,core.getDescriptorSet(presentDescriptorSet).vulkanHandle) },
+			vkcv::PushConstants(0));
+
+		core.prepareSwapchainImageForPresent(cmdStream);
+		core.submitCommandStream(cmdStream);
+
+		if (renderUI) {
+			gui.beginGUI();
+
+			ImGui::Begin("Settings");
+
+			clearMeanImage |= ImGui::ColorEdit3("Sky color", &skyColor.x);
+			clearMeanImage |= ImGui::InputFloat("Sky color multiplier", &skyColorMultiplier);
+
+			if (ImGui::CollapsingHeader("Materials")) {
+
+				for (auto& setting : materialSettings) {
+					if (ImGui::CollapsingHeader(setting.first.c_str())) {
+
+						const glm::vec3 emission            = setting.second.emission;
+						float           emissionStrength    = glm::max(glm::max(glm::max(emission.x, emission.y), emission.z), 1.f);
+						glm::vec3       emissionColor       = emission / emissionStrength;
+
+						updateMaterials |= ImGui::ColorEdit3((std::string("Emission color ")    + setting.first).c_str(), &emissionColor.x);
+						updateMaterials |= ImGui::InputFloat((std::string("Emission strength ") + setting.first).c_str(), &emissionStrength);
+
+						setting.second.emission = emissionStrength * emissionColor;
+
+						updateMaterials |= ImGui::ColorEdit3((std::string("Albedo color ")  + setting.first).c_str(), &setting.second.albedo.x);
+						updateMaterials |= ImGui::ColorEdit3((std::string("F0 ")            + setting.first).c_str(), &setting.second.f0.x);
+						updateMaterials |= ImGui::DragFloat(( std::string("ks ")            + setting.first).c_str(), &setting.second.ks, 0.01, 0, 1);
+						updateMaterials |= ImGui::DragFloat(( std::string("roughness ")     + setting.first).c_str(), &setting.second.roughness, 0.01, 0, 1);
+
+					}
+				}
+			}
+
+			ImGui::End();
+
+			gui.endGUI();
+		}
+
+		core.endFrame();
+
+		frameIndex++;
+	}
+	return 0;
+}
diff --git a/projects/saf_r/.gitignore b/projects/saf_r/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..ff3dff30031efafa24269a9ac0ef93f64f63ded1
--- /dev/null
+++ b/projects/saf_r/.gitignore
@@ -0,0 +1 @@
+saf_r
\ No newline at end of file
diff --git a/projects/saf_r/CMakeLists.txt b/projects/saf_r/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..61ede8ae5a5cedac78ff5781aec20973854a3df7
--- /dev/null
+++ b/projects/saf_r/CMakeLists.txt
@@ -0,0 +1,31 @@
+cmake_minimum_required(VERSION 3.16)
+project(saf_r)
+
+# setting c++ standard for the project
+set(CMAKE_CXX_STANDARD 17)
+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(saf_r
+		src/main.cpp 
+		"src/safrScene.hpp" 
+		)
+
+# this should fix the execution path to load local files from the project (for MSVC)
+if(MSVC)
+	set_target_properties(saf_r PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+	set_target_properties(saf_r PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+
+	# in addition to setting the output directory, the working directory has to be set
+	# by default visual studio sets the working directory to the build directory, when using the debugger
+	set_target_properties(saf_r PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+endif()
+
+# including headers of dependencies and the VkCV framework
+target_include_directories(saf_r SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_testing_include} ${vkcv_asset_loader_include} ${vkcv_camera_include} ${vkcv_shader_compiler_include})
+
+# linking with libraries from all dependencies and the VkCV framework
+target_link_libraries(saf_r vkcv vkcv_testing vkcv_asset_loader ${vkcv_asset_loader_libraries} vkcv_camera vkcv_shader_compiler)
diff --git a/projects/saf_r/shaders/raytracing.comp b/projects/saf_r/shaders/raytracing.comp
new file mode 100644
index 0000000000000000000000000000000000000000..a7c6b92a646e5c2f753946f74fe7ab78aea44fe6
--- /dev/null
+++ b/projects/saf_r/shaders/raytracing.comp
@@ -0,0 +1,292 @@
+#version 450 core
+#extension GL_ARB_separate_shader_objects : enable
+
+// defines constants
+const float pi      = 3.1415926535897932384626433832795;
+const float hitBias = 0.01;   // used to offset hits to avoid self intersection
+
+layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in;
+
+//structs of materials, lights, spheres and intersection for use in compute shader
+struct Material {
+    vec3 albedo;
+    vec3 diffuseColor;
+    float specularExponent;
+    float refractiveIndex;
+};
+
+struct Light{
+    vec3 position;
+    float intensity;
+};
+
+struct Sphere{
+    vec3 center;
+    float radius;
+    Material material;
+};
+
+struct Intersection{
+    bool hit;
+    vec3 pos;
+    vec3 N;
+    Material material;
+};
+
+
+//incoming light data
+layout(std430, binding = 0) coherent buffer lights{
+    Light inLights[];
+};
+
+// incoming sphere data
+layout(std430, binding = 1) coherent buffer spheres{
+    Sphere inSpheres[];
+};
+
+// output store image as swapchain input
+layout(set=0, binding = 2, rgba8) uniform image2D outImage;
+
+// incoming constants, because size of dynamic arrays cannot be computed on gpu
+layout( push_constant ) uniform constants{
+    mat4 viewToWorld;
+    int lightCount;
+    int sphereCount;
+};
+
+/*
+* safrReflect computes the new reflected or refracted ray depending on the material
+* @param vec3: raydirection vector
+* @param vec3: normalvector on which should be reflected or refracted
+* @param float: degree of refraction. In case of simple reflection it is 1.0
+* @return vec3: new ray that is the result of the reflection or refraction
+*/
+vec3 safrReflect(vec3 V, vec3 N, float refractIndex){
+    if(refractIndex != 1.0){
+        // Snell's law
+        float cosi = - max(-1.f, min(1.f, dot(V,N)));
+        float etai = 1;
+        float etat = refractIndex;
+        vec3 n = N;
+        float swap;
+        if(cosi < 0){
+            cosi = -cosi;
+            n = -N;
+            swap = etai;
+            etai = etat;
+            etat = swap;
+        }
+        float eta = etai / etat;
+        float k = 1 - eta * eta * (1 - cosi * cosi);
+        if(k < 0){
+            return vec3(0,0,0);
+        } else {
+            return V * eta + n * (eta * cosi - sqrt(k));
+        }
+    }else{
+        return reflect(V, N);
+    }
+}
+
+/*
+* the rayIntersect function checks, if a ray from the raytracer passes through the sphere, hits the sphere or passes by the the sphere
+* @param vec3: origin of ray
+* @param vec3: direction of ray
+* @param float: distance of the ray to the sphere (out because there are no references in shaders)
+* @return bool: if ray interesects sphere or not (out because there are no references in shaders)
+*/
+
+bool rayIntersect(const vec3 origin, const vec3 dir, out float t0, const int id){
+    vec3 L = inSpheres[id].center - origin;
+    float tca = dot(L, dir);
+    float d2 = dot(L, L) - tca * tca;
+    if (d2 > inSpheres[id].radius * inSpheres[id].radius){
+        return false;
+    }
+    float thc = float(sqrt(inSpheres[id].radius * inSpheres[id].radius - d2));
+    t0 = tca - thc;
+    float t1 = tca + thc;
+    if (t0 < 0) {
+        t0 = t1;
+    }
+    if (t0 < 0){
+        return false;
+    }
+    return true;
+}
+
+/*
+* sceneIntersect iterates over whole scene (over every single object) to check for intersections
+* @param vec3: Origin of the ray
+* @param vec3: direction of the ray
+* @return: Intersection struct with hit(bool) position, normal and material of sphere
+*/
+
+Intersection sceneIntersect(const vec3 rayOrigin, const vec3 rayDirection) {
+    //distance if spheres will be rendered
+    float   min_d    = 1.0 / 0.0;  // lets start with something big
+    
+    Intersection intersection;
+    intersection.hit = false;
+    
+    //go over every sphere, check if sphere is hit by ray, save if hit is near enough into intersection struct
+    for (int i = 0; i < sphereCount; i++) {
+        float d;
+        if (rayIntersect(rayOrigin, rayDirection, d, i)) {
+            
+            intersection.hit = true;
+            
+            if(d < min_d){
+                min_d = d;
+                intersection.pos        = rayOrigin + rayDirection * d;
+                intersection.N          = normalize(intersection.pos - inSpheres[i].center);
+                intersection.material   = inSpheres[i].material;
+            }
+        }
+    }
+
+    float checkerboard_dist = min_d;
+    if (abs(rayDirection.y)>1e-3)  {
+        float d = -(rayOrigin.y + 4) / rayDirection.y; // the checkerboard plane has equation y = -4
+        vec3 pt = rayOrigin + rayDirection * d;
+        if (d > 0 && abs(pt.x) < 10 && pt.z<-10 && pt.z>-30 && d < min_d) {
+            checkerboard_dist = d;
+            intersection.hit = true;
+            intersection.pos = pt;
+            intersection.N = vec3(0, 1, 0);
+            intersection.material = inSpheres[0].material;
+        }
+    }
+    return intersection;
+}
+
+/*
+* biasHitPosition computes the new hitposition with respect to the raydirection and a bias
+* @param vec3: Hit Position
+* @param vec3: direction of ray
+* @param vec3: N(ormal)
+* @return vec3: new Hit position depending on hitBias (used to offset hits to avoid self intersection)
+*/
+vec3 biasHitPosition(vec3 hitPos, vec3 rayDirection, vec3 N){
+    return hitPos + sign(dot(rayDirection, N)) * N * hitBias;
+}
+
+/*
+* computeHitLighting iterates over all lights to compute the color for every ray
+* @param Intersection: struct with all the data of the intersection
+* @param vec3: Raydirection
+* @param float: material albedo of the intersection
+* @return colour/shadows of sphere with illumination
+*/
+vec3 computeHitLighting(Intersection intersection, vec3 V, out float outReflectionThroughput){
+    
+    float lightIntensityDiffuse  = 0;
+    float lightIntensitySpecular = 0;
+
+    //iterate over every light source to compute sphere colours/shadows
+    for (int i = 0; i < lightCount; i++) {
+
+        //compute normal + distance between light and intersection
+        vec3   L = normalize(inLights[i].position - intersection.pos);
+        float  d = distance(inLights[i].position, intersection.pos);
+
+        //compute shadows
+        vec3 shadowOrigin = biasHitPosition(intersection.pos, L, intersection.N);
+        Intersection shadowIntersection = sceneIntersect(shadowOrigin, L);
+        bool isShadowed = false;
+        if(shadowIntersection.hit){
+            isShadowed = distance(shadowIntersection.pos, shadowOrigin) < d;
+        }
+        if(isShadowed){
+            continue;
+        }
+
+        lightIntensityDiffuse  += inLights[i].intensity * max(0.f, dot(L, intersection.N));
+        lightIntensitySpecular += pow(max(0.f, dot(safrReflect(V, intersection.N, intersection.material.refractiveIndex), L)), intersection.material.specularExponent) * inLights[i].intensity;
+    }
+
+    outReflectionThroughput = intersection.material.albedo[2];
+    return intersection.material.diffuseColor * lightIntensityDiffuse * intersection.material.albedo[0] + lightIntensitySpecular * intersection.material.albedo[1];
+}
+
+/*
+* castRay throws a ray out of the initial origin with respect to the initial direction checks for intersection and refelction
+* @param vec3: initial origin of ray
+* @param vec3: initial direction of ray
+* @param int: max depth o ray reflection
+* @return s
+*/
+
+vec3 castRay(const vec3 initialOrigin, const vec3 initialDirection, int max_depth) {
+    
+    vec3 skyColor = vec3(0.2, 0.7, 0.8);
+    vec3 rayOrigin    = initialOrigin;
+    vec3 rayDirection = initialDirection;
+    
+    float   reflectionThroughput    = 1;
+    vec3    color                   = vec3(0);
+
+    //iterate to max depth of reflections
+    for(int i = 0; i < max_depth; i++){
+
+        Intersection intersection = sceneIntersect(rayOrigin, rayDirection);
+
+        vec3 hitColor;
+        float hitReflectionThroughput;
+
+        if(intersection.hit){
+            hitColor = computeHitLighting(intersection, rayDirection, hitReflectionThroughput);
+        }else{
+            hitColor = skyColor;
+        }
+
+        color                   += hitColor * reflectionThroughput;
+        reflectionThroughput    *= hitReflectionThroughput;
+
+        //if there is no intersection of a ray with a sphere, break out of the loop
+        if(!intersection.hit){
+            break;
+        }
+
+        //compute new direction and origin of the reflected ray
+        rayDirection    = normalize(safrReflect(rayDirection, intersection.N, intersection.material.refractiveIndex));
+        rayOrigin       = biasHitPosition(intersection.pos, rayDirection, intersection.N);
+    }
+
+    return color;
+}
+
+/*
+* computeDirection transforms the pixel coords to worldspace coords
+* @param ivec2: pixel coordinates
+* @return vec3: world coordinates
+*/
+vec3 computeDirection(ivec2 coord){
+
+    ivec2 outImageRes   = imageSize(outImage);
+    float fovDegree     = 45;
+    float fov           = fovDegree * pi / 180;
+    
+    vec2 uv     = coord / vec2(outImageRes);
+    vec2 ndc    = 2 * uv - 1;
+    
+    float tanFovHalf    = tan(fov / 2.f);
+    float aspectRatio   = outImageRes.x / float(outImageRes.y);
+    float x             =  ndc.x * tanFovHalf * aspectRatio;
+    float y             = -ndc.y * tanFovHalf;
+    
+    vec3 directionViewSpace     = normalize(vec3(x, y, 1));
+    vec3 directionWorldSpace    = mat3(viewToWorld) * directionViewSpace;
+    return directionWorldSpace;
+}
+
+// the main function
+void main(){
+    ivec2 coord     = ivec2(gl_GlobalInvocationID.xy);
+    int max_depth   = 4;
+    vec3 direction  = computeDirection(coord);
+    vec3 cameraPos  = viewToWorld[3].xyz;
+    vec3 color      = castRay(cameraPos, direction, max_depth);
+    
+    imageStore(outImage, coord, vec4(color, 0.f));
+}
\ No newline at end of file
diff --git a/projects/saf_r/shaders/shader.frag b/projects/saf_r/shaders/shader.frag
new file mode 100644
index 0000000000000000000000000000000000000000..64b45eb26b9831f6504e69882018e46120615615
--- /dev/null
+++ b/projects/saf_r/shaders/shader.frag
@@ -0,0 +1,13 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(location = 0) in vec3 fragColor;
+layout(location = 1) in vec2 texCoord;
+
+layout(location = 0) out vec3 outColor;
+
+layout(set=0, binding=1) uniform sampler    textureSampler;
+
+void main() {
+	outColor = fragColor;
+}
\ No newline at end of file
diff --git a/projects/saf_r/shaders/shader.vert b/projects/saf_r/shaders/shader.vert
new file mode 100644
index 0000000000000000000000000000000000000000..b6419f5e348ddeca66d1c8b0eb0a4cf32e2e80c4
--- /dev/null
+++ b/projects/saf_r/shaders/shader.vert
@@ -0,0 +1,32 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(location = 0) out vec3 fragColor;
+layout(location = 1) out vec2 texCoord;
+
+layout( push_constant ) uniform constants{
+    mat4 mvp;
+    mat4 proj;
+};
+
+void main()	{
+    vec3 positions[3] = {
+        vec3(-1, -1, -1),
+        vec3( 3, -1, -1),
+        vec3(-1, 3, -1)
+    };
+    
+    vec3 colors[3] = {
+        vec3(1, 0, 0),
+        vec3(0, 1, 0),
+        vec3(0, 0, 1)
+    };
+
+    vec4 position = mvp * vec4(positions[gl_VertexIndex], 1.0);
+	gl_Position = position;
+
+    texCoord.x = ((proj * vec4(positions[gl_VertexIndex], 1.0)).x + 1.0) * 0.5;
+    texCoord.y = ((proj * vec4(positions[gl_VertexIndex], 1.0)).y + 1.0) * 0.5;
+
+	fragColor = colors[gl_VertexIndex];
+}
\ No newline at end of file
diff --git a/projects/saf_r/src/main.cpp b/projects/saf_r/src/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..3fef073a00f8263cc08ce17f033170d0f4031dc4
--- /dev/null
+++ b/projects/saf_r/src/main.cpp
@@ -0,0 +1,297 @@
+#include <iostream>
+#include <vkcv/Core.hpp>
+#include <GLFW/glfw3.h>
+#include <vkcv/camera/CameraManager.hpp>
+#include <vkcv/asset/asset_loader.hpp>
+#include <vkcv/shader/GLSLCompiler.hpp>
+#include <chrono>
+#include <limits>
+#include <cmath>
+#include <vector>
+#include <string.h>	// memcpy(3)
+#include "safrScene.hpp"
+
+
+void createQuadraticLightCluster(std::vector<safrScene::Light>& lights, int countPerDimension, float dimension, float height, float intensity) {
+    float distance = dimension/countPerDimension;
+
+    for(int x = 0; x <= countPerDimension; x++) {
+        for (int z = 0; z <= countPerDimension; z++) {
+            lights.push_back(safrScene::Light(glm::vec3(x * distance, height,  z * distance),
+                                              float (intensity/countPerDimension) / 10.f) // Divide by 10, because intensity is busting O.o
+                                              );
+        }
+    }
+
+}
+
+int main(int argc, const char** argv) {
+	const char* applicationName = "SAF_R";
+
+	//window creation
+	const int windowWidth = 800;
+	const int windowHeight = 600;
+
+	vkcv::Core core = vkcv::Core::create(
+		applicationName,
+		VK_MAKE_VERSION(0, 0, 1),
+		{ vk::QueueFlagBits::eTransfer,vk::QueueFlagBits::eGraphics, vk::QueueFlagBits::eCompute },
+		{ "VK_KHR_swapchain" }
+	);
+
+	vkcv::WindowHandle windowHandle = core.createWindow(applicationName, windowWidth, windowHeight, false);
+
+	//configuring the compute Shader
+	vkcv::PassConfig computePassDefinition({});
+	vkcv::PassHandle computePass = core.createPass(computePassDefinition);
+
+	if (!computePass)
+	{
+		std::cout << "Error. Could not create renderpass. Exiting." << std::endl;
+		return EXIT_FAILURE;
+	}
+
+	std::string shaderPathCompute = "shaders/raytracing.comp";
+
+	//creating the shader programs
+	vkcv::ShaderProgram safrShaderProgram;
+	vkcv::shader::GLSLCompiler compiler;
+	vkcv::ShaderProgram computeShaderProgram{};
+
+	compiler.compile(vkcv::ShaderStage::VERTEX, std::filesystem::path("shaders/shader.vert"),
+		[&safrShaderProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+			safrShaderProgram.addShader(shaderStage, path);
+	});
+
+	compiler.compile(vkcv::ShaderStage::FRAGMENT, std::filesystem::path("shaders/shader.frag"),
+		[&safrShaderProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+			safrShaderProgram.addShader(shaderStage, path);
+	});
+
+	compiler.compile(vkcv::ShaderStage::COMPUTE, shaderPathCompute, [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		computeShaderProgram.addShader(shaderStage, path);
+	});
+
+	//create DescriptorSets (...) for every Shader
+	const vkcv::DescriptorBindings& descriptorBindings = safrShaderProgram.getReflectedDescriptors().at(0);
+	vkcv::DescriptorSetLayoutHandle descriptorSetLayout = core.createDescriptorSetLayout(descriptorBindings);
+	vkcv::DescriptorSetHandle descriptorSet = core.createDescriptorSet(descriptorSetLayout);
+	vkcv::ImageHandle swapchainInput = vkcv::ImageHandle::createSwapchainImageHandle();
+
+	const vkcv::DescriptorBindings& computeDescriptorBindings = computeShaderProgram.getReflectedDescriptors().at(0);
+	
+	vkcv::DescriptorSetLayoutHandle computeDescriptorSetLayout = core.createDescriptorSetLayout(computeDescriptorBindings);
+	vkcv::DescriptorSetHandle computeDescriptorSet = core.createDescriptorSet(computeDescriptorSetLayout);
+
+	const std::vector<vkcv::VertexAttachment> computeVertexAttachments = computeShaderProgram.getVertexAttachments();
+
+	std::vector<vkcv::VertexBinding> computeBindings;
+	for (size_t i = 0; i < computeVertexAttachments.size(); i++) {
+		computeBindings.push_back(vkcv::VertexBinding(i, { computeVertexAttachments[i] }));
+	}
+	const vkcv::VertexLayout computeLayout(computeBindings);
+	
+	/*
+	* create the scene
+	*/
+
+	//materials for the spheres
+	std::vector<safrScene::Material> materials;
+	safrScene::Material ivory(glm::vec4(0.6, 0.3, 0.1, 0.0), glm::vec3(0.4, 0.4, 0.3), 50., 1.0);
+	safrScene::Material red_rubber(glm::vec4(0.9, 0.1, 0.0, 0.0), glm::vec3(0.3, 0.1, 0.1), 10., 1.0);
+	safrScene::Material mirror( glm::vec4(0.0, 10.0, 0.8, 0.0), glm::vec3(1.0, 1.0, 1.0), 1425., 1.0);
+    safrScene::Material glass( glm::vec4(0.0, 10.0, 0.8, 0.0), glm::vec3(1.0, 1.0, 1.0), 1425., 1.5);
+
+	materials.push_back(ivory);
+	materials.push_back(red_rubber);
+	materials.push_back(mirror);
+
+	//spheres for the scene
+	std::vector<safrScene::Sphere> spheres;
+	spheres.push_back(safrScene::Sphere(glm::vec3(-3,    0,   -16), 2, ivory));
+	// spheres.push_back(safrScene::Sphere(glm::vec3(-1.0, -1.5, 12), 2, mirror));
+	spheres.push_back(safrScene::Sphere(glm::vec3(-1.0, -1.5, -12), 2, glass));
+	spheres.push_back(safrScene::Sphere(glm::vec3(  1.5, -0.5, -18), 3, red_rubber));
+	spheres.push_back(safrScene::Sphere(glm::vec3( 7,    5,   -18), 4, mirror));
+
+	//lights for the scene
+	std::vector<safrScene::Light> lights;
+	/*
+	lights.push_back(safrScene::Light(glm::vec3(-20, 20,  20), 1.5));
+	lights.push_back(safrScene::Light(glm::vec3(30,  50, -25), 1.8));
+	lights.push_back(safrScene::Light(glm::vec3(30,  20,  30), 1.7));
+    */
+    createQuadraticLightCluster(lights, 10, 2.5f, 20, 1.5f);
+
+
+	vkcv::SamplerHandle sampler = core.createSampler(
+		vkcv::SamplerFilterType::LINEAR,
+		vkcv::SamplerFilterType::LINEAR,
+		vkcv::SamplerMipmapMode::LINEAR,
+		vkcv::SamplerAddressMode::REPEAT
+	);
+
+	
+	//create Buffer for compute shader
+	vkcv::Buffer<safrScene::Light> lightsBuffer = core.createBuffer<safrScene::Light>(
+		vkcv::BufferType::STORAGE,
+		lights.size()
+	);
+	lightsBuffer.fill(lights);
+
+	vkcv::Buffer<safrScene::Sphere> sphereBuffer = core.createBuffer<safrScene::Sphere>(
+		vkcv::BufferType::STORAGE,
+		spheres.size()
+	);
+	sphereBuffer.fill(spheres);
+
+	vkcv::DescriptorWrites computeWrites;
+	computeWrites.storageBufferWrites = { vkcv::BufferDescriptorWrite(0,lightsBuffer.getHandle()),
+                                          vkcv::BufferDescriptorWrite(1,sphereBuffer.getHandle())};
+    core.writeDescriptorSet(computeDescriptorSet, computeWrites);
+
+
+	const auto& context = core.getContext();
+
+	auto safrIndexBuffer = core.createBuffer<uint16_t>(vkcv::BufferType::INDEX, 3, vkcv::BufferMemoryType::DEVICE_LOCAL);
+	uint16_t indices[3] = { 0, 1, 2 };
+	safrIndexBuffer.fill(&indices[0], sizeof(indices));
+
+	// an example attachment for passes that output to the window
+	const vkcv::AttachmentDescription present_color_attachment(
+		vkcv::AttachmentOperation::STORE,
+		vkcv::AttachmentOperation::CLEAR,
+		core.getSwapchain(windowHandle).getFormat());
+
+	vkcv::PassConfig safrPassDefinition({ present_color_attachment });
+	vkcv::PassHandle safrPass = core.createPass(safrPassDefinition);
+
+	if (!safrPass)
+	{
+		std::cout << "Error. Could not create renderpass. Exiting." << std::endl;
+		return EXIT_FAILURE;
+	}
+
+	//create the render pipeline + compute pipeline
+	const vkcv::GraphicsPipelineConfig safrPipelineDefinition{
+			safrShaderProgram,
+			(uint32_t)windowWidth,
+			(uint32_t)windowHeight,
+			safrPass,
+			{},
+			{ core.getDescriptorSetLayout(descriptorSetLayout).vulkanHandle },
+			false
+	};
+
+	vkcv::GraphicsPipelineHandle safrPipeline = core.createGraphicsPipeline(safrPipelineDefinition);
+
+	const vkcv::ComputePipelineConfig computePipelineConfig{
+			computeShaderProgram,
+			{core.getDescriptorSetLayout(computeDescriptorSetLayout).vulkanHandle}
+	};
+
+	vkcv::ComputePipelineHandle computePipeline = core.createComputePipeline(computePipelineConfig);
+
+	if (!safrPipeline || !computePipeline)
+	{
+		std::cout << "Error. Could not create graphics pipeline. Exiting." << std::endl;
+		return EXIT_FAILURE;
+	}
+	
+	auto start = std::chrono::system_clock::now();
+
+	const vkcv::Mesh renderMesh({}, safrIndexBuffer.getVulkanHandle(), 3);
+	vkcv::DescriptorSetUsage descriptorUsage(0, core.getDescriptorSet(descriptorSet).vulkanHandle);
+	vkcv::DrawcallInfo drawcall(renderMesh, { descriptorUsage }, 1);
+
+	//create the camera
+	vkcv::camera::CameraManager cameraManager(core.getWindow(windowHandle));
+	uint32_t camIndex0 = cameraManager.addCamera(vkcv::camera::ControllerType::PILOT);
+	uint32_t camIndex1 = cameraManager.addCamera(vkcv::camera::ControllerType::TRACKBALL);
+
+	cameraManager.getCamera(camIndex0).setPosition(glm::vec3(0, 0, 2));
+	cameraManager.getCamera(camIndex1).setPosition(glm::vec3(0.0f, 0.0f, 0.0f));
+	cameraManager.getCamera(camIndex1).setCenter(glm::vec3(0.0f, 0.0f, -1.0f));
+
+	float time = 0;
+	
+	while (vkcv::Window::hasOpenWindow())
+	{
+		vkcv::Window::pollEvents();
+
+		uint32_t swapchainWidth, swapchainHeight; // No resizing = No problem
+		if (!core.beginFrame(swapchainWidth, swapchainHeight, windowHandle)) {
+			continue;
+		}
+
+		//configure timer
+		auto end = std::chrono::system_clock::now();
+		auto deltatime = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
+		start = end;
+		
+		time += 0.000001f * static_cast<float>(deltatime.count());
+		
+		//adjust light position
+		/*
+		639a53157e7d3936caf7c3e40379159cbcf4c89e
+		lights[0].position.x += std::cos(time * 3.0f) * 2.5f;
+		lights[1].position.z += std::cos(time * 2.5f) * 3.0f;
+		lights[2].position.y += std::cos(time * 1.5f) * 4.0f;
+		lightsBuffer.fill(lights);
+		*/
+
+		spheres[0].center.y += std::cos(time * 0.5f * 3.141f) * 0.25f;
+		spheres[1].center.x += std::cos(time * 2.f) * 0.25f;
+		spheres[1].center.z += std::cos(time * 2.f + 0.5f * 3.141f) * 0.25f;
+        sphereBuffer.fill(spheres);
+
+		//update camera
+		cameraManager.update(0.000001 * static_cast<double>(deltatime.count()));
+		glm::mat4 mvp = cameraManager.getActiveCamera().getMVP();
+		glm::mat4 proj = cameraManager.getActiveCamera().getProjection();
+
+		//create pushconstants for render
+		vkcv::PushConstants pushConstants(sizeof(glm::mat4) * 2);
+		pushConstants.appendDrawcall(std::array<glm::mat4, 2>{ mvp, proj });
+
+		auto cmdStream = core.createCommandStream(vkcv::QueueType::Graphics);
+
+		//configure the outImage for compute shader (render into the swapchain image)
+        computeWrites.storageImageWrites = { vkcv::StorageImageDescriptorWrite(2, swapchainInput)};
+        core.writeDescriptorSet(computeDescriptorSet, computeWrites);
+        core.prepareImageForStorage (cmdStream, swapchainInput);
+
+		//fill pushconstants for compute shader
+        struct RaytracingPushConstantData {
+            glm::mat4 viewToWorld;
+            int32_t lightCount;
+            int32_t sphereCount;
+        };
+
+        RaytracingPushConstantData raytracingPushData;
+        raytracingPushData.lightCount   = lights.size();
+        raytracingPushData.sphereCount  = spheres.size();
+        raytracingPushData.viewToWorld  = glm::inverse(cameraManager.getActiveCamera().getView());
+
+        vkcv::PushConstants pushConstantsCompute(sizeof(RaytracingPushConstantData));
+        pushConstantsCompute.appendDrawcall(raytracingPushData);
+
+		//dispatch compute shader
+		uint32_t computeDispatchCount[3] = {static_cast<uint32_t> (std::ceil( swapchainWidth/16.f)),
+                                            static_cast<uint32_t> (std::ceil(swapchainHeight/16.f)),
+                                            1 }; // Anzahl workgroups
+		core.recordComputeDispatchToCmdStream(cmdStream,
+			computePipeline,
+			computeDispatchCount,
+			{ vkcv::DescriptorSetUsage(0,core.getDescriptorSet(computeDescriptorSet).vulkanHandle) },
+			pushConstantsCompute);
+
+		core.recordBufferMemoryBarrier(cmdStream, lightsBuffer.getHandle());
+
+		core.prepareSwapchainImageForPresent(cmdStream);
+		core.submitCommandStream(cmdStream);
+
+		core.endFrame(windowHandle);
+	}
+	return 0;
+}
diff --git a/projects/saf_r/src/safrScene.hpp b/projects/saf_r/src/safrScene.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..33a298f82121971021d1912e6c1205e9c48a49f0
--- /dev/null
+++ b/projects/saf_r/src/safrScene.hpp
@@ -0,0 +1,51 @@
+#include <iostream>
+#include <vkcv/Core.hpp>
+#include <GLFW/glfw3.h>
+#include <vkcv/camera/CameraManager.hpp>
+#include <vkcv/asset/asset_loader.hpp>
+#include <vkcv/shader/GLSLCompiler.hpp>
+#include <chrono>
+#include <limits>
+#include <cmath>
+#include <vector>
+#include <string.h>	// memcpy(3)
+
+class safrScene {
+
+public:
+
+	/*
+	* Light struct with a position and intensity of the light source
+	*/
+	struct Light {
+		Light(const glm::vec3& p, const float& i) : position(p), intensity(i) {}
+		glm::vec3 position;
+		float intensity;
+	};
+
+	/*
+	* Material struct with defuse color, albedo and specular component
+	*/
+	struct Material {
+		Material(const glm::vec4& a, const glm::vec3& color, const float& spec, const float& r) : albedo(a), diffuse_color(color), specular_exponent(spec), refractive_index(r) {}
+		Material() : refractive_index(1), albedo(1, 0, 0, 0), diffuse_color(), specular_exponent() {}
+        glm::vec4 albedo;
+        alignas(16) glm::vec3 diffuse_color;
+        float specular_exponent;
+		float refractive_index;
+	};
+
+	/*
+	* the sphere is defined by it's center, the radius and the material
+	*/
+	struct Sphere {
+		glm::vec3 center;
+		float radius;
+		Material material;
+
+		Sphere(const glm::vec3& c, const float& r, const Material& m) : center(c), radius(r), material(m) {}
+
+	};
+
+	
+};
\ No newline at end of file
diff --git a/projects/sph/.gitignore b/projects/sph/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..4964f89e973f38358aa57f564f56d3d4b0c328a9
--- /dev/null
+++ b/projects/sph/.gitignore
@@ -0,0 +1 @@
+particle_simulation
\ No newline at end of file
diff --git a/projects/sph/CMakeLists.txt b/projects/sph/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..592aa4409ae3e01f4054f430bab7c424d25219d0
--- /dev/null
+++ b/projects/sph/CMakeLists.txt
@@ -0,0 +1,35 @@
+cmake_minimum_required(VERSION 3.16)
+project(sph)
+
+# setting c++ standard for the project
+set(CMAKE_CXX_STANDARD 17)
+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(sph
+		src/main.cpp
+		src/Particle.hpp 
+		src/Particle.cpp
+		src/BloomAndFlares.hpp
+		src/BloomAndFlares.cpp
+		src/PipelineInit.hpp
+		src/PipelineInit.cpp)
+
+# this should fix the execution path to load local files from the project (for MSVC)
+if(MSVC)
+	set_target_properties(sph PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+	set_target_properties(sph PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+
+	# in addition to setting the output directory, the working directory has to be set
+	# by default visual studio sets the working directory to the build directory, when using the debugger
+	set_target_properties(sph PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+endif()
+
+# including headers of dependencies and the VkCV framework
+target_include_directories(sph SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_testing_include} ${vkcv_camera_include} ${vkcv_shader_compiler_include})
+
+# linking with libraries from all dependencies and the VkCV framework
+target_link_libraries(sph vkcv vkcv_testing vkcv_camera vkcv_shader_compiler)
diff --git a/projects/sph/shaders/bloom/composite.comp b/projects/sph/shaders/bloom/composite.comp
new file mode 100644
index 0000000000000000000000000000000000000000..87b5ddb975106232d1cd3b6e5b8dc7e623dd0b59
--- /dev/null
+++ b/projects/sph/shaders/bloom/composite.comp
@@ -0,0 +1,38 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(set=0, binding=0) uniform texture2D                          blurImage;
+layout(set=0, binding=1) uniform texture2D                          lensImage;
+layout(set=0, binding=2) uniform sampler                            linearSampler;
+layout(set=0, binding=3, r11f_g11f_b10f) uniform image2D            colorBuffer;
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+
+void main()
+{
+    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(colorBuffer)))){
+        return;
+    }
+
+    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
+    vec2  pixel_size    = vec2(1.0f) / textureSize(sampler2D(blurImage, linearSampler), 0);
+    vec2  UV            = pixel_coord.xy * pixel_size;
+
+    vec4 composite_color = vec4(0.0f);
+
+    vec3 blur_color   = texture(sampler2D(blurImage, linearSampler), UV).rgb;
+    vec3 lens_color   = texture(sampler2D(lensImage, linearSampler), UV).rgb;
+    vec3 main_color   = imageLoad(colorBuffer, pixel_coord).rgb;
+
+    // composite blur and lens features
+    float bloom_weight = 0.01f;
+    float lens_weight  = 0.f;
+    float main_weight = 1 - (bloom_weight + lens_weight);
+
+    composite_color.rgb = blur_color * bloom_weight +
+                          lens_color * lens_weight  +
+                          main_color * main_weight;
+
+    imageStore(colorBuffer, pixel_coord, composite_color);
+}
\ No newline at end of file
diff --git a/projects/sph/shaders/bloom/downsample.comp b/projects/sph/shaders/bloom/downsample.comp
new file mode 100644
index 0000000000000000000000000000000000000000..2ab00c7c92798769153634f3479c5b7f3fb61d94
--- /dev/null
+++ b/projects/sph/shaders/bloom/downsample.comp
@@ -0,0 +1,76 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(set=0, binding=0) uniform texture2D                          inBlurImage;
+layout(set=0, binding=1) uniform sampler                            inImageSampler;
+layout(set=0, binding=2, r11f_g11f_b10f) uniform writeonly image2D  outBlurImage;
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+
+void main()
+{
+    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(outBlurImage)))){
+        return;
+    }
+
+    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
+    vec2  pixel_size    = vec2(1.0f) / imageSize(outBlurImage);
+    vec2  UV            = pixel_coord.xy * pixel_size;
+    vec2  UV_offset     = UV + 0.5f * pixel_size;
+
+    vec2 color_fetches[13] = {
+        // center neighbourhood (RED)
+        vec2(-1,  1), // LT
+        vec2(-1, -1), // LB
+        vec2( 1, -1), // RB
+        vec2( 1,  1), // RT
+
+        vec2(-2, 2), // LT
+        vec2( 0, 2), // CT
+        vec2( 2, 2), // RT
+
+        vec2(0 ,-2), // LC
+        vec2(0 , 0), // CC
+        vec2(2,  0), // CR
+
+        vec2(-2, -2), // LB
+        vec2(0 , -2), // CB
+        vec2(2 , -2)  // RB
+    };
+
+    float color_weights[13] = {
+        // 0.5f
+        1.f/8.f,
+        1.f/8.f,
+        1.f/8.f,
+        1.f/8.f,
+
+        // 0.125f
+        1.f/32.f,
+        1.f/16.f,
+        1.f/32.f,
+
+        // 0.25f
+        1.f/16.f,
+        1.f/8.f,
+        1.f/16.f,
+
+        // 0.125f
+        1.f/32.f,
+        1.f/16.f,
+        1.f/32.f
+    };
+
+    vec3 sampled_color = vec3(0.0f);
+
+    for(uint i = 0; i < 13; i++)
+    {
+        vec2 color_fetch = UV_offset + color_fetches[i] * pixel_size;
+        vec3 color = texture(sampler2D(inBlurImage, inImageSampler), color_fetch).rgb;
+        color *= color_weights[i];
+        sampled_color += color;
+    }
+
+    imageStore(outBlurImage, pixel_coord, vec4(sampled_color, 1.f));
+}
\ No newline at end of file
diff --git a/projects/sph/shaders/bloom/lensFlares.comp b/projects/sph/shaders/bloom/lensFlares.comp
new file mode 100644
index 0000000000000000000000000000000000000000..ce27d8850b709f61332d467914ddc944dc63109f
--- /dev/null
+++ b/projects/sph/shaders/bloom/lensFlares.comp
@@ -0,0 +1,109 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(set=0, binding=0) uniform texture2D                          blurBuffer;
+layout(set=0, binding=1) uniform sampler                            linearSampler;
+layout(set=0, binding=2, r11f_g11f_b10f) uniform image2D            lensBuffer;
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+vec3 sampleColorChromaticAberration(vec2 _uv)
+{
+    vec2 toCenter = (vec2(0.5) - _uv);
+
+    vec3    colorScales     = vec3(-1, 0, 1);
+    float   aberrationScale = 0.1;
+    vec3 scaleFactors = colorScales * aberrationScale;
+
+    float r = texture(sampler2D(blurBuffer, linearSampler), _uv + toCenter * scaleFactors.r).r;
+    float g = texture(sampler2D(blurBuffer, linearSampler), _uv + toCenter * scaleFactors.g).g;
+    float b = texture(sampler2D(blurBuffer, linearSampler), _uv + toCenter * scaleFactors.b).b;
+    return vec3(r, g, b);
+}
+
+// _uv assumed to be flipped UV coordinates!
+vec3 ghost_vectors(vec2 _uv)
+{
+    vec2 ghost_vec = (vec2(0.5f) - _uv);
+
+    const uint c_ghost_count = 64;
+    const float c_ghost_spacing = length(ghost_vec) / c_ghost_count;
+
+    ghost_vec *= c_ghost_spacing;
+
+    vec3 ret_color = vec3(0.0f);
+
+    for (uint i = 0; i < c_ghost_count; ++i)
+    {
+        // sample scene color
+        vec2 s_uv = fract(_uv + ghost_vec * vec2(i));
+        vec3 s = sampleColorChromaticAberration(s_uv);
+
+        // tint/weight
+        float d = distance(s_uv, vec2(0.5));
+        float weight = 1.0f - smoothstep(0.0f, 0.75f, d);
+        s *= weight;
+
+        ret_color += s;
+    }
+
+    ret_color /= c_ghost_count;
+    return ret_color;
+}
+
+vec3 halo(vec2 _uv)
+{
+    const float c_aspect_ratio = float(imageSize(lensBuffer).x) / float(imageSize(lensBuffer).y);
+    const float c_radius = 0.6f;
+    const float c_halo_thickness = 0.1f;
+
+    vec2 halo_vec = vec2(0.5) - _uv;
+    //halo_vec.x /= c_aspect_ratio;
+    halo_vec = normalize(halo_vec);
+    //halo_vec.x *= c_aspect_ratio;
+
+
+    //vec2 w_uv = (_uv - vec2(0.5, 0.0)) * vec2(c_aspect_ratio, 1.0) + vec2(0.5, 0.0);
+    vec2 w_uv = _uv;
+    float d = distance(w_uv, vec2(0.5)); // distance to center
+
+    float distance_to_halo = abs(d - c_radius);
+
+    float halo_weight = 0.0f;
+    if(abs(d - c_radius) <= c_halo_thickness)
+    {
+        float distance_to_border = c_halo_thickness - distance_to_halo;
+        halo_weight = distance_to_border / c_halo_thickness;
+
+        //halo_weight = clamp((halo_weight / 0.4f), 0.0f, 1.0f);
+        halo_weight = pow(halo_weight, 2.0f);
+
+
+        //halo_weight = 1.0f;
+    }
+
+    return sampleColorChromaticAberration(_uv + halo_vec) * halo_weight;
+}
+
+
+
+void main()
+{
+    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(lensBuffer)))){
+        return;
+    }
+
+    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
+    vec2  pixel_size    = vec2(1.0f) / imageSize(lensBuffer);
+    vec2  UV            = pixel_coord.xy * pixel_size;
+
+    vec2 flipped_UV = vec2(1.0f) - UV;
+
+    vec3 color = vec3(0.0f);
+
+    color += ghost_vectors(flipped_UV);
+    color += halo(UV);
+    color  *= 0.5f;
+
+    imageStore(lensBuffer, pixel_coord, vec4(color, 0.0f));
+}
\ No newline at end of file
diff --git a/projects/sph/shaders/bloom/upsample.comp b/projects/sph/shaders/bloom/upsample.comp
new file mode 100644
index 0000000000000000000000000000000000000000..0ddeedb5b5af9e476dc19012fed6430544006c0e
--- /dev/null
+++ b/projects/sph/shaders/bloom/upsample.comp
@@ -0,0 +1,45 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(set=0, binding=0) uniform texture2D                          inUpsampleImage;
+layout(set=0, binding=1) uniform sampler                            inImageSampler;
+layout(set=0, binding=2, r11f_g11f_b10f) uniform image2D  outUpsampleImage;
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+void main()
+{
+    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(outUpsampleImage)))){
+        return;
+    }
+
+
+    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
+    vec2  pixel_size    = vec2(1.0f) / imageSize(outUpsampleImage);
+    vec2  UV            = pixel_coord.xy * pixel_size;
+
+    const float gauss_kernel[3] = {1.f, 2.f, 1.f};
+    const float gauss_weight = 16.f;
+
+    vec3 sampled_color = vec3(0.f);
+
+    for(int i = -1; i <= 1; i++)
+    {
+        for(int j = -1; j <= 1; j++)
+        {
+            vec2 sample_location = UV + vec2(j, i) * pixel_size;
+            vec3 color = texture(sampler2D(inUpsampleImage, inImageSampler), sample_location).rgb;
+            color *= gauss_kernel[j+1];
+            color *= gauss_kernel[i+1];
+            color /= gauss_weight;
+
+            sampled_color += color;
+        }
+    }
+
+    //vec3 prev_color = imageLoad(outUpsampleImage, pixel_coord).rgb;
+    //float bloomRimStrength = 0.75f; // adjust this to change strength of bloom
+    //sampled_color = mix(prev_color, sampled_color, bloomRimStrength);
+
+    imageStore(outUpsampleImage, pixel_coord, vec4(sampled_color, 1.f));
+}
\ No newline at end of file
diff --git a/projects/sph/shaders/flip.comp b/projects/sph/shaders/flip.comp
new file mode 100644
index 0000000000000000000000000000000000000000..f5cd7bf3ea5ed8d04de970c816989230421c0b52
--- /dev/null
+++ b/projects/sph/shaders/flip.comp
@@ -0,0 +1,53 @@
+#version 450 core
+#extension GL_ARB_separate_shader_objects : enable
+#extension GL_GOOGLE_include_directive : enable
+
+layout(local_size_x = 256) in;
+
+struct Particle
+{
+    vec3 position;
+    float padding;
+    vec3 velocity;
+    float density;
+    vec3 force;
+    float pressure;
+};
+
+layout(std430, binding = 1) readonly buffer buffer_inParticle
+{
+    Particle inParticle[];
+};
+
+layout(std430, binding = 0) writeonly buffer buffer_outParticle
+{
+    Particle outParticle[];
+};
+
+layout( push_constant ) uniform constants{
+    float h;
+    float mass;
+    float gasConstant;
+    float offset;
+    float gravity;
+    float viscosity;
+    float ABSORBTION;
+    float dt;
+    vec3 gravityDir;
+    float particleCount;
+};
+
+void main() {
+    uint id = gl_GlobalInvocationID.x;
+
+    if(id >= int(particleCount))
+    {
+        return;
+    }
+    
+    outParticle[id].force = inParticle[id].force;
+    outParticle[id].density = inParticle[id].density;
+    outParticle[id].pressure = inParticle[id].pressure;
+    outParticle[id].position =  inParticle[id].position;
+    outParticle[id].velocity =  inParticle[id].velocity;
+}
diff --git a/projects/sph/shaders/force.comp b/projects/sph/shaders/force.comp
new file mode 100644
index 0000000000000000000000000000000000000000..ea9b378b48a23fd0208ab18d884dbccda5ab21f4
--- /dev/null
+++ b/projects/sph/shaders/force.comp
@@ -0,0 +1,95 @@
+#version 450 core
+#extension GL_ARB_separate_shader_objects : enable
+#extension GL_GOOGLE_include_directive : enable
+
+const float PI = 3.1415926535897932384626433832795;
+
+layout(local_size_x = 256) in;
+
+struct Particle
+{
+    vec3 position;
+    float padding;
+    vec3 velocity;
+    float density;
+    vec3 force;
+    float pressure;
+    
+};
+
+layout(std430, binding = 1) readonly buffer buffer_inParticle
+{
+    Particle inParticle[];
+};
+
+layout(std430, binding = 0) writeonly buffer buffer_outParticle
+{
+    Particle outParticle[];
+};
+
+layout( push_constant ) uniform constants{
+    float h;
+    float mass;
+    float gasConstant;
+    float offset;
+    float gravity;
+    float viscosity;
+    float ABSORBTION;
+    float dt;
+    vec3 gravityDir;
+    float particleCount;
+};
+
+float spiky(float r)    
+{
+    return (15.f / (PI * pow(h, 6)) * pow((h-r), 3)) * int(0<=r && r<=h);
+}
+
+float grad_spiky(float r)
+{
+    return -45.f / (PI * pow(h, 6)) * pow((h-r), 2) * int(0<=r && r<=h);
+}
+
+
+
+float laplacian(float r)
+{
+    return (45.f / (PI * pow(h,6)) * (h - r)) * int(0<=r && r<=h);
+}
+
+vec3 pressureForce = vec3(0, 0, 0);
+vec3 viscosityForce = vec3(0, 0, 0); 
+vec3 externalForce = vec3(0, 0, 0);
+
+void main() {
+
+    uint id = gl_GlobalInvocationID.x;
+
+    if(id >= int(particleCount))
+    {
+        return;
+    }
+
+    externalForce = inParticle[id].density * gravity * vec3(-gravityDir.x,gravityDir.y,gravityDir.z);
+
+    for(uint i = 0; i < int(particleCount); i++)  
+    {
+        if (id != i)
+        {
+            vec3 dir = inParticle[id].position - inParticle[i].position;
+            float dist = length(dir);
+            if(dist != 0) 
+            {
+                pressureForce += mass * -(inParticle[id].pressure + inParticle[i].pressure)/(2.f * inParticle[i].density) * grad_spiky(dist) * normalize(dir);
+                viscosityForce += mass * (inParticle[i].velocity - inParticle[id].velocity)/inParticle[i].density * laplacian(dist);
+            }
+        }
+    }
+    viscosityForce *= viscosity;
+
+    outParticle[id].force = externalForce + pressureForce + viscosityForce;
+    outParticle[id].density = inParticle[id].density;
+    outParticle[id].pressure = inParticle[id].pressure;
+    outParticle[id].position = inParticle[id].position;
+    outParticle[id].velocity = inParticle[id].velocity;
+}
diff --git a/projects/sph/shaders/particleShading.inc b/projects/sph/shaders/particleShading.inc
new file mode 100644
index 0000000000000000000000000000000000000000..b2d1832b9ccd6ba05a585b59bdfdedd4729e80f8
--- /dev/null
+++ b/projects/sph/shaders/particleShading.inc
@@ -0,0 +1,6 @@
+float circleFactor(vec2 triangleCoordinates){
+    // percentage of distance from center to circle edge
+    float p = clamp((0.4 - length(triangleCoordinates)) / 0.4, 0, 1);
+    // remapping for nice falloff
+    return sqrt(p);
+}
\ No newline at end of file
diff --git a/projects/sph/shaders/particle_params.inc b/projects/sph/shaders/particle_params.inc
new file mode 100644
index 0000000000000000000000000000000000000000..e35cce13be1bc84f045da276fe46666d0b852984
--- /dev/null
+++ b/projects/sph/shaders/particle_params.inc
@@ -0,0 +1,8 @@
+#define h 0.4
+#define mass 0.15
+#define gasConstant 3000
+#define offset 1800
+#define gravity -1000
+#define viscosity 1500
+#define ABSORBTION 0.9
+#define dt 0.0005
diff --git a/projects/sph/shaders/pressure.comp b/projects/sph/shaders/pressure.comp
new file mode 100644
index 0000000000000000000000000000000000000000..05b3af3afb490b427cc1297f21a82a779d4c8ecb
--- /dev/null
+++ b/projects/sph/shaders/pressure.comp
@@ -0,0 +1,72 @@
+#version 450 core
+#extension GL_ARB_separate_shader_objects : enable
+#extension GL_GOOGLE_include_directive : enable
+
+const float PI = 3.1415926535897932384626433832795;
+
+layout(local_size_x = 256) in;
+
+struct Particle
+{
+    vec3 position;
+    float padding;
+    vec3 velocity;
+    float density;
+    vec3 force;
+    float pressure;
+    
+};
+
+layout(std430, binding = 0) readonly buffer buffer_inParticle
+{
+    Particle inParticle[];
+};
+
+layout(std430, binding = 1) writeonly buffer buffer_outParticle
+{
+    Particle outParticle[];
+};
+
+layout( push_constant ) uniform constants{
+    float h;
+    float mass;
+    float gasConstant;
+    float offset;
+    float gravity;
+    float viscosity;
+    float ABSORBTION;
+    float dt;
+    vec3 gravityDir;
+    float particleCount;
+};
+
+float poly6(float r)    
+{
+    return (315.f * pow((pow(h,2)-pow(r,2)), 3)/(64.f*PI*pow(h, 9))) * int(0<=r && r<=h);
+}
+
+float densitySum = 0.f;
+
+void main() {
+    
+    uint id = gl_GlobalInvocationID.x;
+
+    if(id >= int(particleCount))
+    {
+        return;
+    }
+
+    for(uint i = 0; i < int(particleCount); i++)   
+    {
+        if (id != i)
+        {
+            float dist = distance(inParticle[id].position, 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 = inParticle[id].position;
+    outParticle[id].velocity = inParticle[id].velocity;
+    outParticle[id].force = inParticle[id].force;
+}
diff --git a/projects/sph/shaders/shader.vert b/projects/sph/shaders/shader.vert
new file mode 100644
index 0000000000000000000000000000000000000000..f5531ffa4f26d3652e8e35971c16af6dda2e3b45
--- /dev/null
+++ b/projects/sph/shaders/shader.vert
@@ -0,0 +1,49 @@
+#version 460 core
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(location = 0) in vec3 particle;
+
+struct Particle
+{
+    vec3 position;
+    float padding;
+    vec3 velocity;
+    float density;
+    vec3 force;
+    float pressure;
+};
+
+layout(std430, binding = 2) readonly buffer buffer_inParticle1
+{
+    Particle inParticle1[];
+};
+
+layout(std430, binding = 3) readonly buffer buffer_inParticle2
+{
+    Particle inParticle2[];
+};
+
+layout( push_constant ) uniform constants{
+    mat4 view;
+    mat4 projection;
+};
+
+layout(location = 0) out vec2 passTriangleCoordinates;
+layout(location = 1) out vec3 passVelocity;
+
+void main()
+{
+    int id = gl_InstanceIndex;
+    passVelocity = inParticle1[id].velocity;
+    
+    // particle position in view space
+    vec4 positionView = view * vec4(inParticle1[id].position, 1);
+    // by adding the triangle position in view space the mesh is always camera facing
+    positionView.xyz += particle;
+    // multiply with projection matrix for final position
+	gl_Position = projection * positionView;
+    
+    // 0.01 corresponds to vertex position size in main
+    float normalizationDivider  = 0.012;
+    passTriangleCoordinates     = particle.xy / normalizationDivider;
+}
\ No newline at end of file
diff --git a/projects/sph/shaders/shader_water.frag b/projects/sph/shaders/shader_water.frag
new file mode 100644
index 0000000000000000000000000000000000000000..2aee4ec692a2ada060a77389099b2c279e9c338c
--- /dev/null
+++ b/projects/sph/shaders/shader_water.frag
@@ -0,0 +1,28 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+#extension GL_GOOGLE_include_directive : enable
+
+#include "particleShading.inc"
+
+layout(location = 0) in vec2 passTriangleCoordinates;
+layout(location = 1) in vec3 passVelocity;
+
+layout(location = 0) out vec3 outColor;
+
+layout(set=0, binding=0) uniform uColor {
+	vec4 color;
+} Color;
+
+layout(set=0,binding=1) uniform uPosition{
+	vec2 position;
+} Position;
+
+void main()
+{
+    float p = length(passVelocity)/100.f;
+    outColor = vec3(0.f+p/3.f, 0.05f+p/2.f, 0.4f+p);
+
+    // make the triangle look like a circle
+   outColor *= circleFactor(passTriangleCoordinates);
+
+}
diff --git a/projects/sph/shaders/tonemapping.comp b/projects/sph/shaders/tonemapping.comp
new file mode 100644
index 0000000000000000000000000000000000000000..26f0232d66e3475afdd1266c0cc6288b47ed1c38
--- /dev/null
+++ b/projects/sph/shaders/tonemapping.comp
@@ -0,0 +1,19 @@
+#version 440
+
+layout(set=0, binding=0, rgba16f)   uniform image2D inImage;
+layout(set=0, binding=1, rgba8)     uniform image2D outImage;
+
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+void main(){
+
+    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(inImage)))){
+        return;
+    }
+    ivec2 uv            = ivec2(gl_GlobalInvocationID.xy);
+    vec3 linearColor    = imageLoad(inImage, uv).rgb;
+    vec3 tonemapped     = linearColor / (dot(linearColor, vec3(0.21, 0.71, 0.08)) + 1); // reinhard tonemapping
+    vec3 gammaCorrected = pow(tonemapped, vec3(1.f / 2.2f));
+    imageStore(outImage, uv, vec4(gammaCorrected, 0.f));
+}
\ No newline at end of file
diff --git a/projects/sph/shaders/updateData.comp b/projects/sph/shaders/updateData.comp
new file mode 100644
index 0000000000000000000000000000000000000000..3c935b232aff11388cc3b371e5524fa30486b36f
--- /dev/null
+++ b/projects/sph/shaders/updateData.comp
@@ -0,0 +1,109 @@
+#version 450 core
+#extension GL_ARB_separate_shader_objects : enable
+#extension GL_GOOGLE_include_directive : enable
+
+layout(local_size_x = 256) in;
+
+struct Particle
+{
+    vec3 position;
+    float padding;
+    vec3 velocity;
+    float density;
+    vec3 force;
+    float pressure;
+    
+};
+
+layout(std430, binding = 0) readonly buffer buffer_inParticle
+{
+    Particle inParticle[];
+};
+
+layout(std430, binding = 1) writeonly buffer buffer_outParticle
+{
+    Particle outParticle[];
+};
+
+layout( push_constant ) uniform constants{
+    float h;
+    float mass;
+    float gasConstant;
+    float offset;
+    float gravity;
+    float viscosity;
+    float ABSORBTION;
+    float dt;
+    vec3 gravityDir;
+    float particleCount;
+};
+
+void main() {
+
+    uint id = gl_GlobalInvocationID.x;
+
+    if(id >= int(particleCount))
+    {
+        return;
+    }
+
+    vec3 accel = inParticle[id].force / inParticle[id].density;
+    vec3 vel_new = inParticle[id].velocity + (dt * accel);
+
+    vec3 out_force = inParticle[id].force;
+    float out_density = inParticle[id].density;
+    float out_pressure = inParticle[id].pressure;
+    
+    if (length(vel_new) > 100.f)
+    {
+        vel_new = normalize(vel_new)*50;
+        out_density = 0.01f;
+        out_pressure = 0.01f;
+        out_force = gravity * vec3(-gravityDir.x,gravityDir.y,gravityDir.z);
+    }
+
+    vec3 pos_new = inParticle[id].position + (dt * vel_new);
+
+    // Überprüfe Randbedingungen x
+    if (inParticle[id].position.x < -1.0)
+    {
+        vel_new = reflect(vel_new.xyz, vec3(1.f,0.f,0.f)) * ABSORBTION;
+        pos_new.x = -1.0 + 0.01f;
+    }
+    else if (inParticle[id].position.x > 1.0)
+    {
+        vel_new = reflect(vel_new,vec3(1.f,0.f,0.f)) * ABSORBTION;
+        pos_new.x = 1.0 - 0.01f;
+    }
+
+    // Überprüfe Randbedingungen y
+    if (inParticle[id].position.y < -1.0)
+    {
+        vel_new = reflect(vel_new,vec3(0.f,1.f,0.f)) * ABSORBTION;
+        pos_new.y = -1.0 + 0.01f;
+
+    }
+    else if (inParticle[id].position.y > 1.0)
+    {
+        vel_new = reflect(vel_new,vec3(0.f,1.f,0.f)) * ABSORBTION;
+        pos_new.y = 1.0 - 0.01f;
+    }
+
+    // Überprüfe Randbedingungen z
+    if (inParticle[id].position.z < -1.0 )
+    {
+        vel_new = reflect(vel_new,vec3(0.f,0.f,1.f)) * ABSORBTION;
+        pos_new.z = -1.0 + 0.01f;
+    }
+    else if (inParticle[id].position.z > 1.0 )
+    {
+        vel_new = reflect(vel_new,vec3(0.f,0.f,1.f)) * ABSORBTION;
+        pos_new.z = 1.0 - 0.01f;
+    }
+
+    outParticle[id].force = out_force;
+    outParticle[id].density = out_density;
+    outParticle[id].pressure = out_pressure;
+    outParticle[id].position = pos_new;
+    outParticle[id].velocity = vel_new;
+}
diff --git a/projects/sph/src/BloomAndFlares.cpp b/projects/sph/src/BloomAndFlares.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..09534815afcd8ab238b79da5c6bbceb6672b043a
--- /dev/null
+++ b/projects/sph/src/BloomAndFlares.cpp
@@ -0,0 +1,285 @@
+#include "BloomAndFlares.hpp"
+#include <vkcv/shader/GLSLCompiler.hpp>
+
+BloomAndFlares::BloomAndFlares(
+        vkcv::Core *p_Core,
+        vk::Format colorBufferFormat,
+        uint32_t width,
+        uint32_t height) :
+
+        p_Core(p_Core),
+        m_ColorBufferFormat(colorBufferFormat),
+        m_Width(width),
+        m_Height(height),
+        m_LinearSampler(p_Core->createSampler(vkcv::SamplerFilterType::LINEAR,
+                                              vkcv::SamplerFilterType::LINEAR,
+                                              vkcv::SamplerMipmapMode::LINEAR,
+                                              vkcv::SamplerAddressMode::CLAMP_TO_EDGE)),
+        m_Blur(p_Core->createImage(colorBufferFormat, width, height, 1, true, true, false)),
+        m_LensFeatures(p_Core->createImage(colorBufferFormat, width, height, 1, false, true, false))
+{
+    vkcv::shader::GLSLCompiler compiler;
+
+    // DOWNSAMPLE
+    vkcv::ShaderProgram dsProg;
+    compiler.compile(vkcv::ShaderStage::COMPUTE,
+                     "shaders/bloom/downsample.comp",
+                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
+                     {
+                         dsProg.addShader(shaderStage, path);
+                     });
+    for(uint32_t mipLevel = 0; mipLevel < m_Blur.getMipCount(); mipLevel++)
+    {
+        m_DownsampleDescSetLayouts.push_back(
+                p_Core->createDescriptorSetLayout(dsProg.getReflectedDescriptors().at(0)));
+
+		m_DownsampleDescSets.push_back(
+		        p_Core->createDescriptorSet(m_DownsampleDescSetLayouts.back()));
+    }
+    m_DownsamplePipe = p_Core->createComputePipeline({
+            dsProg, { p_Core->getDescriptorSetLayout(m_DownsampleDescSetLayouts[0]).vulkanHandle } });
+
+    // UPSAMPLE
+    vkcv::ShaderProgram usProg;
+    compiler.compile(vkcv::ShaderStage::COMPUTE,
+                     "shaders/bloom/upsample.comp",
+                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
+                     {
+                         usProg.addShader(shaderStage, path);
+                     });
+    for(uint32_t mipLevel = 0; mipLevel < m_Blur.getMipCount(); mipLevel++)
+    {
+        m_UpsampleDescSetLayouts.push_back(
+                p_Core->createDescriptorSetLayout(usProg.getReflectedDescriptors().at(0)));
+        m_UpsampleDescSets.push_back(
+                p_Core->createDescriptorSet(m_UpsampleDescSetLayouts.back()));
+    }
+    m_UpsamplePipe = p_Core->createComputePipeline({
+            usProg, { p_Core->getDescriptorSetLayout(m_UpsampleDescSetLayouts[0]).vulkanHandle } });
+
+    // LENS FEATURES
+    vkcv::ShaderProgram lensProg;
+    compiler.compile(vkcv::ShaderStage::COMPUTE,
+                     "shaders/bloom/lensFlares.comp",
+                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
+                     {
+                         lensProg.addShader(shaderStage, path);
+                     });
+    m_LensFlareDescSetLayout = p_Core->createDescriptorSetLayout(lensProg.getReflectedDescriptors().at(0));
+    m_LensFlareDescSet = p_Core->createDescriptorSet(m_LensFlareDescSetLayout);
+    m_LensFlarePipe = p_Core->createComputePipeline({
+            lensProg, { p_Core->getDescriptorSetLayout(m_LensFlareDescSetLayout).vulkanHandle } });
+
+    // COMPOSITE
+    vkcv::ShaderProgram compProg;
+    compiler.compile(vkcv::ShaderStage::COMPUTE,
+                     "shaders/bloom/composite.comp",
+                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
+                     {
+                         compProg.addShader(shaderStage, path);
+                     });
+    m_CompositeDescSetLayout = p_Core->createDescriptorSetLayout(compProg.getReflectedDescriptors().at(0));
+    m_CompositeDescSet = p_Core->createDescriptorSet(m_CompositeDescSetLayout);
+    m_CompositePipe = p_Core->createComputePipeline({
+            compProg, { p_Core->getDescriptorSetLayout(m_CompositeDescSetLayout).vulkanHandle } });
+}
+
+void BloomAndFlares::execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStream,
+                                        const vkcv::ImageHandle &colorAttachment)
+{
+    auto dispatchCountX  = static_cast<float>(m_Width)  / 8.0f;
+    auto dispatchCountY = static_cast<float>(m_Height) / 8.0f;
+    // blur dispatch
+    uint32_t initialDispatchCount[3] = {
+            static_cast<uint32_t>(glm::ceil(dispatchCountX)),
+            static_cast<uint32_t>(glm::ceil(dispatchCountY)),
+            1
+    };
+
+    // downsample dispatch of original color attachment
+    p_Core->prepareImageForSampling(cmdStream, colorAttachment);
+    p_Core->prepareImageForStorage(cmdStream, m_Blur.getHandle());
+
+    vkcv::DescriptorWrites initialDownsampleWrites;
+    initialDownsampleWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, colorAttachment)};
+    initialDownsampleWrites.samplerWrites      = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
+    initialDownsampleWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_Blur.getHandle(), 0) };
+    p_Core->writeDescriptorSet(m_DownsampleDescSets[0], initialDownsampleWrites);
+
+    p_Core->recordComputeDispatchToCmdStream(
+            cmdStream,
+            m_DownsamplePipe,
+            initialDispatchCount,
+            {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_DownsampleDescSets[0]).vulkanHandle)},
+            vkcv::PushConstants(0));
+
+    // downsample dispatches of blur buffer's mip maps
+    float mipDispatchCountX = dispatchCountX;
+    float mipDispatchCountY = dispatchCountY;
+    for(uint32_t mipLevel = 1; mipLevel < std::min((uint32_t)m_DownsampleDescSets.size(), m_Blur.getMipCount()); mipLevel++)
+    {
+        // mip descriptor writes
+        vkcv::DescriptorWrites mipDescriptorWrites;
+        mipDescriptorWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle(), mipLevel - 1, true)};
+        mipDescriptorWrites.samplerWrites      = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
+        mipDescriptorWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_Blur.getHandle(), mipLevel) };
+        p_Core->writeDescriptorSet(m_DownsampleDescSets[mipLevel], mipDescriptorWrites);
+
+        // mip dispatch calculation
+        mipDispatchCountX  /= 2.0f;
+        mipDispatchCountY /= 2.0f;
+
+        uint32_t mipDispatchCount[3] = {
+                static_cast<uint32_t>(glm::ceil(mipDispatchCountX)),
+                static_cast<uint32_t>(glm::ceil(mipDispatchCountY)),
+                1
+        };
+
+        if(mipDispatchCount[0] == 0)
+            mipDispatchCount[0] = 1;
+        if(mipDispatchCount[1] == 0)
+            mipDispatchCount[1] = 1;
+
+        // mip blur dispatch
+        p_Core->recordComputeDispatchToCmdStream(
+                cmdStream,
+                m_DownsamplePipe,
+                mipDispatchCount,
+                {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_DownsampleDescSets[mipLevel]).vulkanHandle)},
+                vkcv::PushConstants(0));
+
+        // image barrier between mips
+        p_Core->recordImageMemoryBarrier(cmdStream, m_Blur.getHandle());
+    }
+}
+
+void BloomAndFlares::execUpsamplePipe(const vkcv::CommandStreamHandle &cmdStream)
+{
+    // upsample dispatch
+    p_Core->prepareImageForStorage(cmdStream, m_Blur.getHandle());
+
+    const uint32_t upsampleMipLevels = std::min(
+    		static_cast<uint32_t>(m_UpsampleDescSets.size() - 1),
+    		static_cast<uint32_t>(3)
+	);
+
+    // upsample dispatch for each mip map
+    for(uint32_t mipLevel = upsampleMipLevels; mipLevel > 0; mipLevel--)
+    {
+        // mip descriptor writes
+        vkcv::DescriptorWrites mipUpsampleWrites;
+        mipUpsampleWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle(), mipLevel, true)};
+        mipUpsampleWrites.samplerWrites      = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
+        mipUpsampleWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_Blur.getHandle(), mipLevel - 1) };
+        p_Core->writeDescriptorSet(m_UpsampleDescSets[mipLevel], mipUpsampleWrites);
+
+        auto mipDivisor = glm::pow(2.0f, static_cast<float>(mipLevel) - 1.0f);
+
+        auto upsampleDispatchX  = static_cast<float>(m_Width) / mipDivisor;
+        auto upsampleDispatchY = static_cast<float>(m_Height) / mipDivisor;
+        upsampleDispatchX /= 8.0f;
+        upsampleDispatchY /= 8.0f;
+
+        const uint32_t upsampleDispatchCount[3] = {
+                static_cast<uint32_t>(glm::ceil(upsampleDispatchX)),
+                static_cast<uint32_t>(glm::ceil(upsampleDispatchY)),
+                1
+        };
+
+        p_Core->recordComputeDispatchToCmdStream(
+                cmdStream,
+                m_UpsamplePipe,
+                upsampleDispatchCount,
+                {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_UpsampleDescSets[mipLevel]).vulkanHandle)},
+                vkcv::PushConstants(0)
+        );
+        // image barrier between mips
+        p_Core->recordImageMemoryBarrier(cmdStream, m_Blur.getHandle());
+    }
+}
+
+void BloomAndFlares::execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStream)
+{
+    // lens feature generation descriptor writes
+    p_Core->prepareImageForSampling(cmdStream, m_Blur.getHandle());
+    p_Core->prepareImageForStorage(cmdStream, m_LensFeatures.getHandle());
+
+    vkcv::DescriptorWrites lensFeatureWrites;
+    lensFeatureWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle(), 0)};
+    lensFeatureWrites.samplerWrites = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
+    lensFeatureWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_LensFeatures.getHandle(), 0)};
+    p_Core->writeDescriptorSet(m_LensFlareDescSet, lensFeatureWrites);
+
+    auto dispatchCountX  = static_cast<float>(m_Width)  / 8.0f;
+    auto dispatchCountY = static_cast<float>(m_Height) / 8.0f;
+    // lens feature generation dispatch
+    uint32_t lensFeatureDispatchCount[3] = {
+            static_cast<uint32_t>(glm::ceil(dispatchCountX)),
+            static_cast<uint32_t>(glm::ceil(dispatchCountY)),
+            1
+    };
+    p_Core->recordComputeDispatchToCmdStream(
+            cmdStream,
+            m_LensFlarePipe,
+            lensFeatureDispatchCount,
+            {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_LensFlareDescSet).vulkanHandle)},
+            vkcv::PushConstants(0));
+}
+
+void BloomAndFlares::execCompositePipe(const vkcv::CommandStreamHandle &cmdStream,
+                                       const vkcv::ImageHandle &colorAttachment)
+{
+    p_Core->prepareImageForSampling(cmdStream, m_Blur.getHandle());
+    p_Core->prepareImageForSampling(cmdStream, m_LensFeatures.getHandle());
+    p_Core->prepareImageForStorage(cmdStream, colorAttachment);
+
+    // bloom composite descriptor write
+    vkcv::DescriptorWrites compositeWrites;
+    compositeWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle()),
+                                          vkcv::SampledImageDescriptorWrite(1, m_LensFeatures.getHandle())};
+    compositeWrites.samplerWrites = {vkcv::SamplerDescriptorWrite(2, m_LinearSampler)};
+    compositeWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(3, colorAttachment)};
+    p_Core->writeDescriptorSet(m_CompositeDescSet, compositeWrites);
+
+    float dispatchCountX = static_cast<float>(m_Width)  / 8.0f;
+    float dispatchCountY = static_cast<float>(m_Height) / 8.0f;
+
+    uint32_t compositeDispatchCount[3] = {
+            static_cast<uint32_t>(glm::ceil(dispatchCountX)),
+            static_cast<uint32_t>(glm::ceil(dispatchCountY)),
+            1
+    };
+
+    // bloom composite dispatch
+    p_Core->recordComputeDispatchToCmdStream(
+            cmdStream,
+            m_CompositePipe,
+            compositeDispatchCount,
+            {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_CompositeDescSet).vulkanHandle)},
+            vkcv::PushConstants(0));
+}
+
+void BloomAndFlares::execWholePipeline(const vkcv::CommandStreamHandle &cmdStream,
+                                       const vkcv::ImageHandle &colorAttachment)
+{
+    execDownsamplePipe(cmdStream, colorAttachment);
+    execUpsamplePipe(cmdStream);
+    execLensFeaturePipe(cmdStream);
+    execCompositePipe(cmdStream, colorAttachment);
+}
+
+void BloomAndFlares::updateImageDimensions(uint32_t width, uint32_t height)
+{
+    if ((width == m_Width) && (height == m_Height)) {
+        return;
+    }
+    
+    m_Width  = width;
+    m_Height = height;
+
+    p_Core->getContext().getDevice().waitIdle();
+    m_Blur = p_Core->createImage(m_ColorBufferFormat, m_Width, m_Height, 1, true, true, false);
+    m_LensFeatures = p_Core->createImage(m_ColorBufferFormat, m_Width, m_Height, 1, false, true, false);
+}
+
+
diff --git a/projects/sph/src/BloomAndFlares.hpp b/projects/sph/src/BloomAndFlares.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..1644d38e9c98c7a0bf74d48b173f0e627214f1e1
--- /dev/null
+++ b/projects/sph/src/BloomAndFlares.hpp
@@ -0,0 +1,51 @@
+#pragma once
+#include <vkcv/Core.hpp>
+#include <glm/glm.hpp>
+
+class BloomAndFlares{
+public:
+    BloomAndFlares(vkcv::Core *p_Core,
+                   vk::Format colorBufferFormat,
+                   uint32_t width,
+                   uint32_t height);
+
+    void execWholePipeline(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment);
+
+    void updateImageDimensions(uint32_t width, uint32_t height);
+
+private:
+    vkcv::Core *p_Core;
+
+    vk::Format m_ColorBufferFormat;
+    uint32_t m_Width;
+    uint32_t m_Height;
+
+    vkcv::SamplerHandle m_LinearSampler;
+    vkcv::Image m_Blur;
+    vkcv::Image m_LensFeatures;
+
+
+    vkcv::ComputePipelineHandle                     m_DownsamplePipe;
+    std::vector<vkcv::DescriptorSetLayoutHandle>    m_DownsampleDescSetLayouts;
+    std::vector<vkcv::DescriptorSetHandle>          m_DownsampleDescSets; // per mip desc set
+
+    vkcv::ComputePipelineHandle                     m_UpsamplePipe;
+    std::vector<vkcv::DescriptorSetLayoutHandle>    m_UpsampleDescSetLayouts;
+    std::vector<vkcv::DescriptorSetHandle>          m_UpsampleDescSets;   // per mip desc set
+
+    vkcv::ComputePipelineHandle                     m_LensFlarePipe;
+    vkcv::DescriptorSetLayoutHandle                 m_LensFlareDescSetLayout;
+    vkcv::DescriptorSetHandle                       m_LensFlareDescSet;
+
+    vkcv::ComputePipelineHandle                     m_CompositePipe;
+    vkcv::DescriptorSetLayoutHandle                 m_CompositeDescSetLayout;
+    vkcv::DescriptorSetHandle                       m_CompositeDescSet;
+
+    void execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment);
+    void execUpsamplePipe(const vkcv::CommandStreamHandle &cmdStream);
+    void execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStream);
+    void execCompositePipe(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment);
+};
+
+
+
diff --git a/projects/sph/src/Particle.cpp b/projects/sph/src/Particle.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..329236989b7cab502e7a4e1bb5aa27869bed53cb
--- /dev/null
+++ b/projects/sph/src/Particle.cpp
@@ -0,0 +1,39 @@
+
+#include "Particle.hpp"
+
+Particle::Particle(glm::vec3 position, glm::vec3 velocity)
+: m_position(position),
+  m_velocity(velocity)
+{
+    m_density = 0.f;
+    m_force = glm::vec3(0.f);
+    m_pressure = 0.f;
+}
+
+const glm::vec3& Particle::getPosition()const{
+    return m_position;
+}
+
+void Particle::setPosition( const glm::vec3 pos ){
+    m_position = pos;
+}
+
+const glm::vec3& Particle::getVelocity()const{
+    return m_velocity;
+}
+
+void Particle::setVelocity( const glm::vec3 vel ){
+    m_velocity = vel;
+}
+
+const float& Particle::getDensity()const {
+    return m_density;
+}
+
+const glm::vec3& Particle::getForce()const {
+    return m_force;
+}
+
+const float& Particle::getPressure()const {
+    return m_pressure;
+}
\ No newline at end of file
diff --git a/projects/sph/src/Particle.hpp b/projects/sph/src/Particle.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..6c4ab50b74cc4544976318c23e36f4b91989ee66
--- /dev/null
+++ b/projects/sph/src/Particle.hpp
@@ -0,0 +1,34 @@
+#pragma once
+
+#include <glm/glm.hpp>
+
+class Particle {
+
+public:
+    Particle(glm::vec3 position, glm::vec3 velocity);
+
+    const glm::vec3& getPosition()const;
+
+    void setPosition( const glm::vec3 pos );
+
+    const glm::vec3& getVelocity()const;
+
+    void setVelocity( const glm::vec3 vel );
+
+    const float& getDensity()const;
+
+    const glm::vec3& getForce()const;
+
+    const float& getPressure()const;
+
+
+
+private:
+    // all properties of the Particle
+    glm::vec3 m_position;
+    float m_padding1;
+    glm::vec3 m_velocity;
+    float m_density;
+    glm::vec3 m_force;
+    float m_pressure;
+};
diff --git a/projects/sph/src/PipelineInit.cpp b/projects/sph/src/PipelineInit.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6cf941fa0d8f8716b7d05daf9b6fb618b0fa7d85
--- /dev/null
+++ b/projects/sph/src/PipelineInit.cpp
@@ -0,0 +1,27 @@
+#include "PipelineInit.hpp"
+
+vkcv::DescriptorSetHandle PipelineInit::ComputePipelineInit(vkcv::Core *pCore, vkcv::ShaderStage shaderStage, std::filesystem::path includePath,
+                                vkcv::ComputePipelineHandle &pipeline) {
+    vkcv::ShaderProgram shaderProgram;
+    vkcv::shader::GLSLCompiler compiler;
+    compiler.compile(shaderStage, includePath,
+                     [&](vkcv::ShaderStage shaderStage1, const std::filesystem::path& path1) {shaderProgram.addShader(shaderStage1, path1);
+    });
+    vkcv::DescriptorSetLayoutHandle descriptorSetLayout = pCore->createDescriptorSetLayout(
+            shaderProgram.getReflectedDescriptors().at(0));
+    vkcv::DescriptorSetHandle descriptorSet = pCore->createDescriptorSet(descriptorSetLayout);
+
+    const std::vector<vkcv::VertexAttachment> vertexAttachments = shaderProgram.getVertexAttachments();
+
+    std::vector<vkcv::VertexBinding> bindings;
+    for (size_t i = 0; i < vertexAttachments.size(); i++) {
+        bindings.push_back(vkcv::VertexBinding(i, { vertexAttachments[i] }));
+    }
+    const vkcv::VertexLayout layout(bindings);
+
+    pipeline = pCore->createComputePipeline({
+            shaderProgram,
+            { pCore->getDescriptorSetLayout(descriptorSetLayout).vulkanHandle } });
+
+    return  descriptorSet;
+}
\ No newline at end of file
diff --git a/projects/sph/src/PipelineInit.hpp b/projects/sph/src/PipelineInit.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..e628af0eef9c0558719b405790246946d8720d47
--- /dev/null
+++ b/projects/sph/src/PipelineInit.hpp
@@ -0,0 +1,12 @@
+#pragma once
+#include <vkcv/Core.hpp>
+#include <vkcv/shader/GLSLCompiler.hpp>
+#include <fstream>
+
+class PipelineInit{
+public:
+    static vkcv::DescriptorSetHandle ComputePipelineInit(vkcv::Core *pCore,
+                                                         vkcv::ShaderStage shaderStage,
+                                                         std::filesystem::path includePath,
+                                                         vkcv::ComputePipelineHandle& pipeline);
+};
diff --git a/projects/sph/src/main.cpp b/projects/sph/src/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..255fb0e73f068ff9e5e8ce892897a1325631b6e1
--- /dev/null
+++ b/projects/sph/src/main.cpp
@@ -0,0 +1,409 @@
+#include <iostream>
+#include <vkcv/Core.hpp>
+#include <GLFW/glfw3.h>
+#include <vkcv/camera/CameraManager.hpp>
+#include <chrono>
+#include <random>
+#include <glm/glm.hpp>
+#include <glm/gtc/matrix_access.hpp>
+#include <glm/gtc/matrix_transform.hpp>
+#include <time.h>
+#include <vkcv/shader/GLSLCompiler.hpp>
+#include "BloomAndFlares.hpp"
+#include "PipelineInit.hpp"
+#include "Particle.hpp"
+
+int main(int argc, const char **argv) {
+    const char *applicationName = "SPH";
+
+    vkcv::Core core = vkcv::Core::create(
+        applicationName,
+        VK_MAKE_VERSION(0, 0, 1),
+        { vk::QueueFlagBits::eTransfer, vk::QueueFlagBits::eGraphics, vk::QueueFlagBits::eCompute },
+        { VK_KHR_SWAPCHAIN_EXTENSION_NAME }
+    );
+
+    vkcv::WindowHandle windowHandle = core.createWindow(applicationName, 1920, 1080, false);
+    vkcv::Window& window = core.getWindow(windowHandle);
+
+    vkcv::camera::CameraManager cameraManager(window);
+
+    auto particleIndexBuffer = core.createBuffer<uint16_t>(vkcv::BufferType::INDEX, 3,
+                                                           vkcv::BufferMemoryType::DEVICE_LOCAL);
+    uint16_t indices[3] = {0, 1, 2};
+    particleIndexBuffer.fill(&indices[0], sizeof(indices));
+
+    vk::Format colorFormat = vk::Format::eR16G16B16A16Sfloat;
+    // an example attachment for passes that output to the window
+    const vkcv::AttachmentDescription present_color_attachment(
+            vkcv::AttachmentOperation::STORE,
+            vkcv::AttachmentOperation::CLEAR,
+            colorFormat);
+
+
+    vkcv::PassConfig particlePassDefinition({present_color_attachment});
+    vkcv::PassHandle particlePass = core.createPass(particlePassDefinition);
+
+    vkcv::PassConfig computePassDefinition({});
+    vkcv::PassHandle computePass = core.createPass(computePassDefinition);
+
+    //rotation
+    float rotationx = 0;
+    float rotationy = 0;
+
+    // params  
+    float param_h = 0.20;
+    float param_mass = 0.03;
+    float param_gasConstant = 3500;
+    float param_offset = 200;
+    float param_gravity = -5000;
+    float param_viscosity = 10;
+    float param_ABSORBTION = 0.5;
+    float param_dt = 0.0005;
+
+    if (!particlePass || !computePass)
+    {
+        std::cout << "Error. Could not create renderpass. Exiting." << std::endl;
+        return EXIT_FAILURE;
+    }
+	vkcv::shader::GLSLCompiler compiler;
+
+// comp shader 1
+    vkcv::ComputePipelineHandle computePipeline1;
+    vkcv::DescriptorSetHandle computeDescriptorSet1 = PipelineInit::ComputePipelineInit(&core, vkcv::ShaderStage::COMPUTE,
+                                                                          "shaders/pressure.comp", computePipeline1);
+// comp shader 2
+    vkcv::ComputePipelineHandle computePipeline2;
+    vkcv::DescriptorSetHandle computeDescriptorSet2 = PipelineInit::ComputePipelineInit(&core, vkcv::ShaderStage::COMPUTE,
+                                                                          "shaders/force.comp", computePipeline2);
+
+//comp shader 3
+    vkcv::ComputePipelineHandle computePipeline3;
+    vkcv::DescriptorSetHandle computeDescriptorSet3 = PipelineInit::ComputePipelineInit(&core, vkcv::ShaderStage::COMPUTE,
+                                                                           "shaders/updateData.comp", computePipeline3);
+
+//comp shader 4
+    vkcv::ComputePipelineHandle computePipeline4;
+    vkcv::DescriptorSetHandle computeDescriptorSet4 = PipelineInit::ComputePipelineInit(&core, vkcv::ShaderStage::COMPUTE,
+                                                                            "shaders/flip.comp", computePipeline4);
+
+// shader
+    vkcv::ShaderProgram particleShaderProgram{};
+    compiler.compile(vkcv::ShaderStage::VERTEX, "shaders/shader.vert", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+        particleShaderProgram.addShader(shaderStage, path);
+    });
+    compiler.compile(vkcv::ShaderStage::FRAGMENT, "shaders/shader_water.frag", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+        particleShaderProgram.addShader(shaderStage, path);
+    });
+
+    vkcv::DescriptorSetLayoutHandle descriptorSetLayout = core.createDescriptorSetLayout(
+            particleShaderProgram.getReflectedDescriptors().at(0));
+    vkcv::DescriptorSetHandle descriptorSet = core.createDescriptorSet(descriptorSetLayout);
+
+    vkcv::Buffer<glm::vec3> vertexBuffer = core.createBuffer<glm::vec3>(
+            vkcv::BufferType::VERTEX,
+            3
+    );
+    const std::vector<vkcv::VertexAttachment> vertexAttachments = particleShaderProgram.getVertexAttachments();
+
+    const std::vector<vkcv::VertexBufferBinding> vertexBufferBindings = {
+            vkcv::VertexBufferBinding(0, vertexBuffer.getVulkanHandle())};
+
+    std::vector<vkcv::VertexBinding> bindings;
+    for (size_t i = 0; i < vertexAttachments.size(); i++) {
+        bindings.push_back(vkcv::VertexBinding(i, {vertexAttachments[i]}));
+    }
+
+    const vkcv::VertexLayout particleLayout(bindings);
+
+    vkcv::GraphicsPipelineConfig particlePipelineDefinition{
+            particleShaderProgram,
+            UINT32_MAX,
+            UINT32_MAX,
+            particlePass,
+            {particleLayout},
+            {core.getDescriptorSetLayout(descriptorSetLayout).vulkanHandle},
+            true};
+    particlePipelineDefinition.m_blendMode = vkcv::BlendMode::Additive;
+
+    const std::vector<glm::vec3> vertices = {glm::vec3(-0.012, 0.012, 0),
+                                             glm::vec3(0.012, 0.012, 0),
+                                             glm::vec3(0, -0.012, 0)};
+
+    vertexBuffer.fill(vertices);
+
+    vkcv::GraphicsPipelineHandle particlePipeline = core.createGraphicsPipeline(particlePipelineDefinition);
+
+    vkcv::Buffer<glm::vec4> color = core.createBuffer<glm::vec4>(
+            vkcv::BufferType::UNIFORM,
+            1
+    );
+
+    vkcv::Buffer<glm::vec2> position = core.createBuffer<glm::vec2>(
+            vkcv::BufferType::UNIFORM,
+            1
+    );
+
+    int numberParticles = 20000;
+    std::vector<Particle> particles;
+    for (int i = 0; i < numberParticles; i++) {
+        const float lo = 0.6;
+        const float hi = 0.9;
+        const float vlo = 0;
+        const float vhi = 70;
+        float x = lo + static_cast <float> (rand()) /( static_cast <float> (RAND_MAX/(hi-lo)));
+        float y = lo + static_cast <float> (rand()) /( static_cast <float> (RAND_MAX/(hi-lo)));
+        float z = lo + static_cast <float> (rand()) /( static_cast <float> (RAND_MAX/(hi-lo)));
+        float vx = vlo + static_cast <float> (rand()) /( static_cast <float> (RAND_MAX/(vhi-vlo)));
+        float vy = vlo + static_cast <float> (rand()) /( static_cast <float> (RAND_MAX/(vhi-vlo)));
+        float vz = vlo + static_cast <float> (rand()) /( static_cast <float> (RAND_MAX/(vhi-vlo)));
+        glm::vec3 pos = glm::vec3(x,y,z);
+        glm::vec3 vel = glm::vec3(vx,vy,vz);
+        //glm::vec3 vel = glm::vec3(0.0,0.0,0.0);
+        particles.push_back(Particle(pos, vel));
+    }
+
+    vkcv::Buffer<Particle> particleBuffer1 = core.createBuffer<Particle>(
+            vkcv::BufferType::STORAGE,
+            numberParticles * sizeof(glm::vec4) * 3
+
+    );
+
+    vkcv::Buffer<Particle> particleBuffer2 = core.createBuffer<Particle>(
+        vkcv::BufferType::STORAGE,
+        numberParticles * sizeof(glm::vec4) * 3
+    );
+
+    particleBuffer1.fill(particles);
+	particleBuffer2.fill(particles);
+
+    vkcv::DescriptorWrites setWrites;
+    setWrites.uniformBufferWrites = {vkcv::BufferDescriptorWrite(0,color.getHandle()),
+                                     vkcv::BufferDescriptorWrite(1,position.getHandle())};
+    setWrites.storageBufferWrites = { vkcv::BufferDescriptorWrite(2,particleBuffer1.getHandle()),
+									  vkcv::BufferDescriptorWrite(3,particleBuffer2.getHandle())};
+    core.writeDescriptorSet(descriptorSet, setWrites);
+
+    vkcv::DescriptorWrites computeWrites;
+    computeWrites.storageBufferWrites = { vkcv::BufferDescriptorWrite(0,particleBuffer1.getHandle()),
+										  vkcv::BufferDescriptorWrite(1,particleBuffer2.getHandle())};
+    
+    core.writeDescriptorSet(computeDescriptorSet1, computeWrites);
+	core.writeDescriptorSet(computeDescriptorSet2, computeWrites);
+    core.writeDescriptorSet(computeDescriptorSet3, computeWrites);
+	core.writeDescriptorSet(computeDescriptorSet4, computeWrites);
+
+    if (!particlePipeline || !computePipeline1 || !computePipeline2 || !computePipeline3 || !computePipeline4)
+    {
+        std::cout << "Error. Could not create graphics pipeline. Exiting." << std::endl;
+        return EXIT_FAILURE;
+    }
+
+    const vkcv::ImageHandle swapchainInput = vkcv::ImageHandle::createSwapchainImageHandle();
+
+    const vkcv::Mesh renderMesh({vertexBufferBindings}, particleIndexBuffer.getVulkanHandle(),
+                                particleIndexBuffer.getCount());
+    vkcv::DescriptorSetUsage descriptorUsage(0, core.getDescriptorSet(descriptorSet).vulkanHandle);
+
+    auto pos = glm::vec2(0.f);
+    auto spawnPosition = glm::vec3(0.f);
+
+    std::vector<vkcv::DrawcallInfo> drawcalls;
+    drawcalls.push_back(vkcv::DrawcallInfo(renderMesh, {descriptorUsage}, numberParticles));
+
+    auto start = std::chrono::system_clock::now();
+
+    glm::vec4 colorData = glm::vec4(1.0f, 1.0f, 0.0f, 1.0f);
+    uint32_t camIndex0 = cameraManager.addCamera(vkcv::camera::ControllerType::PILOT);
+    uint32_t camIndex1 = cameraManager.addCamera(vkcv::camera::ControllerType::TRACKBALL);
+
+    cameraManager.getCamera(camIndex0).setNearFar(0.1, 30);
+    cameraManager.getCamera(camIndex1).setNearFar(0.1, 30);
+
+    cameraManager.setActiveCamera(1);
+
+    cameraManager.getCamera(camIndex0).setPosition(glm::vec3(0, 0, -2.5));
+    cameraManager.getCamera(camIndex1).setPosition(glm::vec3(0.0f, 0.0f, -2.5f));
+    cameraManager.getCamera(camIndex1).setCenter(glm::vec3(0.0f, 0.0f, 0.0f));
+
+	auto swapchainExtent = core.getSwapchain(window.getSwapchainHandle()).getExtent();
+	
+    vkcv::ImageHandle colorBuffer = core.createImage(
+			colorFormat,
+			swapchainExtent.width,
+			swapchainExtent.height,
+			1, false, true, true
+	).getHandle();
+    BloomAndFlares bloomAndFlares(&core, colorFormat, swapchainExtent.width, swapchainExtent.height);
+
+    //tone mapping shader & pipeline
+    vkcv::ComputePipelineHandle tonemappingPipe;
+    vkcv::DescriptorSetHandle tonemappingDescriptor = PipelineInit::ComputePipelineInit(&core, vkcv::ShaderStage::COMPUTE,
+                                                                                        "shaders/tonemapping.comp", tonemappingPipe);
+
+
+    while (vkcv::Window::hasOpenWindow()) {
+        vkcv::Window::pollEvents();
+
+        uint32_t swapchainWidth, swapchainHeight;
+        if (!core.beginFrame(swapchainWidth, swapchainHeight, windowHandle)) {
+            continue;
+        }
+
+        color.fill(&colorData);
+        position.fill(&pos);
+
+        auto end = std::chrono::system_clock::now();
+        float deltatime = 0.000001 * static_cast<float>( std::chrono::duration_cast<std::chrono::microseconds>(end - start).count() );
+        start = end;
+
+        cameraManager.update(deltatime);
+
+        // split view and projection to allow for easy billboarding in shader
+        struct {
+			glm::mat4 view;
+			glm::mat4 projection;
+        } renderingMatrices;
+
+        glm::vec3 gravityDir = glm::rotate(glm::mat4(1.0), glm::radians(rotationx), glm::vec3(0.f,0.f,1.f)) * glm::vec4(0.f,1.f,0.f,0.f);
+        gravityDir = glm::rotate(glm::mat4(1.0), glm::radians(rotationy), glm::vec3(0.f,1.f,0.f)) * glm::vec4(gravityDir,0.f);
+
+        renderingMatrices.view = cameraManager.getActiveCamera().getView();
+        renderingMatrices.view = glm::rotate(renderingMatrices.view, glm::radians(rotationx), glm::vec3(0.f, 0.f, 1.f));
+        renderingMatrices.view = glm::rotate(renderingMatrices.view, glm::radians(rotationy), glm::vec3(0.f, 1.f, 0.f));
+        renderingMatrices.projection = cameraManager.getActiveCamera().getProjection();
+
+        // keybindings rotation
+        if (glfwGetKey(window.getWindow(), GLFW_KEY_LEFT) == GLFW_PRESS)
+            rotationx += deltatime * 50;
+        if (glfwGetKey(window.getWindow(), GLFW_KEY_RIGHT) == GLFW_PRESS)
+            rotationx -= deltatime * 50;
+        
+        if (glfwGetKey(window.getWindow(), GLFW_KEY_UP) == GLFW_PRESS)
+            rotationy += deltatime * 50;
+        if (glfwGetKey(window.getWindow(), GLFW_KEY_DOWN) == GLFW_PRESS)
+            rotationy -= deltatime * 50;
+
+        // keybindings params
+        if (glfwGetKey(window.getWindow(), GLFW_KEY_T) == GLFW_PRESS)
+            param_h += deltatime * 0.2;
+        if (glfwGetKey(window.getWindow(), GLFW_KEY_G) == GLFW_PRESS)
+            param_h -= deltatime * 0.2;
+
+        if (glfwGetKey(window.getWindow(), GLFW_KEY_Y) == GLFW_PRESS)
+            param_mass += deltatime * 0.2;
+        if (glfwGetKey(window.getWindow(), GLFW_KEY_H) == GLFW_PRESS)
+            param_mass -= deltatime * 0.2;
+
+        if (glfwGetKey(window.getWindow(), GLFW_KEY_U) == GLFW_PRESS)
+            param_gasConstant += deltatime * 1500.0;
+        if (glfwGetKey(window.getWindow(), GLFW_KEY_J) == GLFW_PRESS)
+            param_gasConstant -= deltatime * 1500.0;
+
+        if (glfwGetKey(window.getWindow(), GLFW_KEY_I) == GLFW_PRESS)
+            param_offset += deltatime * 400.0;
+        if (glfwGetKey(window.getWindow(), GLFW_KEY_K) == GLFW_PRESS)
+            param_offset -= deltatime * 400.0;
+
+        if (glfwGetKey(window.getWindow(), GLFW_KEY_O) == GLFW_PRESS)
+            param_viscosity = 50;
+        if (glfwGetKey(window.getWindow(), GLFW_KEY_L) == GLFW_PRESS)
+            param_viscosity = 1200;
+        
+
+        auto cmdStream = core.createCommandStream(vkcv::QueueType::Graphics);
+
+        glm::vec4 pushData[3] = {
+            glm::vec4(param_h,param_mass,param_gasConstant,param_offset),
+            glm::vec4(param_gravity,param_viscosity,param_ABSORBTION,param_dt),
+            glm::vec4(gravityDir.x,gravityDir.y,gravityDir.z,(float)numberParticles)
+        };
+
+        std::cout << "h: " << param_h << " | mass: " << param_mass << " | gasConstant: " << param_gasConstant << " | offset: " << param_offset << " | viscosity: " << param_viscosity << std::endl;
+
+        vkcv::PushConstants pushConstantsCompute (sizeof(pushData));
+        pushConstantsCompute.appendDrawcall(pushData);
+
+        uint32_t computeDispatchCount[3] = {static_cast<uint32_t> (std::ceil(numberParticles/256.f)),1,1};
+        
+        core.recordComputeDispatchToCmdStream(cmdStream,
+                                              computePipeline1,
+                                              computeDispatchCount,
+                                              {vkcv::DescriptorSetUsage(0,core.getDescriptorSet(computeDescriptorSet1).vulkanHandle)},
+											  pushConstantsCompute);
+
+        core.recordBufferMemoryBarrier(cmdStream, particleBuffer1.getHandle());
+		core.recordBufferMemoryBarrier(cmdStream, particleBuffer2.getHandle());
+
+		core.recordComputeDispatchToCmdStream(cmdStream,
+											  computePipeline2,
+											  computeDispatchCount,
+											  {vkcv::DescriptorSetUsage(0,core.getDescriptorSet(computeDescriptorSet2).vulkanHandle)},
+											  pushConstantsCompute);
+
+		core.recordBufferMemoryBarrier(cmdStream, particleBuffer1.getHandle());
+		core.recordBufferMemoryBarrier(cmdStream, particleBuffer2.getHandle());
+
+        core.recordComputeDispatchToCmdStream(cmdStream,
+                                              computePipeline3,
+                                              computeDispatchCount,
+                                              { vkcv::DescriptorSetUsage(0,core.getDescriptorSet(computeDescriptorSet3).vulkanHandle) },
+                                              pushConstantsCompute);
+
+        core.recordBufferMemoryBarrier(cmdStream, particleBuffer1.getHandle());
+        core.recordBufferMemoryBarrier(cmdStream, particleBuffer2.getHandle());
+
+        core.recordComputeDispatchToCmdStream(cmdStream,
+                                              computePipeline4,
+                                              computeDispatchCount,
+                                              { vkcv::DescriptorSetUsage(0,core.getDescriptorSet(computeDescriptorSet4).vulkanHandle) },
+                                              pushConstantsCompute);
+
+        core.recordBufferMemoryBarrier(cmdStream, particleBuffer1.getHandle());
+        core.recordBufferMemoryBarrier(cmdStream, particleBuffer2.getHandle());
+
+
+        // bloomAndFlares & tonemapping
+        vkcv::PushConstants pushConstantsDraw (sizeof(renderingMatrices));
+        pushConstantsDraw.appendDrawcall(renderingMatrices);
+        
+        core.recordDrawcallsToCmdStream(
+                cmdStream,
+                particlePass,
+                particlePipeline,
+				pushConstantsDraw,
+                {drawcalls},
+                { colorBuffer },
+                windowHandle);
+
+        bloomAndFlares.execWholePipeline(cmdStream, colorBuffer);
+
+        core.prepareImageForStorage(cmdStream, colorBuffer);
+        core.prepareImageForStorage(cmdStream, swapchainInput);
+
+        vkcv::DescriptorWrites tonemappingDescriptorWrites;
+        tonemappingDescriptorWrites.storageImageWrites = {
+            vkcv::StorageImageDescriptorWrite(0, colorBuffer),
+            vkcv::StorageImageDescriptorWrite(1, swapchainInput)
+        };
+        core.writeDescriptorSet(tonemappingDescriptor, tonemappingDescriptorWrites);
+
+        uint32_t tonemappingDispatchCount[3];
+        tonemappingDispatchCount[0] = std::ceil(swapchainExtent.width / 8.f);
+        tonemappingDispatchCount[1] = std::ceil(swapchainExtent.height / 8.f);
+        tonemappingDispatchCount[2] = 1;
+
+        core.recordComputeDispatchToCmdStream(
+            cmdStream, 
+            tonemappingPipe, 
+            tonemappingDispatchCount, 
+            {vkcv::DescriptorSetUsage(0, core.getDescriptorSet(tonemappingDescriptor).vulkanHandle) },
+            vkcv::PushConstants(0));
+
+        core.prepareSwapchainImageForPresent(cmdStream);
+        core.submitCommandStream(cmdStream);
+        core.endFrame(windowHandle);
+    }
+
+    return 0;
+}
diff --git a/projects/voxelization/assets/shaders/depthToMoments.comp b/projects/voxelization/assets/shaders/depthToMoments.comp
index 5a78d0cb9b748187d12057708fcd0de7658a61ed..79e47cdef02143ed97d53e533f47e822db8c0f6f 100644
--- a/projects/voxelization/assets/shaders/depthToMoments.comp
+++ b/projects/voxelization/assets/shaders/depthToMoments.comp
@@ -28,9 +28,11 @@ void main(){
         z += texelFetch(sampler2DMS(srcTexture, depthSampler), uv, i).r;
     }
     z /= msaaCount;
-    
+    z = 2 * z - 1;	// algorithm expects depth in range [-1:1]
+	
     float   z2                  = z*z;   
     vec4    moments             = vec4(z, z2, z2*z, z2*z2);
     vec4    momentsQuantized    = quantizeMoments(moments);
+	
     imageStore(outImage, uv, momentsQuantized);
 }
\ No newline at end of file
diff --git a/projects/voxelization/assets/shaders/shadowBlur.inc b/projects/voxelization/assets/shaders/shadowBlur.inc
index 06147415f118dca9badd15813b431a68682ce0b0..ed4994ed1ace34afdafff15920d18a2433a3c0a4 100644
--- a/projects/voxelization/assets/shaders/shadowBlur.inc
+++ b/projects/voxelization/assets/shaders/shadowBlur.inc
@@ -3,7 +3,7 @@
 
 vec4 blurMomentShadowMap1D(ivec2 coord, ivec2 blurDirection, texture2D srcTexture, sampler depthSampler){
     
-    int blurRadius  = 9;
+    int blurRadius  = 7;
     int minOffset   = -(blurRadius-1) / 2;
     int maxOffset   = -minOffset;
     
diff --git a/projects/voxelization/assets/shaders/shadowBlurX.comp b/projects/voxelization/assets/shaders/shadowBlurX.comp
index 45b91aad71673347dbf607fecef92463ef1c3c88..41d127fdf5ce46dec883d49af4f284b5787d5d38 100644
--- a/projects/voxelization/assets/shaders/shadowBlurX.comp
+++ b/projects/voxelization/assets/shaders/shadowBlurX.comp
@@ -18,6 +18,6 @@ void main(){
     }
     ivec2 coord = ivec2(gl_GlobalInvocationID.xy);    
     vec4 moments = blurMomentShadowMap1D(coord, ivec2(1, 0), srcTexture, depthSampler);
-    
+    // moments = texelFetch(sampler2D(srcTexture, depthSampler), coord, 0);
     imageStore(outImage, coord, moments);
 }
\ No newline at end of file
diff --git a/projects/voxelization/assets/shaders/shadowBlurY.comp b/projects/voxelization/assets/shaders/shadowBlurY.comp
index 51d4df054b0d99e54149863a5967143518f61dd2..c1710d7d6c75ef0093fecfe708272f56f9541eaf 100644
--- a/projects/voxelization/assets/shaders/shadowBlurY.comp
+++ b/projects/voxelization/assets/shaders/shadowBlurY.comp
@@ -16,10 +16,8 @@ void main(){
     if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(outImage)))){
         return;
     }
-    ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
-    vec2 pixelSize = vec2(1) / textureSize(sampler2D(srcTexture, depthSampler), 0);
-    
+    ivec2 coord = ivec2(gl_GlobalInvocationID.xy);    
     vec4 moments = blurMomentShadowMap1D(coord, ivec2(0, 1), srcTexture, depthSampler);
-    
+    // moments = texelFetch(sampler2D(srcTexture, depthSampler), coord, 0);
     imageStore(outImage, coord, moments);
 }
\ No newline at end of file
diff --git a/projects/voxelization/assets/shaders/shadowMapping.inc b/projects/voxelization/assets/shaders/shadowMapping.inc
index c56ae8985c5c5fcef780b622d8b888f1081af74c..9124a05c310c2cc16e6b02802f5adb36bde42804 100644
--- a/projects/voxelization/assets/shaders/shadowMapping.inc
+++ b/projects/voxelization/assets/shaders/shadowMapping.inc
@@ -6,7 +6,8 @@
 // nice math blob from the moment shadow mapping presentation
 float ComputeMSMShadowIntensity(vec4 _4Moments, float FragmentDepth, float DepthBias, float MomentBias)
 {
-    vec4 b=mix(_4Moments, vec4(0.5),MomentBias);
+    vec4 b=mix(_4Moments, vec4(0, 0.63, 0, 0.63),MomentBias);
+	
     vec3 z;
     z[0]=FragmentDepth-DepthBias;
     float L32D22=fma(-b[0], b[1], b[2]);
@@ -39,24 +40,23 @@ float ComputeMSMShadowIntensity(vec4 _4Moments, float FragmentDepth, float Depth
 }
 
 vec4 quantizeMoments(vec4 moments){
-    mat4 T = mat4(
-        -2.07224649,     13.7948857237,   0.105877704,   9.7924062118,
-         32.23703778,   -59.4683975703, -1.9077466311, -33.7652110555,
-        -68.571074599,   82.0359750338,  9.3496555107,  47.9456096605,
-         39.3703274134, -35.364903257,  -6.6543490743, -23.9728048165);
-    vec4 quantized = T * moments;
-    quantized[0] += 0.0359558848;
-    return quantized;
+    vec4 quantized;
+	quantized.r = 1.5 * moments.r - 2 * moments.b + 0.5;
+	quantized.g = 4   * moments.g - 4 * moments.a;
+	quantized.b = sqrt(3)/2 * moments.r - sqrt(12)/9 * moments.b + 0.5;
+	quantized.a = 0.5 * moments.g + 0.5 * moments.a;
+	
+	return quantized;
 }
 
 vec4 unquantizeMoments(vec4 moments){
-    moments[0] -= 0.0359558848;
-    mat4 T = mat4(
-        0.2227744146,  0.1549679261,  0.1451988946,  0.163127443,
-        0.0771972861,  0.1394629426,  0.2120202157,  0.2591432266,
-        0.7926986636,  0.7963415838,  0.7258694464,  0.6539092497,
-        0.0319417555,  -0.1722823173, -0.2758014811, -0.3376131734);
-    return T * moments;
+    moments -= vec4(0.5, 0, 0.5, 0);
+	vec4 unquantized;
+	unquantized.r = -1.f / 3 * moments.r + sqrt(3) * moments.b;
+	unquantized.g = 0.125 * moments.g + moments.a;
+	unquantized.b = -0.75 * moments.r + 0.75 * sqrt(3) * moments.b;
+	unquantized.a = -0.125 * moments.g + moments.a;
+	return unquantized / 0.98;	// division reduces light bleeding
 }
 
 float rescaleRange(float a, float b, float v)
@@ -78,18 +78,20 @@ float shadowTest(vec3 worldPos, LightInfo lightInfo, texture2D shadowMap, sample
     if(any(lessThan(lightPos.xy, vec2(0))) || any(greaterThan(lightPos.xy, vec2(1)))){
         return 1;
     }
-    
+	
     lightPos.z = clamp(lightPos.z, 0, 1);
+	lightPos.z = 2 * lightPos.z - 1;	// algorithm expects depth in range [-1:1]
 
     vec4 shadowMapSample = texture(sampler2D(shadowMap, shadowMapSampler), lightPos.xy);
     
     shadowMapSample = unquantizeMoments(shadowMapSample);
     
     float depthBias     = 0.f;
-    float momentBias    = 0.0003;
+    float momentBias    = 0.0006;
     
     float shadow = ComputeMSMShadowIntensity(shadowMapSample, lightPos.z, depthBias, momentBias);
-    return reduceLightBleeding(shadow, 0.1f);
+	return clamp(shadow, 0, 1);
+    // return reduceLightBleeding(shadow, 0.1f);
 }
 
 #endif // #ifndef SHADOW_MAPPING_INC
\ No newline at end of file
diff --git a/projects/voxelization/src/BloomAndFlares.cpp b/projects/voxelization/src/BloomAndFlares.cpp
index d47f61d0dc7fea4e38508b7b1d6c040595e2944a..2014d7a0219141ec6363b38a5311cb924b6b6a45 100644
--- a/projects/voxelization/src/BloomAndFlares.cpp
+++ b/projects/voxelization/src/BloomAndFlares.cpp
@@ -127,6 +127,8 @@ void BloomAndFlares::execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStre
             1
     };
 
+	p_Core->recordBeginDebugLabel(cmdStream, "Bloom downsample", { 1, 1, 1, 1 });
+
     // downsample dispatch of original color attachment
     p_Core->prepareImageForSampling(cmdStream, colorAttachment);
     p_Core->prepareImageForStorage(cmdStream, m_Blur.getHandle());
@@ -182,10 +184,14 @@ void BloomAndFlares::execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStre
         // image barrier between mips
         p_Core->recordImageMemoryBarrier(cmdStream, m_Blur.getHandle());
     }
+
+    p_Core->recordEndDebugLabel(cmdStream);
 }
 
 void BloomAndFlares::execUpsamplePipe(const vkcv::CommandStreamHandle &cmdStream)
 {
+    p_Core->recordBeginDebugLabel(cmdStream, "Bloom upsample", { 1, 1, 1, 1 });
+
     // upsample dispatch
     p_Core->prepareImageForStorage(cmdStream, m_Blur.getHandle());
 
@@ -227,10 +233,14 @@ void BloomAndFlares::execUpsamplePipe(const vkcv::CommandStreamHandle &cmdStream
         // image barrier between mips
         p_Core->recordImageMemoryBarrier(cmdStream, m_Blur.getHandle());
     }
+
+    p_Core->recordEndDebugLabel(cmdStream);
 }
 
 void BloomAndFlares::execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStream)
 {
+    p_Core->recordBeginDebugLabel(cmdStream, "Lense flare generation", { 1, 1, 1, 1 });
+
     // lens feature generation descriptor writes
     p_Core->prepareImageForSampling(cmdStream, m_Blur.getHandle());
     p_Core->prepareImageForStorage(cmdStream, m_LensFeatures.getHandle());
@@ -295,11 +305,15 @@ void BloomAndFlares::execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStr
         // image barrier between mips
         p_Core->recordImageMemoryBarrier(cmdStream, m_LensFeatures.getHandle());
     }
+
+    p_Core->recordEndDebugLabel(cmdStream);
 }
 
 void BloomAndFlares::execCompositePipe(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle& colorAttachment,
     const uint32_t attachmentWidth, const uint32_t attachmentHeight, const glm::vec3& cameraForward)
 {
+    p_Core->recordBeginDebugLabel(cmdStream, "Bloom/lense flare composition", { 1, 1, 1, 1 });
+
     p_Core->prepareImageForSampling(cmdStream, m_Blur.getHandle());
     p_Core->prepareImageForSampling(cmdStream, m_LensFeatures.getHandle());
     p_Core->prepareImageForStorage(cmdStream, colorAttachment);
@@ -329,11 +343,13 @@ void BloomAndFlares::execCompositePipe(const vkcv::CommandStreamHandle &cmdStrea
 
     // bloom composite dispatch
     p_Core->recordComputeDispatchToCmdStream(
-            cmdStream,
-            m_CompositePipe,
-            compositeDispatchCount,
-            {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_CompositeDescSet).vulkanHandle)},
-			pushConstants);
+        cmdStream,
+        m_CompositePipe,
+        compositeDispatchCount,
+        {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_CompositeDescSet).vulkanHandle)},
+        pushConstants);
+
+    p_Core->recordEndDebugLabel(cmdStream);
 }
 
 void BloomAndFlares::execWholePipeline(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment, 
diff --git a/projects/voxelization/src/ShadowMapping.cpp b/projects/voxelization/src/ShadowMapping.cpp
index d8041c1e9935d148ded8fb1ca8f0e4d8b79fce71..ce4261ff2403139d10b9d677e7aa216a3e41178f 100644
--- a/projects/voxelization/src/ShadowMapping.cpp
+++ b/projects/voxelization/src/ShadowMapping.cpp
@@ -266,6 +266,7 @@ void ShadowMapping::recordShadowMapRendering(
 		drawcalls.push_back(vkcv::DrawcallInfo(mesh, {}));
 	}
 
+	m_corePtr->recordBeginDebugLabel(cmdStream, "Shadow map depth", {1, 1, 1, 1});
 	m_corePtr->recordDrawcallsToCmdStream(
 		cmdStream,
 		m_shadowMapPass,
@@ -275,6 +276,7 @@ void ShadowMapping::recordShadowMapRendering(
 		{ m_shadowMapDepth.getHandle() },
 		windowHandle);
 	m_corePtr->prepareImageForSampling(cmdStream, m_shadowMapDepth.getHandle());
+	m_corePtr->recordEndDebugLabel(cmdStream);
 
 	// depth to moments
 	uint32_t dispatchCount[3];
@@ -287,6 +289,8 @@ void ShadowMapping::recordShadowMapRendering(
 	vkcv::PushConstants msaaPushConstants (sizeof(msaaSampleCount));
 	msaaPushConstants.appendDrawcall(msaaSampleCount);
 
+	m_corePtr->recordBeginDebugLabel(cmdStream, "Depth to moments", { 1, 1, 1, 1 });
+
 	m_corePtr->prepareImageForStorage(cmdStream, m_shadowMap.getHandle());
 	m_corePtr->recordComputeDispatchToCmdStream(
 		cmdStream,
@@ -295,6 +299,9 @@ void ShadowMapping::recordShadowMapRendering(
 		{ vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_depthToMomentsDescriptorSet).vulkanHandle) },
 		msaaPushConstants);
 	m_corePtr->prepareImageForSampling(cmdStream, m_shadowMap.getHandle());
+	m_corePtr->recordEndDebugLabel(cmdStream);
+
+	m_corePtr->recordBeginDebugLabel(cmdStream, "Moment shadow map blur", { 1, 1, 1, 1 });
 
 	// blur X
 	m_corePtr->prepareImageForStorage(cmdStream, m_shadowMapIntermediate.getHandle());
@@ -316,6 +323,8 @@ void ShadowMapping::recordShadowMapRendering(
 		vkcv::PushConstants(0));
 	m_shadowMap.recordMipChainGeneration(cmdStream);
 	m_corePtr->prepareImageForSampling(cmdStream, m_shadowMap.getHandle());
+
+	m_corePtr->recordEndDebugLabel(cmdStream);
 }
 
 vkcv::ImageHandle ShadowMapping::getShadowMap() {
diff --git a/projects/voxelization/src/Voxelization.cpp b/projects/voxelization/src/Voxelization.cpp
index 3cbff0df84757fb370a0372ddd45a9df401d4b60..c023af21c673651984e945ab03d16280c18f0768 100644
--- a/projects/voxelization/src/Voxelization.cpp
+++ b/projects/voxelization/src/Voxelization.cpp
@@ -251,6 +251,7 @@ void Voxelization::voxelizeMeshes(
 	vkcv::PushConstants voxelCountPushConstants (sizeof(voxelCount));
 	voxelCountPushConstants.appendDrawcall(voxelCount);
 
+	m_corePtr->recordBeginDebugLabel(cmdStream, "Voxel reset", { 1, 1, 1, 1 });
 	m_corePtr->recordComputeDispatchToCmdStream(
 		cmdStream,
 		m_voxelResetPipe,
@@ -258,6 +259,7 @@ void Voxelization::voxelizeMeshes(
 		{ vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_voxelResetDescriptorSet).vulkanHandle) },
 		voxelCountPushConstants);
 	m_corePtr->recordBufferMemoryBarrier(cmdStream, m_voxelBuffer.getHandle());
+	m_corePtr->recordEndDebugLabel(cmdStream);
 
 	// voxelization
 	std::vector<vkcv::DrawcallInfo> drawcalls;
@@ -270,6 +272,7 @@ void Voxelization::voxelizeMeshes(
 			},1));
 	}
 
+	m_corePtr->recordBeginDebugLabel(cmdStream, "Voxelization", { 1, 1, 1, 1 });
 	m_corePtr->prepareImageForStorage(cmdStream, m_voxelImageIntermediate.getHandle());
 	m_corePtr->recordDrawcallsToCmdStream(
 		cmdStream,
@@ -279,6 +282,7 @@ void Voxelization::voxelizeMeshes(
 		drawcalls,
 		{ m_dummyRenderTarget.getHandle() },
 		windowHandle);
+	m_corePtr->recordEndDebugLabel(cmdStream);
 
 	// buffer to image
 	const uint32_t bufferToImageGroupSize[3] = { 4, 4, 4 };
@@ -287,6 +291,7 @@ void Voxelization::voxelizeMeshes(
 		bufferToImageDispatchCount[i] = glm::ceil(voxelResolution / float(bufferToImageGroupSize[i]));
 	}
 
+	m_corePtr->recordBeginDebugLabel(cmdStream, "Voxel buffer to image", { 1, 1, 1, 1 });
 	m_corePtr->recordComputeDispatchToCmdStream(
 		cmdStream,
 		m_bufferToImagePipe,
@@ -295,14 +300,17 @@ void Voxelization::voxelizeMeshes(
 		vkcv::PushConstants(0));
 
 	m_corePtr->recordImageMemoryBarrier(cmdStream, m_voxelImageIntermediate.getHandle());
+	m_corePtr->recordEndDebugLabel(cmdStream);
 
 	// intermediate image mipchain
+	m_corePtr->recordBeginDebugLabel(cmdStream, "Intermediate Voxel mipmap generation", { 1, 1, 1, 1 });
 	m_voxelImageIntermediate.recordMipChainGeneration(cmdStream);
 	m_corePtr->prepareImageForSampling(cmdStream, m_voxelImageIntermediate.getHandle());
+	m_corePtr->recordEndDebugLabel(cmdStream);
 
 	// secondary bounce
+	m_corePtr->recordBeginDebugLabel(cmdStream, "Voxel secondary bounce", { 1, 1, 1, 1 });
 	m_corePtr->prepareImageForStorage(cmdStream, m_voxelImage.getHandle());
-
 	m_corePtr->recordComputeDispatchToCmdStream(
 		cmdStream,
 		m_secondaryBouncePipe,
@@ -310,12 +318,14 @@ void Voxelization::voxelizeMeshes(
 		{ vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_secondaryBounceDescriptorSet).vulkanHandle) },
 		vkcv::PushConstants(0));
 	m_voxelImage.recordMipChainGeneration(cmdStream);
-
 	m_corePtr->recordImageMemoryBarrier(cmdStream, m_voxelImage.getHandle());
+	m_corePtr->recordEndDebugLabel(cmdStream);
 
 	// final image mipchain
+	m_corePtr->recordBeginDebugLabel(cmdStream, "Voxel mipmap generation", { 1, 1, 1, 1 });
 	m_voxelImage.recordMipChainGeneration(cmdStream);
 	m_corePtr->prepareImageForSampling(cmdStream, m_voxelImage.getHandle());
+	m_corePtr->recordEndDebugLabel(cmdStream);
 }
 
 void Voxelization::renderVoxelVisualisation(
@@ -344,6 +354,7 @@ void Voxelization::renderVoxelVisualisation(
 		vkcv::Mesh({}, nullptr, drawVoxelCount),
 		{ vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_visualisationDescriptorSet).vulkanHandle) },1);
 
+	m_corePtr->recordBeginDebugLabel(cmdStream, "Voxel visualisation", { 1, 1, 1, 1 });
 	m_corePtr->prepareImageForStorage(cmdStream, m_voxelImage.getHandle());
 	m_corePtr->recordDrawcallsToCmdStream(
 		cmdStream,
@@ -353,6 +364,7 @@ void Voxelization::renderVoxelVisualisation(
 		{ drawcall },
 		renderTargets,
 		windowHandle);
+	m_corePtr->recordEndDebugLabel(cmdStream);
 }
 
 void Voxelization::updateVoxelOffset(const vkcv::camera::Camera& camera) {
diff --git a/projects/voxelization/src/main.cpp b/projects/voxelization/src/main.cpp
index 763856cbe7b8b51c9d15e41a8170a2afe72c107d..a4ffb668e74d0a0829bb3c436ed5b992695b6ecf 100644
--- a/projects/voxelization/src/main.cpp
+++ b/projects/voxelization/src/main.cpp
@@ -776,6 +776,7 @@ int main(int argc, const char** argv) {
 		
 		const std::vector<vkcv::ImageHandle>    prepassRenderTargets = { depthBuffer };
 
+		core.recordBeginDebugLabel(cmdStream, "Depth prepass", { 1, 1, 1, 1 });
 		core.recordDrawcallsToCmdStream(
 			cmdStream,
 			prepassPass,
@@ -786,6 +787,7 @@ int main(int argc, const char** argv) {
 			windowHandle);
 
 		core.recordImageMemoryBarrier(cmdStream, depthBuffer);
+		core.recordEndDebugLabel(cmdStream);
 		
 		vkcv::PushConstants pushConstants (2 * sizeof(glm::mat4));
 		
@@ -802,6 +804,7 @@ int main(int argc, const char** argv) {
 		
 		const std::vector<vkcv::ImageHandle>    renderTargets = { colorBuffer, depthBuffer };
 
+		core.recordBeginDebugLabel(cmdStream, "Forward rendering", { 1, 1, 1, 1 });
 		core.recordDrawcallsToCmdStream(
 			cmdStream,
 			forwardPass,
@@ -810,6 +813,7 @@ int main(int argc, const char** argv) {
 			drawcalls,
 			renderTargets,
 			windowHandle);
+		core.recordEndDebugLabel(cmdStream);
 
 		if (renderVoxelVis) {
 			voxelization.renderVoxelVisualisation(cmdStream, viewProjectionCamera, renderTargets, voxelVisualisationMip, windowHandle);
@@ -819,6 +823,7 @@ int main(int argc, const char** argv) {
 		skySettingsPushConstants.appendDrawcall(skySettings);
 
 		// sky
+		core.recordBeginDebugLabel(cmdStream, "Sky", { 1, 1, 1, 1 });
 		core.recordDrawcallsToCmdStream(
 			cmdStream,
 			skyPass,
@@ -827,6 +832,7 @@ int main(int argc, const char** argv) {
 			{ vkcv::DrawcallInfo(vkcv::Mesh({}, nullptr, 3), {}) },
 			renderTargets,
 			windowHandle);
+		core.recordEndDebugLabel(cmdStream);
 
 		const uint32_t fullscreenLocalGroupSize = 8;
 		
@@ -843,8 +849,8 @@ int main(int argc, const char** argv) {
 		fulsscreenDispatchCount[2] = 1;
 
 		if (usingMsaa) {
+			core.recordBeginDebugLabel(cmdStream, "MSAA resolve", { 1, 1, 1, 1 });
 			if (msaaCustomResolve) {
-
 				core.prepareImageForSampling(cmdStream, colorBuffer);
 				core.prepareImageForStorage(cmdStream, resolvedColorBuffer);
 
@@ -861,6 +867,7 @@ int main(int argc, const char** argv) {
 			else {
 				core.resolveMSAAImage(cmdStream, colorBuffer, resolvedColorBuffer);
 			}
+			core.recordEndDebugLabel(cmdStream);
 		}
 
 		bloomFlares.execWholePipeline(cmdStream, resolvedColorBuffer, fsrWidth, fsrHeight,
@@ -870,6 +877,7 @@ int main(int argc, const char** argv) {
 		core.prepareImageForStorage(cmdStream, swapBuffer);
 		core.prepareImageForSampling(cmdStream, resolvedColorBuffer);
 		
+		core.recordBeginDebugLabel(cmdStream, "Tonemapping", { 1, 1, 1, 1 });
 		core.recordComputeDispatchToCmdStream(
 			cmdStream, 
 			tonemappingPipeline, 
@@ -882,6 +890,7 @@ int main(int argc, const char** argv) {
 		
 		core.prepareImageForStorage(cmdStream, swapBuffer2);
 		core.prepareImageForSampling(cmdStream, swapBuffer);
+		core.recordEndDebugLabel(cmdStream);
 		
 		if (bilinearUpscaling) {
 			upscaling1.recordUpscaling(cmdStream, swapBuffer, swapBuffer2);
@@ -906,6 +915,7 @@ int main(int argc, const char** argv) {
 				glm::ceil(swapchainHeight / static_cast<float>(fullscreenLocalGroupSize))
 		);
 		
+		core.recordBeginDebugLabel(cmdStream, "Post Processing", { 1, 1, 1, 1 });
 		core.recordComputeDispatchToCmdStream(
 				cmdStream,
 				postEffectsPipeline,
@@ -915,6 +925,7 @@ int main(int argc, const char** argv) {
 				).vulkanHandle) },
 				timePushConstants
 		);
+		core.recordEndDebugLabel(cmdStream);
 
 		// present and end
 		core.prepareSwapchainImageForPresent(cmdStream);
diff --git a/src/vkcv/BufferManager.cpp b/src/vkcv/BufferManager.cpp
index f468bccc68c7b79391f1132160b7237a51f64081..9bb9e67166013a8d1c2f7eb22a2ea867ddc9da32 100644
--- a/src/vkcv/BufferManager.cpp
+++ b/src/vkcv/BufferManager.cpp
@@ -328,8 +328,8 @@ namespace vkcv {
 			buffer.m_size);
 
 		cmdBuffer.pipelineBarrier(
-			vk::PipelineStageFlagBits::eTopOfPipe,
-			vk::PipelineStageFlagBits::eBottomOfPipe,
+			vk::PipelineStageFlagBits::eAllCommands,
+			vk::PipelineStageFlagBits::eAllCommands,
 			{},
 			nullptr,
 			memoryBarrier,
diff --git a/src/vkcv/Core.cpp b/src/vkcv/Core.cpp
index de01b590c13e4941bd99619de05f2da144d6a2ec..efba11ec2a95d8b08c9c9ddc646ec471f4f7adce 100644
--- a/src/vkcv/Core.cpp
+++ b/src/vkcv/Core.cpp
@@ -52,7 +52,7 @@ namespace vkcv
     Core::Core(Context &&context, const CommandResources& commandResources, const SyncResources& syncResources) noexcept :
             m_Context(std::move(context)),
             m_PassManager{std::make_unique<PassManager>(m_Context.m_Device)},
-            m_PipelineManager{std::make_unique<GraphicsPipelineManager>(m_Context.m_Device)},
+            m_PipelineManager{std::make_unique<GraphicsPipelineManager>(m_Context.m_Device, m_Context.m_PhysicalDevice)},
             m_ComputePipelineManager{std::make_unique<ComputePipelineManager>(m_Context.m_Device)},
             m_DescriptorManager(std::make_unique<DescriptorManager>(m_Context.m_Device)),
             m_BufferManager{std::unique_ptr<BufferManager>(new BufferManager())},
@@ -487,7 +487,7 @@ namespace vkcv
 	void Core::recordBeginDebugLabel(const CommandStreamHandle &cmdStream,
 									 const std::string& label,
 									 const std::array<float, 4>& color) {
-#ifndef NDEBUG
+	#ifdef VULKAN_DEBUG_LABELS
 		static PFN_vkCmdBeginDebugUtilsLabelEXT beginDebugLabel = reinterpret_cast<PFN_vkCmdBeginDebugUtilsLabelEXT>(
 				m_Context.getDevice().getProcAddr("vkCmdBeginDebugUtilsLabelEXT")
 		);
@@ -506,11 +506,11 @@ namespace vkcv
 		};
 
 		recordCommandsToStream(cmdStream, submitFunction, nullptr);
-#endif
+	#endif
 	}
 	
 	void Core::recordEndDebugLabel(const CommandStreamHandle &cmdStream) {
-#ifndef NDEBUG
+	#ifdef VULKAN_DEBUG_LABELS
 		static PFN_vkCmdEndDebugUtilsLabelEXT endDebugLabel = reinterpret_cast<PFN_vkCmdEndDebugUtilsLabelEXT>(
 				m_Context.getDevice().getProcAddr("vkCmdEndDebugUtilsLabelEXT")
 		);
@@ -524,7 +524,7 @@ namespace vkcv
 		};
 
 		recordCommandsToStream(cmdStream, submitFunction, nullptr);
-#endif
+	#endif
 	}
 	
 	void Core::recordComputeIndirectDispatchToCmdStream(
@@ -781,6 +781,14 @@ namespace vkcv
 		}, nullptr);
 	}
 
+	void Core::prepareImageForAttachmentManually(const vk::CommandBuffer& cmdBuffer, const ImageHandle& image) {
+		transitionRendertargetsToAttachmentLayout({ image }, *m_ImageManager, cmdBuffer);
+	}
+
+	void Core::updateImageLayoutManual(const vkcv::ImageHandle& image, const vk::ImageLayout layout) {
+		m_ImageManager->updateImageLayoutManual(image, layout);
+	}
+
 	void Core::recordImageMemoryBarrier(const CommandStreamHandle& cmdStream, const ImageHandle& image) {
 		recordCommandsToStream(cmdStream, [image, this](const vk::CommandBuffer cmdBuffer) {
 			m_ImageManager->recordImageMemoryBarrier(image, cmdBuffer);
@@ -900,7 +908,7 @@ namespace vkcv
 	
 	static void setDebugObjectLabel(const vk::Device& device, const vk::ObjectType& type,
 									uint64_t handle, const std::string& label) {
-#ifndef NDEBUG
+#ifndef VULKAN_DEBUG_LABELS
 		static PFN_vkSetDebugUtilsObjectNameEXT setDebugLabel = reinterpret_cast<PFN_vkSetDebugUtilsObjectNameEXT>(
 				device.getProcAddr("vkSetDebugUtilsObjectNameEXT")
 		);
diff --git a/src/vkcv/DescriptorManager.cpp b/src/vkcv/DescriptorManager.cpp
index 69a35685098509afcbfd49210b2b09b454e9bbb9..7480289b1f2a0b1db194db3a06c52f7050888afe 100644
--- a/src/vkcv/DescriptorManager.cpp
+++ b/src/vkcv/DescriptorManager.cpp
@@ -80,7 +80,7 @@ namespace vkcv
         }
 
         //create the descriptor set's layout from the binding data gathered above
-        vk::DescriptorSetLayout vulkanHandle = nullptr;
+        vk::DescriptorSetLayout vulkanHandle;
         vk::DescriptorSetLayoutCreateInfo layoutInfo({}, bindingsVector);
         auto result = m_Device.createDescriptorSetLayout(&layoutInfo, nullptr, &vulkanHandle);
         if (result != vk::Result::eSuccess) {
@@ -97,7 +97,7 @@ namespace vkcv
     {
         //create and allocate the set based on the layout provided
         DescriptorSetLayout setLayout = m_DescriptorSetLayouts[setLayoutHandle.getId()];
-        vk::DescriptorSet vulkanHandle = nullptr;
+        vk::DescriptorSet vulkanHandle;
         vk::DescriptorSetAllocateInfo allocInfo(m_Pools.back(), 1, &setLayout.vulkanHandle);
         auto result = m_Device.allocateDescriptorSets(&allocInfo, &vulkanHandle);
         if(result != vk::Result::eSuccess)
diff --git a/src/vkcv/GraphicsPipelineManager.cpp b/src/vkcv/GraphicsPipelineManager.cpp
index cb7dd31dddd3a5c0742f95e8429175724aa26454..870220f45b52c022f7c2d445a40e99ab4a6a4a2f 100644
--- a/src/vkcv/GraphicsPipelineManager.cpp
+++ b/src/vkcv/GraphicsPipelineManager.cpp
@@ -5,8 +5,9 @@
 namespace vkcv
 {
 	
-	GraphicsPipelineManager::GraphicsPipelineManager(vk::Device device) noexcept :
-            m_Device{device},
+	GraphicsPipelineManager::GraphicsPipelineManager(vk::Device device, vk::PhysicalDevice physicalDevice) noexcept :
+            m_Device(device),
+            m_physicalDevice(physicalDevice),
             m_Pipelines{}
     {}
 	
@@ -237,7 +238,9 @@ namespace vkcv
 	 * @param config sets Depth Clamping and Culling Mode
 	 * @return Pipeline Rasterization State Create Info Struct
 	 */
-	vk::PipelineRasterizationStateCreateInfo createPipelineRasterizationStateCreateInfo(const GraphicsPipelineConfig &config) {
+	vk::PipelineRasterizationStateCreateInfo createPipelineRasterizationStateCreateInfo(
+		const GraphicsPipelineConfig &config,
+		const vk::PhysicalDeviceConservativeRasterizationPropertiesEXT& conservativeRasterProperties) {
 		vk::CullModeFlags cullMode;
 		switch (config.m_culling) {
 			case CullMode::None:
@@ -267,14 +270,14 @@ namespace vkcv
 				0.f,
 				1.f
 		);
-		
+
 		static vk::PipelineRasterizationConservativeStateCreateInfoEXT conservativeRasterization;
 		
 		if (config.m_UseConservativeRasterization) {
 			conservativeRasterization = vk::PipelineRasterizationConservativeStateCreateInfoEXT(
 					{},
 					vk::ConservativeRasterizationModeEXT::eOverestimate,
-					0.f
+					std::max(1 - conservativeRasterProperties.primitiveOverestimationSize, 0.f)
 			);
 			
 			pipelineRasterizationStateCreateInfo.pNext = &conservativeRasterization;
@@ -544,8 +547,13 @@ namespace vkcv
                 createPipelineViewportStateCreateInfo(config);
 
         // rasterization state
+        vk::PhysicalDeviceConservativeRasterizationPropertiesEXT    conservativeRasterProperties;
+        vk::PhysicalDeviceProperties                                deviceProperties;
+        vk::PhysicalDeviceProperties2                               deviceProperties2(deviceProperties);
+        deviceProperties2.pNext = &conservativeRasterProperties;
+        m_physicalDevice.getProperties2(&deviceProperties2);
         vk::PipelineRasterizationStateCreateInfo pipelineRasterizationStateCreateInfo =
-                createPipelineRasterizationStateCreateInfo(config);
+                createPipelineRasterizationStateCreateInfo(config, conservativeRasterProperties);
 
         // multisample state
         vk::PipelineMultisampleStateCreateInfo pipelineMultisampleStateCreateInfo =
diff --git a/src/vkcv/GraphicsPipelineManager.hpp b/src/vkcv/GraphicsPipelineManager.hpp
index a08e64939dc967511095f22862c52de05148d7e9..782603ab0e1ffa9bde05fda96c5d2d259eff1953 100644
--- a/src/vkcv/GraphicsPipelineManager.hpp
+++ b/src/vkcv/GraphicsPipelineManager.hpp
@@ -20,7 +20,7 @@ namespace vkcv
     {
     public:
 		GraphicsPipelineManager() = delete; // no default ctor
-        explicit GraphicsPipelineManager(vk::Device device) noexcept; // ctor
+        explicit GraphicsPipelineManager(vk::Device device, vk::PhysicalDevice physicalDevice) noexcept; // ctor
         ~GraphicsPipelineManager() noexcept; // dtor
 	
 		GraphicsPipelineManager(const GraphicsPipelineManager &other) = delete; // copy-ctor
@@ -71,8 +71,9 @@ namespace vkcv
 			GraphicsPipelineConfig m_config;
         };
 
-        vk::Device m_Device;
-        std::vector<GraphicsPipeline> m_Pipelines;
+        vk::Device                      m_Device;
+        vk::PhysicalDevice              m_physicalDevice; // needed to get infos to configure conservative rasterization
+        std::vector<GraphicsPipeline>   m_Pipelines;
 
         void destroyPipelineById(uint64_t id);
 
diff --git a/src/vkcv/ImageLayoutTransitions.cpp b/src/vkcv/ImageLayoutTransitions.cpp
index 8d31c64ccbcbf33e259714f8c581c920738190b4..14b226847a15b5e9cadffc28555e76b88b61e6a3 100644
--- a/src/vkcv/ImageLayoutTransitions.cpp
+++ b/src/vkcv/ImageLayoutTransitions.cpp
@@ -58,8 +58,8 @@ namespace vkcv {
 
 	void recordImageBarrier(vk::CommandBuffer cmdBuffer, vk::ImageMemoryBarrier barrier) {
 		cmdBuffer.pipelineBarrier(
-			vk::PipelineStageFlagBits::eTopOfPipe,
-			vk::PipelineStageFlagBits::eBottomOfPipe,
+			vk::PipelineStageFlagBits::eAllCommands,
+			vk::PipelineStageFlagBits::eAllCommands,
 			{},
 			nullptr,
 			nullptr,
diff --git a/src/vkcv/ImageManager.cpp b/src/vkcv/ImageManager.cpp
index 4ddd7f8c44c6023a80831bc8b4b092692e84ec86..bde020498e19e3f9bf0667c7182ca13d11f9044f 100644
--- a/src/vkcv/ImageManager.cpp
+++ b/src/vkcv/ImageManager.cpp
@@ -685,4 +685,20 @@ namespace vkcv {
 		}
 	}
 
+	void ImageManager::updateImageLayoutManual(const vkcv::ImageHandle& handle, const vk::ImageLayout layout) {
+		const uint64_t id = handle.getId();
+
+		if (handle.isSwapchainImage()) {
+			m_swapchainImages[m_currentSwapchainInputImage].m_layout = layout;
+		}
+		else {
+			if (id >= m_images.size()) {
+				vkcv_log(LogLevel::ERROR, "Invalid handle");
+				return;
+			}
+			m_swapchainImages[id].m_layout = layout;
+		}
+		
+	}
+
 }
\ No newline at end of file
diff --git a/src/vkcv/ImageManager.hpp b/src/vkcv/ImageManager.hpp
index 4d99422118e8d464ea75d9f013b471f3dd40fd8c..124508d5e36711a13ad49ae289b9de98600e0d6e 100644
--- a/src/vkcv/ImageManager.hpp
+++ b/src/vkcv/ImageManager.hpp
@@ -120,5 +120,9 @@ namespace vkcv {
 		void setSwapchainImages(const std::vector<vk::Image>& images, const std::vector<vk::ImageView>& views,
 								uint32_t width, uint32_t height, vk::Format format);
 
+		// if manual vulkan work, e.g. ImGui integration, changes an image layout this function must be used
+		// to update the internal image state
+		void updateImageLayoutManual(const vkcv::ImageHandle& handle, const vk::ImageLayout layout);
+
 	};
 }
\ No newline at end of file