diff --git a/include/vkcv/DrawcallRecording.hpp b/include/vkcv/DrawcallRecording.hpp
index 0929ad038fb95ec1573e7c76e5ce13adb84ab760..9f162a499a38d5633703f70eec8a8682e3328d72 100644
--- a/include/vkcv/DrawcallRecording.hpp
+++ b/include/vkcv/DrawcallRecording.hpp
@@ -37,11 +37,12 @@ namespace vkcv {
     };
 
     struct DrawcallInfo {
-        inline DrawcallInfo(const Mesh& mesh, const std::vector<DescriptorSetUsage>& descriptorSets)
-            : mesh(mesh), descriptorSets(descriptorSets) {}
+        inline DrawcallInfo(const Mesh& mesh, const std::vector<DescriptorSetUsage>& descriptorSets, const uint32_t instanceCount = 1)
+            : mesh(mesh), descriptorSets(descriptorSets), instanceCount(instanceCount){}
 
         Mesh                            mesh;
         std::vector<DescriptorSetUsage> descriptorSets;
+        uint32_t                        instanceCount;
     };
 
     void recordDrawcall(
diff --git a/include/vkcv/PipelineConfig.hpp b/include/vkcv/PipelineConfig.hpp
index b1dd56d5ddb29d6f986bfa490d3cb7bdfb37c4ca..5e6dbaa3306f8d2aa6fc44d7dd1fadd9b79be3b4 100644
--- a/include/vkcv/PipelineConfig.hpp
+++ b/include/vkcv/PipelineConfig.hpp
@@ -18,16 +18,21 @@ namespace vkcv {
 	enum class CullMode{ None, Front, Back };
     enum class DepthTest { None, Less, LessEqual, Greater, GreatherEqual, Equal };
 
+    // add more as needed
+    // alternatively we could expose the blend factors directly
+    enum class BlendMode{ None, Additive };
+
     struct PipelineConfig {
-        ShaderProgram                           m_ShaderProgram;
-        uint32_t                                m_Width;
-		uint32_t                                m_Height;
-        PassHandle                              m_PassHandle;
-        VertexLayout                            m_VertexLayout;
-        std::vector<vk::DescriptorSetLayout>    m_DescriptorLayouts;
-        bool                                    m_UseDynamicViewport;
-        bool                                    m_UseConservativeRasterization  = false;
-        PrimitiveTopology                       m_PrimitiveTopology             = PrimitiveTopology::TriangleList;
+        ShaderProgram                         	m_ShaderProgram;
+        uint32_t                              	m_Width;
+		uint32_t                              	m_Height;
+        PassHandle                            	m_PassHandle;
+        VertexLayout                          	m_VertexLayout;
+        std::vector<vk::DescriptorSetLayout>  	m_DescriptorLayouts;
+        bool                                  	m_UseDynamicViewport;
+        bool                                  	m_UseConservativeRasterization 	= false;
+        PrimitiveTopology                     	m_PrimitiveTopology 			= PrimitiveTopology::TriangleList;
+		BlendMode                             	m_blendMode 					= BlendMode::None;
         bool                                    m_EnableDepthClamping           = false;
         Multisampling                           m_multisampling                 = Multisampling::None;
         CullMode                                m_culling                       = CullMode::None;
diff --git a/modules/camera/include/vkcv/camera/Camera.hpp b/modules/camera/include/vkcv/camera/Camera.hpp
index ce32d3f8a0c6ee3e0dd882f24a9ac2d12c14a024..9d85df7dce6d043630fd9d39287cace8530dbd6a 100644
--- a/modules/camera/include/vkcv/camera/Camera.hpp
+++ b/modules/camera/include/vkcv/camera/Camera.hpp
@@ -75,7 +75,7 @@ namespace vkcv::camera {
          * @brief Gets the current projection of the camera
          * @return The current projection matrix
          */
-        const glm::mat4& getProjection() const;
+        glm::mat4 getProjection() const;
 
         /**
          * @brief Gets the model-view-projection matrix of the camera with y-axis-correction applied
diff --git a/modules/camera/src/vkcv/camera/Camera.cpp b/modules/camera/src/vkcv/camera/Camera.cpp
index 18bf94463a0e2c4cb7d64526f4c30835cb451eb2..3541b1a5bc1253c6b0f2b044d757341855a5e900 100644
--- a/modules/camera/src/vkcv/camera/Camera.cpp
+++ b/modules/camera/src/vkcv/camera/Camera.cpp
@@ -37,22 +37,22 @@ namespace vkcv::camera {
 		m_view = view;
 	}
 
-    const glm::mat4& Camera::getProjection() const {
-        return m_projection;
+    const glm::mat4 y_correction(
+        1.0f, 0.0f, 0.0f, 0.0f,
+        0.0f, -1.0f, 0.0f, 0.0f,
+        0.0f, 0.0f, 1.0f, 0.0f,
+        0.0f, 0.0f, 0.0f, 1.0f
+    );
+
+    glm::mat4 Camera::getProjection() const {
+        return y_correction * m_projection;
     }
 
     void Camera::setProjection(const glm::mat4& projection) {
-        m_projection =  projection;
+        m_projection = glm::inverse(y_correction) * projection;
     }
 
     glm::mat4 Camera::getMVP() const {
-		const glm::mat4 y_correction (
-				1.0f,  0.0f,  0.0f,  0.0f,
-				0.0f, -1.0f,  0.0f,  0.0f,
-				0.0f,  0.0f,  1.0f,  0.0f,
-				0.0f,  0.0f,  0.0f,  1.0f
-		);
-    	
         return y_correction * m_projection * m_view;
     }
 
diff --git a/projects/CMakeLists.txt b/projects/CMakeLists.txt
index 34fbcb0cf8dd3f1d34efd2cc8424994c7da76e32..1c6e3afe2347f6ef8ea8a62be7acbe0ea750497d 100644
--- a/projects/CMakeLists.txt
+++ b/projects/CMakeLists.txt
@@ -3,5 +3,6 @@
 add_subdirectory(bloom)
 add_subdirectory(first_triangle)
 add_subdirectory(first_mesh)
+add_subdirectory(particle_simulation)
 add_subdirectory(first_scene)
-add_subdirectory(voxelization)
\ No newline at end of file
+add_subdirectory(voxelization)
diff --git a/projects/cmd_sync_test/src/main.cpp b/projects/cmd_sync_test/src/main.cpp
index eccc0af7331dc140f3a15ddf12c5645e685abc90..6e53eb8c5ec1825135778dc91b11dd6e45f44276 100644
--- a/projects/cmd_sync_test/src/main.cpp
+++ b/projects/cmd_sync_test/src/main.cpp
@@ -175,8 +175,8 @@ int main(int argc, const char** argv) {
 	std::vector<vkcv::DrawcallInfo> shadowDrawcalls;
 	for (const auto& position : instancePositions) {
 		modelMatrices.push_back(glm::translate(glm::mat4(1.f), position));
-		drawcalls.push_back(vkcv::DrawcallInfo(loadedMesh, { descriptorUsage }));
-		shadowDrawcalls.push_back(vkcv::DrawcallInfo(loadedMesh, {}));
+		drawcalls.push_back(vkcv::DrawcallInfo(loadedMesh, { descriptorUsage },1));
+		shadowDrawcalls.push_back(vkcv::DrawcallInfo(loadedMesh, {},1));
 	}
 
 	modelMatrices.back() *= glm::scale(glm::mat4(1.f), glm::vec3(10.f, 1.f, 10.f));
diff --git a/projects/first_mesh/src/main.cpp b/projects/first_mesh/src/main.cpp
index dc43c905784525a34732bc0e66343fbdcc17a639..e7546fc3a143b3638cceb36869c519336ebec751 100644
--- a/projects/first_mesh/src/main.cpp
+++ b/projects/first_mesh/src/main.cpp
@@ -83,6 +83,7 @@ int main(int argc, const char** argv) {
     firstMeshProgram.addShader(vkcv::ShaderStage::FRAGMENT, std::filesystem::path("resources/shaders/frag.spv"));
 	
 	auto& attributes = mesh.vertexGroups[0].vertexBuffer.attributes;
+
 	
 	std::sort(attributes.begin(), attributes.end(), [](const vkcv::asset::VertexAttribute& x, const vkcv::asset::VertexAttribute& y) {
 		return static_cast<uint32_t>(x.type) < static_cast<uint32_t>(y.type);
@@ -149,7 +150,7 @@ int main(int argc, const char** argv) {
 	const vkcv::Mesh renderMesh(vertexBufferBindings, indexBuffer.getVulkanHandle(), mesh.vertexGroups[0].numIndices);
 
 	vkcv::DescriptorSetUsage    descriptorUsage(0, core.getDescriptorSet(descriptorSet).vulkanHandle);
-	vkcv::DrawcallInfo          drawcall(renderMesh, { descriptorUsage });
+	vkcv::DrawcallInfo          drawcall(renderMesh, { descriptorUsage },1);
 
     vkcv::camera::CameraManager cameraManager(window);
     uint32_t camIndex0 = cameraManager.addCamera(vkcv::camera::ControllerType::PILOT);
diff --git a/projects/first_scene/src/main.cpp b/projects/first_scene/src/main.cpp
index 420400cdd04865ddd48eec7cf6000e36417d8095..521818732f7a60eabe9f0c2c080c6d343a71b1d8 100644
--- a/projects/first_scene/src/main.cpp
+++ b/projects/first_scene/src/main.cpp
@@ -199,7 +199,7 @@ int main(int argc, const char** argv) {
 
         vkcv::DescriptorSetUsage descriptorUsage(0, core.getDescriptorSet(descriptorSets[i]).vulkanHandle);
 
-	    drawcalls.push_back(vkcv::DrawcallInfo(renderMesh, {descriptorUsage}));
+	    drawcalls.push_back(vkcv::DrawcallInfo(renderMesh, {descriptorUsage},1));
 	}
 
 	std::vector<glm::mat4> modelMatrices;
diff --git a/projects/first_triangle/src/main.cpp b/projects/first_triangle/src/main.cpp
index 20cfdddf5c1baa9e8727312daa36de94bd56672f..5bdd55a263f4d81d8f424c056d7d6c0b54ccb1ca 100644
--- a/projects/first_triangle/src/main.cpp
+++ b/projects/first_triangle/src/main.cpp
@@ -167,7 +167,7 @@ int main(int argc, const char** argv) {
 	vkcv::ImageHandle swapchainImageHandle = vkcv::ImageHandle::createSwapchainImageHandle();
 
 	const vkcv::Mesh renderMesh({}, triangleIndexBuffer.getVulkanHandle(), 3);
-	vkcv::DrawcallInfo drawcall(renderMesh, {});
+	vkcv::DrawcallInfo drawcall(renderMesh, {},1);
 
 	const vkcv::ImageHandle swapchainInput = vkcv::ImageHandle::createSwapchainImageHandle();
 
diff --git a/projects/particle_simulation/.gitignore b/projects/particle_simulation/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..4964f89e973f38358aa57f564f56d3d4b0c328a9
--- /dev/null
+++ b/projects/particle_simulation/.gitignore
@@ -0,0 +1 @@
+particle_simulation
\ No newline at end of file
diff --git a/projects/particle_simulation/CMakeLists.txt b/projects/particle_simulation/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..2a665202c521ac10ae94905cb7580205e897eef9
--- /dev/null
+++ b/projects/particle_simulation/CMakeLists.txt
@@ -0,0 +1,35 @@
+cmake_minimum_required(VERSION 3.16)
+project(particle_simulation)
+
+# 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(particle_simulation 
+		src/main.cpp
+		src/ParticleSystem.hpp 
+		src/ParticleSystem.cpp
+		src/Particle.hpp 
+		src/Particle.cpp
+		src/BloomAndFlares.hpp
+		src/BloomAndFlares.cpp)
+
+# this should fix the execution path to load local files from the project (for MSVC)
+if(MSVC)
+	set_target_properties(particle_simulation PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+	set_target_properties(particle_simulation 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(particle_simulation PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+endif()
+
+# including headers of dependencies and the VkCV framework
+target_include_directories(particle_simulation 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(particle_simulation vkcv vkcv_testing vkcv_camera vkcv_shader_compiler)
diff --git a/projects/particle_simulation/shaders/bloom/composite.comp b/projects/particle_simulation/shaders/bloom/composite.comp
new file mode 100644
index 0000000000000000000000000000000000000000..87b5ddb975106232d1cd3b6e5b8dc7e623dd0b59
--- /dev/null
+++ b/projects/particle_simulation/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/particle_simulation/shaders/bloom/downsample.comp b/projects/particle_simulation/shaders/bloom/downsample.comp
new file mode 100644
index 0000000000000000000000000000000000000000..2ab00c7c92798769153634f3479c5b7f3fb61d94
--- /dev/null
+++ b/projects/particle_simulation/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/particle_simulation/shaders/bloom/lensFlares.comp b/projects/particle_simulation/shaders/bloom/lensFlares.comp
new file mode 100644
index 0000000000000000000000000000000000000000..ce27d8850b709f61332d467914ddc944dc63109f
--- /dev/null
+++ b/projects/particle_simulation/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/particle_simulation/shaders/bloom/upsample.comp b/projects/particle_simulation/shaders/bloom/upsample.comp
new file mode 100644
index 0000000000000000000000000000000000000000..0ddeedb5b5af9e476dc19012fed6430544006c0e
--- /dev/null
+++ b/projects/particle_simulation/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/particle_simulation/shaders/particleShading.inc b/projects/particle_simulation/shaders/particleShading.inc
new file mode 100644
index 0000000000000000000000000000000000000000..b2d1832b9ccd6ba05a585b59bdfdedd4729e80f8
--- /dev/null
+++ b/projects/particle_simulation/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/particle_simulation/shaders/shader.vert b/projects/particle_simulation/shaders/shader.vert
new file mode 100644
index 0000000000000000000000000000000000000000..0a889b35dbb750dc932de57b611f22acaa1ac3f2
--- /dev/null
+++ b/projects/particle_simulation/shaders/shader.vert
@@ -0,0 +1,45 @@
+#version 460 core
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(location = 0) in vec3 particle;
+
+struct Particle
+{
+    vec3 position;
+    float lifeTime;
+    vec3 velocity;
+    float padding_2;
+    vec3 reset_velocity;
+    float padding_3;
+};
+
+layout(std430, binding = 2) coherent buffer buffer_inParticle
+{
+    Particle inParticle[];
+};
+
+layout( push_constant ) uniform constants{
+    mat4 view;
+    mat4 projection;
+};
+
+layout(location = 0) out vec2 passTriangleCoordinates;
+layout(location = 1) out vec3 passVelocity;
+layout(location = 2) out float passlifeTime;
+
+void main()
+{
+    int id = gl_InstanceIndex;
+    passVelocity = inParticle[id].velocity;
+    passlifeTime = inParticle[id].lifeTime;
+    // particle position in view space
+    vec4 positionView = view * vec4(inParticle[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/particle_simulation/shaders/shader_gravity.comp b/projects/particle_simulation/shaders/shader_gravity.comp
new file mode 100644
index 0000000000000000000000000000000000000000..77954958c694a3c6c620818dd3b5d999e51b4a42
--- /dev/null
+++ b/projects/particle_simulation/shaders/shader_gravity.comp
@@ -0,0 +1,80 @@
+#version 450 core
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(local_size_x = 256) in;
+
+struct Particle
+{
+    vec3 position;
+    float lifeTime;
+    vec3 velocity;
+    float mass;
+    vec3 reset_velocity;
+    float _padding;
+};
+
+layout(std430, binding = 0) coherent buffer buffer_inParticle
+{
+    Particle inParticle[];
+};
+
+layout( push_constant ) uniform constants{
+    float deltaTime;
+    float rand;
+};
+
+const int n = 4;
+vec4 gravityPoint[n] = vec4[n](
+    vec4(-0.8, -0.5,  0.0, 3),
+    vec4(-0.4,  0.5,  0.8, 2),
+    vec4( 0.8,  0.8, -0.3, 4),
+    vec4( 0.5, -0.7, -0.5, 1)
+);
+
+const float G = 6.6743015e-11;
+const float sim_d_factor = 10e11;
+const float sim_g_factor = 10e30;
+const float sim_t_factor = 5;
+const float c = 299792458;
+
+void main() {
+    uint id = gl_GlobalInvocationID.x;
+    inParticle[id].lifeTime -= deltaTime;
+    vec3 pos = inParticle[id].position;
+    vec3 vel = inParticle[id].velocity;
+    float mass = inParticle[id].mass;
+
+    if(inParticle[id].lifeTime < 0.f)
+    {
+        inParticle[id].lifeTime = 5.f * rand;
+        inParticle[id].mass *= rand;
+
+        pos = vec3(0);
+        vel *= rand;
+    }
+
+    for(int i = 0; i < n; i++)
+    {
+        vec3 d = (gravityPoint[i].xyz - pos) * sim_d_factor;
+        float r = length(d);
+        float g = G * (gravityPoint[i].w * sim_g_factor) / (r * r);
+
+        if (r > 0) {
+            vec3 dvel = (deltaTime * sim_t_factor) * g * (d / r);
+
+            vel = (vel + dvel) / (1.0 + dot(vel, dvel) / (c*c));
+        }
+    }
+
+    pos += vel * (deltaTime * sim_t_factor);
+
+    vec3 a_pos = abs(pos);
+
+    if ((a_pos.x > 2.0) || (a_pos.y > 2.0) || (a_pos.z > 2.0))
+    {
+        inParticle[id].lifeTime *= 0.9;
+    }
+
+    inParticle[id].position = pos;
+    inParticle[id].velocity = vel;
+}
diff --git a/projects/particle_simulation/shaders/shader_space.comp b/projects/particle_simulation/shaders/shader_space.comp
new file mode 100644
index 0000000000000000000000000000000000000000..6e25fff8aec8ceab7c1ffdd9be65d9b8fa8f0974
--- /dev/null
+++ b/projects/particle_simulation/shaders/shader_space.comp
@@ -0,0 +1,73 @@
+#version 450 core
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(local_size_x = 256) in;
+
+struct Particle
+{
+    vec3 position;
+    float lifeTime;
+    vec3 velocity;
+    float padding_2;
+    vec3 reset_velocity;
+    float padding_3;
+};
+
+layout(std430, binding = 0) coherent buffer buffer_inParticle
+{
+    Particle inParticle[];
+};
+
+layout( push_constant ) uniform constants{
+    float deltaTime;
+    float rand;
+};
+
+vec3 attraction(vec3 pos, vec3 attractPos)
+{
+    vec3 delta = attractPos - pos;
+    const float damp = 0.5;
+    float dDampedDot = dot(delta, delta) + damp;
+    float invDist = 1.0f / sqrt(dDampedDot);
+    float invDistCubed = invDist*invDist*invDist;
+    return delta * invDistCubed * 0.0035;
+}
+
+vec3 repulsion(vec3 pos, vec3 attractPos)
+{
+    vec3 delta = attractPos - pos;
+    float targetDistance = sqrt(dot(delta, delta));
+    return delta * (1.0 / (targetDistance * targetDistance * targetDistance)) * -0.000035;
+}
+
+
+const int n = 4;
+vec3 gravity = vec3(0,-9.8,0);
+vec3 gravityPoint[n] = vec3[n](vec3(-0.3, .5, -0.6),vec3(-0.2, 0.6, -0.3),vec3(.4, -0.4, 0.6),vec3(-.4, -0.4, -0.6));
+//vec3 gravityPoint[n] = vec3[n](vec3(-0.5, 0.5, 0));
+void main() {
+    uint id = gl_GlobalInvocationID.x;
+    inParticle[id].lifeTime -= deltaTime;
+    vec3 pos = inParticle[id].position;
+    vec3 vel = inParticle[id].velocity;
+    if(inParticle[id].lifeTime < 0.f)
+    {
+        inParticle[id].lifeTime = 5.f;
+        pos = vec3(0);
+    }
+    //    inParticle[id].position += deltaTime * -normalize(max(2 - distance(inParticle[id].position,respawnPos),0.0) * respawnPos - inParticle[id].position);
+
+    for(int i = 0; i < n; i++)
+    {
+        vel += deltaTime * deltaTime * normalize(max(2 - distance(pos,gravityPoint[i]),0.1) * gravityPoint[i] - pos);
+    }
+
+    if((pos.x <= -2.0) || (pos.x > 2.0) || (pos.y <= -2.0) || (pos.y > 2.0)|| (pos.z <= -2.0) || (pos.z > 2.0)){
+        vel = (-vel * 0.1);
+    }
+
+    pos += normalize(vel) * deltaTime;
+    inParticle[id].position = pos;
+    float rand1 = rand;
+    inParticle[id].velocity = vel;
+}
diff --git a/projects/particle_simulation/shaders/shader_space.frag b/projects/particle_simulation/shaders/shader_space.frag
new file mode 100644
index 0000000000000000000000000000000000000000..7f6d22065caa3c4b3ab2b1f697c9545a66d7bd54
--- /dev/null
+++ b/projects/particle_simulation/shaders/shader_space.frag
@@ -0,0 +1,46 @@
+#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 = 2) in float passlifeTime;
+
+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()
+{
+	vec2 mouse = vec2(Position.position.x, Position.position.y);
+    
+    vec3 c0 = vec3(1, 1, 0.05);
+    vec3 c1 = vec3(1, passlifeTime * 0.5, 0.05);
+    vec3 c2 = vec3(passlifeTime * 0.5,passlifeTime * 0.5,0.05);
+    vec3 c3 = vec3(1, 0.05, 0.05);
+    
+    if(passlifeTime  < 1){
+        outColor = mix(c0, c1, passlifeTime );
+    }
+    else if(passlifeTime  < 2){
+        outColor = mix(c1, c2, passlifeTime  - 1);
+    }
+    else{
+        outColor = mix(c2, c3, clamp((passlifeTime  - 2) * 0.5, 0, 1));
+    }
+   
+   // make the triangle look like a circle
+   outColor *= circleFactor(passTriangleCoordinates);
+   
+   // fade out particle shortly before it dies
+   outColor *= clamp(passlifeTime * 2, 0, 1);
+}
\ No newline at end of file
diff --git a/projects/particle_simulation/shaders/shader_water.comp b/projects/particle_simulation/shaders/shader_water.comp
new file mode 100644
index 0000000000000000000000000000000000000000..d1a0e761038b5fb367a33454c746871f1a6a4553
--- /dev/null
+++ b/projects/particle_simulation/shaders/shader_water.comp
@@ -0,0 +1,84 @@
+#version 450 core
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(local_size_x = 256) in;
+
+struct Particle
+{
+    vec3 position;
+    float lifeTime;
+    vec3 velocity;
+    float padding_2;
+    vec3 reset_velocity;
+    float padding_3;
+};
+
+layout(std430, binding = 0) coherent buffer buffer_inParticle
+{
+    Particle inParticle[];
+};
+
+layout( push_constant ) uniform constants{
+    float deltaTime;
+    float rand;
+};
+
+vec3 attraction(vec3 pos, vec3 attractPos)
+{
+    vec3 delta = attractPos - pos;
+    const float damp = 0.5;
+    float dDampedDot = dot(delta, delta) + damp;
+    float invDist = 1.0f / sqrt(dDampedDot);
+    float invDistCubed = invDist*invDist*invDist;
+    return delta * invDistCubed * 0.0035;
+}
+
+vec3 repulsion(vec3 pos, vec3 attractPos)
+{
+    vec3 delta = attractPos - pos;
+    float targetDistance = sqrt(dot(delta, delta));
+    return delta * (1.0 / (targetDistance * targetDistance * targetDistance)) * -0.000035;
+}
+
+
+const int n = 3;
+vec3 gravity = vec3(0,-9.8,0);
+vec3 gravityPoint[n] = vec3[n](vec3(-0.5, 0.5, 0),vec3(0.5, 0.5, 0),vec3(0, -0.5, 0));
+//vec3 gravityPoint[n] = vec3[n](vec3(-0.5, 0.5, 0));
+void main() {
+    uint id = gl_GlobalInvocationID.x;
+    inParticle[id].lifeTime -= deltaTime;
+    vec3 pos = inParticle[id].position;
+    vec3 vel = inParticle[id].velocity;
+    if(inParticle[id].lifeTime < 0.f)
+    {
+        inParticle[id].lifeTime = 7.f;
+        pos = vec3(0);
+        vel = inParticle[id].reset_velocity;
+        inParticle[id].velocity = inParticle[id].reset_velocity;
+    }
+    //    inParticle[id].position += deltaTime * -normalize(max(2 - distance(inParticle[id].position,respawnPos),0.0) * respawnPos - inParticle[id].position);
+
+    for(int i = 0; i < n; i++)
+    {
+        vel += deltaTime * deltaTime * deltaTime * normalize(max(2 - distance(pos,gravityPoint[i]),0.1) * gravityPoint[i] - pos);
+    }
+
+    //vec3 delta = respawnPos - pos;
+    //float targetDistane = sqrt(dot(delta,delta));
+    //vel += repulsion(pos, respawnPos);
+
+    //if((pos.x <= -1.0) || (pos.x > 1.0) || (pos.y <= -1.0) || (pos.y > 1.0)|| (pos.z <= -1.0) || (pos.z > 1.0))
+    vel = (-vel * 0.01);
+
+    if((pos.y <= -1.0) || (pos.y > 1.0)){
+        vel = reflect(vel, vec3(0,1,0));
+    }
+
+    pos += normalize(vel) * deltaTime;
+    inParticle[id].position = pos;
+
+    float weight = 1.0;
+    float rand1 = rand;
+    inParticle[id].velocity = vel;
+}
diff --git a/projects/particle_simulation/shaders/shader_water.frag b/projects/particle_simulation/shaders/shader_water.frag
new file mode 100644
index 0000000000000000000000000000000000000000..b68f9572a91b05e836c3fead9ae9afd7ce16ba8e
--- /dev/null
+++ b/projects/particle_simulation/shaders/shader_water.frag
@@ -0,0 +1,46 @@
+#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 = 2) in float passlifeTime;
+
+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 normlt = 1-normalize(passlifeTime);
+	vec2 mouse = vec2(Position.position.x, Position.position.y);
+    
+    vec3 c0 = vec3(0.2,0.5,1);
+    vec3 c1 = vec3(0.3, 0.7,1);
+    vec3 c2 = vec3(0.5,0.9,1);
+    vec3 c3 = vec3(0.9,1,1);
+    
+    if(passlifeTime  < 1){
+        outColor = mix(c0, c1, passlifeTime );
+    }
+    else if(passlifeTime  < 2){
+        outColor = mix(c1, c2, passlifeTime  - 1);
+    }
+    else{
+        outColor = mix(c2, c3, clamp((passlifeTime  - 2) * 0.5, 0, 1));
+    }
+    
+    // make the triangle look like a circle
+   outColor *= circleFactor(passTriangleCoordinates);
+   
+   // fade out particle shortly before it dies
+   outColor *= clamp(passlifeTime * 2, 0, 1);
+}
diff --git a/projects/particle_simulation/shaders/tonemapping.comp b/projects/particle_simulation/shaders/tonemapping.comp
new file mode 100644
index 0000000000000000000000000000000000000000..26f0232d66e3475afdd1266c0cc6288b47ed1c38
--- /dev/null
+++ b/projects/particle_simulation/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/particle_simulation/src/BloomAndFlares.cpp b/projects/particle_simulation/src/BloomAndFlares.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..23ace2bc35a2e421613718c62380f9161a408f70
--- /dev/null
+++ b/projects/particle_simulation/src/BloomAndFlares.cpp
@@ -0,0 +1,274 @@
+#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_DownsampleDescSets.push_back(
+                p_Core->createDescriptorSet(dsProg.getReflectedDescriptors()[0]));
+    }
+    m_DownsamplePipe = p_Core->createComputePipeline(
+            dsProg, { p_Core->getDescriptorSet(m_DownsampleDescSets[0]).layout });
+
+    // 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_UpsampleDescSets.push_back(
+                p_Core->createDescriptorSet(usProg.getReflectedDescriptors()[0]));
+    }
+    m_UpsamplePipe = p_Core->createComputePipeline(
+            usProg, { p_Core->getDescriptorSet(m_UpsampleDescSets[0]).layout });
+
+    // 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_LensFlareDescSet = p_Core->createDescriptorSet(lensProg.getReflectedDescriptors()[0]);
+    m_LensFlarePipe = p_Core->createComputePipeline(
+            lensProg, { p_Core->getDescriptorSet(m_LensFlareDescSet).layout });
+
+    // 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_CompositeDescSet = p_Core->createDescriptorSet(compProg.getReflectedDescriptors()[0]);
+    m_CompositePipe = p_Core->createComputePipeline(
+            compProg, { p_Core->getDescriptorSet(m_CompositeDescSet).layout });
+}
+
+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::PushConstantData(nullptr, 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::PushConstantData(nullptr, 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::PushConstantData(nullptr, 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::PushConstantData(nullptr, 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::PushConstantData(nullptr, 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)
+{
+    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/particle_simulation/src/BloomAndFlares.hpp b/projects/particle_simulation/src/BloomAndFlares.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..756b1ca154ea5232df04eb09a88bb743c5bd28aa
--- /dev/null
+++ b/projects/particle_simulation/src/BloomAndFlares.hpp
@@ -0,0 +1,47 @@
+#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::PipelineHandle                     m_DownsamplePipe;
+    std::vector<vkcv::DescriptorSetHandle>   m_DownsampleDescSets; // per mip desc set
+
+    vkcv::PipelineHandle                     m_UpsamplePipe;
+    std::vector<vkcv::DescriptorSetHandle>   m_UpsampleDescSets;   // per mip desc set
+
+    vkcv::PipelineHandle                     m_LensFlarePipe;
+    vkcv::DescriptorSetHandle                m_LensFlareDescSet;
+
+    vkcv::PipelineHandle                     m_CompositePipe;
+    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/particle_simulation/src/Particle.cpp b/projects/particle_simulation/src/Particle.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..387728eb366430e4373282da785bbff47de17e7a
--- /dev/null
+++ b/projects/particle_simulation/src/Particle.cpp
@@ -0,0 +1,41 @@
+
+#include "Particle.hpp"
+
+Particle::Particle(glm::vec3 position, glm::vec3 velocity, float lifeTime)
+: m_position(position),
+m_velocity(velocity),
+m_lifeTime(lifeTime),
+m_reset_velocity(velocity)
+{}
+
+const glm::vec3& Particle::getPosition()const{
+    return m_position;
+}
+
+const bool Particle::isAlive()const{
+    return m_lifeTime > 0.f;
+}
+
+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;
+}
+
+void Particle::update( const float delta ){
+    m_position += m_velocity * delta;
+}
+
+void Particle::setLifeTime( const float lifeTime ){
+    m_lifeTime = lifeTime;
+}
+
+const float& Particle::getLifeTime()const{
+    return m_lifeTime;
+}
\ No newline at end of file
diff --git a/projects/particle_simulation/src/Particle.hpp b/projects/particle_simulation/src/Particle.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..f374218fd8a08f1e1bf367bdc899a71c55ea1b78
--- /dev/null
+++ b/projects/particle_simulation/src/Particle.hpp
@@ -0,0 +1,34 @@
+#pragma once
+
+#include <glm/glm.hpp>
+
+class Particle {
+
+public:
+    Particle(glm::vec3 position, glm::vec3 velocity, float lifeTime = 1.f);
+
+    const glm::vec3& getPosition()const;
+
+    void setPosition( const glm::vec3 pos );
+
+    const glm::vec3& getVelocity()const;
+
+    void setVelocity( const glm::vec3 vel );
+
+    void update( const float delta );
+
+    const bool isAlive()const;
+
+    void setLifeTime( const float lifeTime );
+
+    const float& getLifeTime()const;
+
+private:
+    // all properties of the Particle
+    glm::vec3 m_position;
+    float m_lifeTime;
+    glm::vec3 m_velocity;
+    float mass = 1.f;
+    glm::vec3 m_reset_velocity;
+    float padding_3 = 0.f;
+};
diff --git a/projects/particle_simulation/src/ParticleSystem.cpp b/projects/particle_simulation/src/ParticleSystem.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b3162d6bea685640d3949577271affc8b2080407
--- /dev/null
+++ b/projects/particle_simulation/src/ParticleSystem.cpp
@@ -0,0 +1,60 @@
+#include "ParticleSystem.hpp"
+
+ParticleSystem::ParticleSystem(uint32_t particleCount ,glm::vec3 minVelocity , glm::vec3 maxVelocity , glm::vec2 lifeTime )
+{
+    m_rdmVel.resize(3);
+    m_rdmVel[0] = std::uniform_real_distribution<float>(minVelocity.x, maxVelocity.x);
+    m_rdmVel[1] = std::uniform_real_distribution<float>(minVelocity.y, maxVelocity.y);
+    m_rdmVel[2] = std::uniform_real_distribution<float>(minVelocity.z, maxVelocity.z);
+    m_rdmLifeTime = std::uniform_real_distribution<float>(lifeTime.x, lifeTime.y);
+
+    for(uint32_t i = 0; i < particleCount ;i++ ){
+        addParticle(Particle(m_respawnPos, getRandomVelocity(), getRandomLifeTime()));
+    }
+}
+
+const std::vector<Particle>& ParticleSystem::getParticles() const{
+    return m_particles;
+}
+
+void ParticleSystem::addParticle( const Particle particle ){
+    m_particles.push_back(particle);
+}
+void ParticleSystem::addParticles( const std::vector<Particle> particles ){
+    m_particles.insert(m_particles.end(), particles.begin(), particles.end());
+}
+
+void ParticleSystem::updateParticles( const float deltaTime ){
+    for(Particle& particle :m_particles){
+        bool alive = particle.isAlive();
+        particle.setPosition( particle.getPosition() * static_cast<float>(alive) + static_cast<float>(!alive) * m_respawnPos );
+        particle.setVelocity( particle.getVelocity() * static_cast<float>(alive) + static_cast<float>(!alive) *  getRandomVelocity());
+        particle.setLifeTime( (particle.getLifeTime() * alive + !alive * getRandomLifeTime() ) - deltaTime );
+        particle.update(deltaTime);
+    }
+}
+
+glm::vec3 ParticleSystem::getRandomVelocity(){
+    return glm::vec3(m_rdmVel[0](m_rdmEngine), m_rdmVel[1](m_rdmEngine),m_rdmVel[2](m_rdmEngine));
+}
+
+float ParticleSystem::getRandomLifeTime(){
+    return m_rdmLifeTime(m_rdmEngine);
+}
+
+void ParticleSystem::setRespawnPos( const glm::vec3 respawnPos){
+    m_respawnPos = respawnPos;
+}
+void ParticleSystem::setRdmLifeTime( const glm::vec2 lifeTime ){
+    m_rdmLifeTime = std::uniform_real_distribution<float> (lifeTime.x,lifeTime.y);
+}
+
+void ParticleSystem::setRdmVelocity( glm::vec3 minVelocity, glm::vec3 maxVelocity ){
+    m_rdmVel[0] = std::uniform_real_distribution<float> (minVelocity.x,maxVelocity.x);
+    m_rdmVel[1] = std::uniform_real_distribution<float> (minVelocity.y,maxVelocity.y);
+    m_rdmVel[2] = std::uniform_real_distribution<float> (minVelocity.z,maxVelocity.z);
+}
+
+const glm::vec3 ParticleSystem::getRespawnPos() const{
+    return m_respawnPos;
+}
diff --git a/projects/particle_simulation/src/ParticleSystem.hpp b/projects/particle_simulation/src/ParticleSystem.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..fe5c99f9b407b9dbdfd414e265e7cd91bbe790b9
--- /dev/null
+++ b/projects/particle_simulation/src/ParticleSystem.hpp
@@ -0,0 +1,32 @@
+#pragma once
+
+#include <vector>
+#include "Particle.hpp"
+#include <random>
+#include "vkcv/Buffer.hpp"
+
+class ParticleSystem {
+
+public:
+    ParticleSystem(uint32_t particleCount , glm::vec3 minVelocity = glm::vec3(0.f,0.f,0.f), glm::vec3 maxVelocity = glm::vec3(1.f,1.f,0.f), glm::vec2 lifeTime = glm::vec2(2.f,3.f));
+    const std::vector<Particle> &getParticles() const;
+    void updateParticles( const float deltaTime );
+    void setRespawnPos( const glm::vec3 respawnPos );
+    void setRdmLifeTime( const glm::vec2 lifeTime );
+    void setRdmVelocity( glm::vec3 minVelocity, glm::vec3 maxVelocity );
+    const glm::vec3 getRespawnPos() const;
+
+private:
+
+    void addParticle( const Particle particle );
+    void addParticles( const std::vector<Particle> particles );
+    glm::vec3 getRandomVelocity();
+    float getRandomLifeTime();
+
+    std::vector<Particle> m_particles;
+    glm::vec3 m_respawnPos = glm::vec3(0.f);
+
+    std::vector<std::uniform_real_distribution<float>> m_rdmVel;
+    std::uniform_real_distribution<float> m_rdmLifeTime;
+    std::default_random_engine m_rdmEngine;
+};
\ No newline at end of file
diff --git a/projects/particle_simulation/src/main.cpp b/projects/particle_simulation/src/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a22044f0d2588a43a5e7a0f6cba25d9c7460be9f
--- /dev/null
+++ b/projects/particle_simulation/src/main.cpp
@@ -0,0 +1,320 @@
+#include <iostream>
+#include <vkcv/Core.hpp>
+#include <GLFW/glfw3.h>
+#include <vkcv/camera/CameraManager.hpp>
+#include <chrono>
+#include "ParticleSystem.hpp"
+#include <random>
+#include <glm/gtc/matrix_access.hpp>
+#include <time.h>
+#include <vkcv/shader/GLSLCompiler.hpp>
+#include "BloomAndFlares.hpp"
+
+int main(int argc, const char **argv) {
+    const char *applicationName = "Particlesystem";
+
+    uint32_t windowWidth = 800;
+    uint32_t windowHeight = 600;
+    vkcv::Window window = vkcv::Window::create(
+            applicationName,
+            windowWidth,
+            windowHeight,
+            true
+    );
+
+    vkcv::camera::CameraManager cameraManager(window);
+
+    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"}
+    );
+
+    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);
+
+    if (!particlePass || !computePass)
+    {
+        std::cout << "Error. Could not create renderpass. Exiting." << std::endl;
+        return EXIT_FAILURE;
+    }
+
+    // use space or use water
+    bool useSpace = true;
+
+    vkcv::shader::GLSLCompiler compiler;
+    vkcv::ShaderProgram computeShaderProgram{};
+    compiler.compile(vkcv::ShaderStage::COMPUTE, useSpace ? "shaders/shader_space.comp" : "shaders/shader_water.comp", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+        computeShaderProgram.addShader(shaderStage, path);
+    });
+
+    vkcv::DescriptorSetHandle computeDescriptorSet = core.createDescriptorSet(computeShaderProgram.getReflectedDescriptors()[0]);
+
+    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);
+
+    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, useSpace ? "shaders/shader_space.frag" : "shaders/shader_water.frag", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+        particleShaderProgram.addShader(shaderStage, path);
+    });
+
+    vkcv::DescriptorSetHandle descriptorSet = core.createDescriptorSet(
+            particleShaderProgram.getReflectedDescriptors()[0]);
+
+    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::PipelineConfig particlePipelineDefinition{
+            particleShaderProgram,
+            UINT32_MAX,
+            UINT32_MAX,
+            particlePass,
+            {particleLayout},
+            {core.getDescriptorSet(descriptorSet).layout},
+            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::PipelineHandle particlePipeline = core.createGraphicsPipeline(particlePipelineDefinition);
+
+    vkcv::PipelineHandle computePipeline = core.createComputePipeline(computeShaderProgram, {core.getDescriptorSet(computeDescriptorSet).layout} );
+
+    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
+    );
+
+    glm::vec3 minVelocity = glm::vec3(-0.1f,-0.1f,-0.1f);
+    glm::vec3 maxVelocity = glm::vec3(0.1f,0.1f,0.1f);
+    glm::vec2 lifeTime = glm::vec2(-1.f,8.f);
+    ParticleSystem particleSystem = ParticleSystem( 100000 , minVelocity, maxVelocity, lifeTime);
+
+    vkcv::Buffer<Particle> particleBuffer = core.createBuffer<Particle>(
+            vkcv::BufferType::STORAGE,
+            particleSystem.getParticles().size()
+    );
+
+    particleBuffer.fill(particleSystem.getParticles());
+
+    vkcv::DescriptorWrites setWrites;
+    setWrites.uniformBufferWrites = {vkcv::UniformBufferDescriptorWrite(0,color.getHandle()),
+                                     vkcv::UniformBufferDescriptorWrite(1,position.getHandle())};
+    setWrites.storageBufferWrites = { vkcv::StorageBufferDescriptorWrite(2,particleBuffer.getHandle())};
+    core.writeDescriptorSet(descriptorSet, setWrites);
+
+    vkcv::DescriptorWrites computeWrites;
+    computeWrites.storageBufferWrites = { vkcv::StorageBufferDescriptorWrite(0,particleBuffer.getHandle())};
+    core.writeDescriptorSet(computeDescriptorSet, computeWrites);
+
+    if (!particlePipeline || !computePipeline)
+    {
+        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);
+    //vkcv::DrawcallInfo drawcalls(renderMesh, {vkcv::DescriptorSetUsage(0, core.getDescriptorSet(descriptorSet).vulkanHandle)});
+
+    glm::vec2 pos = glm::vec2(0.f);
+    glm::vec3 spawnPosition = glm::vec3(0.f);
+    glm::vec4 tempPosition = glm::vec4(0.f);
+
+    window.e_mouseMove.add([&](double offsetX, double offsetY) {
+        pos = glm::vec2(static_cast<float>(offsetX), static_cast<float>(offsetY));
+//        std::cout << offsetX << " , " << offsetY << std::endl;
+        // borders are assumed to be 0.5
+        //pos = glm::vec2((pos.x -0.5f * static_cast<float>(window.getWidth()))/static_cast<float>(window.getWidth()), (pos.y -0.5f * static_cast<float>(window.getHeight()))/static_cast<float>(window.getHeight()));
+        //borders are assumed to be 1
+        pos.x = (-2 * pos.x + static_cast<float>(window.getWidth())) / static_cast<float>(window.getWidth());
+        pos.y = (-2 * pos.y + static_cast<float>(window.getHeight())) / static_cast<float>(window.getHeight());
+        glm::vec4 row1 = glm::row(cameraManager.getCamera(0).getView(), 0);
+        glm::vec4 row2 = glm::row(cameraManager.getCamera(0).getView(), 1);
+        glm::vec4 row3 = glm::row(cameraManager.getCamera(0).getView(), 2);
+        glm::vec4 camera_pos = glm::column(cameraManager.getCamera(0).getView(), 3);
+//        std::cout << "row1: " << row1.x << ", " << row1.y << ", " << row1.z << std::endl;
+//        std::cout << "row2: " << row2.x << ", " << row2.y << ", " << row2.z << std::endl;
+//        std::cout << "row3: " << row3.x << ", " << row3.y << ", " << row3.z << std::endl;
+//        std::cout << "camerapos: " << camera_pos.x << ", " << camera_pos.y << ", " << camera_pos.z << std::endl;
+//        std::cout << "camerapos: " << camera_pos.x << ", " << camera_pos.y << ", " << camera_pos.z << std::endl;
+        //glm::vec4 view_axis = glm::row(cameraManager.getCamera().getView(), 2);
+        // std::cout << "view_axis: " << view_axis.x << ", " << view_axis.y << ", " << view_axis.z << std::endl;
+        //std::cout << "Front: " << cameraManager.getCamera().getFront().x << ", " << cameraManager.getCamera().getFront().z << ", " << cameraManager.getCamera().getFront().z << std::endl;
+        glm::mat4 viewmat = cameraManager.getCamera(0).getView();
+        spawnPosition = glm::vec3(pos.x, pos.y, 0.f);
+        tempPosition = glm::vec4(spawnPosition, 1.0f);
+        spawnPosition = glm::vec3(tempPosition.x, tempPosition.y, tempPosition.z);
+        particleSystem.setRespawnPos(glm::vec3(-spawnPosition.x, spawnPosition.y, spawnPosition.z));
+//        std::cout << "respawn pos: " << spawnPosition.x << ", " << spawnPosition.y << ", " << spawnPosition.z << std::endl;
+    });
+
+    std::vector<glm::mat4> modelMatrices;
+    std::vector<vkcv::DrawcallInfo> drawcalls;
+    drawcalls.push_back(vkcv::DrawcallInfo(renderMesh, {descriptorUsage}, particleSystem.getParticles().size()));
+
+    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));
+    cameraManager.getCamera(camIndex1).setPosition(glm::vec3(0.0f, 0.0f, -2.0f));
+    cameraManager.getCamera(camIndex1).setCenter(glm::vec3(0.0f, 0.0f, 0.0f));
+
+    vkcv::ImageHandle colorBuffer = core.createImage(colorFormat, windowWidth, windowHeight, 1, false, true, true).getHandle();
+    BloomAndFlares bloomAndFlares(&core, colorFormat, windowWidth, windowHeight);
+    window.e_resize.add([&](int width, int height) {
+        windowWidth = width;
+        windowHeight = height;
+        colorBuffer = core.createImage(colorFormat, windowWidth, windowHeight, 1, false, true, true).getHandle();
+        bloomAndFlares.updateImageDimensions(width, height);
+    });
+
+    vkcv::ShaderProgram tonemappingShader;
+    compiler.compile(vkcv::ShaderStage::COMPUTE, "shaders/tonemapping.comp", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+        tonemappingShader.addShader(shaderStage, path);
+    });
+
+    vkcv::DescriptorSetHandle tonemappingDescriptor = core.createDescriptorSet(tonemappingShader.getReflectedDescriptors()[0]);
+    vkcv::PipelineHandle tonemappingPipe = core.createComputePipeline(
+        tonemappingShader, 
+        { core.getDescriptorSet(tonemappingDescriptor).layout });
+
+    std::uniform_real_distribution<float> rdm = std::uniform_real_distribution<float>(0.95f, 1.05f);
+    std::default_random_engine rdmEngine;
+    while (window.isWindowOpen()) {
+        window.pollEvents();
+
+        uint32_t swapchainWidth, swapchainHeight;
+        if (!core.beginFrame(swapchainWidth, swapchainHeight)) {
+            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;
+//        particleSystem.updateParticles(deltatime);
+
+        cameraManager.update(deltatime);
+
+        // split view and projection to allow for easy billboarding in shader
+        glm::mat4 renderingMatrices[2];
+        renderingMatrices[0] = cameraManager.getActiveCamera().getView();
+        renderingMatrices[1] = cameraManager.getActiveCamera().getProjection();
+
+        auto cmdStream = core.createCommandStream(vkcv::QueueType::Graphics);
+        float random = rdm(rdmEngine);
+        glm::vec2 pushData = glm::vec2(deltatime, random);
+
+        vkcv::PushConstantData pushConstantDataCompute( &pushData, sizeof(glm::vec2));
+        uint32_t computeDispatchCount[3] = {static_cast<uint32_t> (std::ceil(particleSystem.getParticles().size()/256.f)),1,1};
+        core.recordComputeDispatchToCmdStream(cmdStream,
+                                              computePipeline,
+                                              computeDispatchCount,
+                                              {vkcv::DescriptorSetUsage(0,core.getDescriptorSet(computeDescriptorSet).vulkanHandle)},
+                                              pushConstantDataCompute);
+
+        core.recordBufferMemoryBarrier(cmdStream, particleBuffer.getHandle());
+
+        vkcv::PushConstantData pushConstantDataDraw((void *) &renderingMatrices[0], 2 * sizeof(glm::mat4));
+        core.recordDrawcallsToCmdStream(
+                cmdStream,
+                particlePass,
+                particlePipeline,
+                pushConstantDataDraw,
+                {drawcalls},
+                { colorBuffer });
+
+        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(windowWidth / 8.f);
+        tonemappingDispatchCount[1] = std::ceil(windowHeight / 8.f);
+        tonemappingDispatchCount[2] = 1;
+
+        core.recordComputeDispatchToCmdStream(
+            cmdStream, 
+            tonemappingPipe, 
+            tonemappingDispatchCount, 
+            {vkcv::DescriptorSetUsage(0, core.getDescriptorSet(tonemappingDescriptor).vulkanHandle) },
+            vkcv::PushConstantData(nullptr, 0));
+
+        core.prepareSwapchainImageForPresent(cmdStream);
+        core.submitCommandStream(cmdStream);
+        core.endFrame();
+    }
+
+    return 0;
+}
diff --git a/projects/voxelization/src/Voxelization.cpp b/projects/voxelization/src/Voxelization.cpp
index a5105bd26cc171d884075ebf5dc0e8f3d511e949..c117b4b9e6b896fbf51aae83343f30281061be9f 100644
--- a/projects/voxelization/src/Voxelization.cpp
+++ b/projects/voxelization/src/Voxelization.cpp
@@ -263,7 +263,7 @@ void Voxelization::voxelizeMeshes(
 			{ 
 				vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_voxelizationDescriptorSet).vulkanHandle),
 				vkcv::DescriptorSetUsage(1, m_corePtr->getDescriptorSet(perMeshDescriptorSets[i]).vulkanHandle) 
-			}));
+			},1));
 	}
 
 	m_corePtr->prepareImageForStorage(cmdStream, m_voxelImageIntermediate.getHandle());
@@ -335,7 +335,7 @@ void Voxelization::renderVoxelVisualisation(
 
 	const auto drawcall = vkcv::DrawcallInfo(
 		vkcv::Mesh({}, nullptr, drawVoxelCount),
-		{ vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_visualisationDescriptorSet).vulkanHandle) });
+		{ vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_visualisationDescriptorSet).vulkanHandle) },1);
 
 	m_corePtr->prepareImageForStorage(cmdStream, m_voxelImage.getHandle());
 	m_corePtr->recordDrawcallsToCmdStream(
diff --git a/src/vkcv/DrawcallRecording.cpp b/src/vkcv/DrawcallRecording.cpp
index d28ea36c6108306fe603185e9848a72af6455606..e6ea18588c251b5e49f454618a5ac9962cc8a264 100644
--- a/src/vkcv/DrawcallRecording.cpp
+++ b/src/vkcv/DrawcallRecording.cpp
@@ -38,10 +38,10 @@ namespace vkcv {
 
         if (drawcall.mesh.indexBuffer) {
             cmdBuffer.bindIndexBuffer(drawcall.mesh.indexBuffer, 0, vk::IndexType::eUint16);	//FIXME: choose proper size
-            cmdBuffer.drawIndexed(drawcall.mesh.indexCount, 1, 0, 0, {});
+            cmdBuffer.drawIndexed(drawcall.mesh.indexCount, drawcall.instanceCount, 0, 0, {});
         }
         else {
             cmdBuffer.draw(drawcall.mesh.indexCount, 1, 0, 0, {});
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/vkcv/ImageManager.cpp b/src/vkcv/ImageManager.cpp
index 5ee8d38764f81b3b24046837734ea45017bc0faa..ae554e6babdd2b2f42c352515c02a34e45182fec 100644
--- a/src/vkcv/ImageManager.cpp
+++ b/src/vkcv/ImageManager.cpp
@@ -278,7 +278,7 @@ namespace vkcv {
 		
 		const auto& image = m_images[id];
 
-		if (mipLevel >= m_images.size()) {
+		if (mipLevel >= image.m_viewPerMip.size()) {
 			vkcv_log(LogLevel::ERROR, "Image does not have requested mipLevel");
 			return nullptr;
 		}
diff --git a/src/vkcv/PipelineManager.cpp b/src/vkcv/PipelineManager.cpp
index 762078befe456de438349612155f3624de4e5d6e..8b1f0b68be3a72f60103ca0dd8136f2c923513a5 100644
--- a/src/vkcv/PipelineManager.cpp
+++ b/src/vkcv/PipelineManager.cpp
@@ -202,8 +202,11 @@ namespace vkcv
                                                VK_COLOR_COMPONENT_G_BIT |
                                                VK_COLOR_COMPONENT_B_BIT |
                                                VK_COLOR_COMPONENT_A_BIT);
+
+        // currently set to additive, if not disabled
+        // BlendFactors must be set as soon as additional BlendModes are added
         vk::PipelineColorBlendAttachmentState colorBlendAttachmentState(
-                false,
+                config.m_blendMode != BlendMode::None,
                 vk::BlendFactor::eOne,
                 vk::BlendFactor::eOne,
                 vk::BlendOp::eAdd,