diff --git a/projects/voxelization/CMakeLists.txt b/projects/voxelization/CMakeLists.txt
index 21c42b79811afba05c929f9a47f1033913c8f9f6..c962409f2e14994f0c38b923de7b9b1a4d198cab 100644
--- a/projects/voxelization/CMakeLists.txt
+++ b/projects/voxelization/CMakeLists.txt
@@ -15,7 +15,9 @@ target_sources(voxelization PRIVATE
     src/Voxelization.hpp
     src/Voxelization.cpp
     src/ShadowMapping.hpp
-    src/ShadowMapping.cpp)
+    src/ShadowMapping.cpp
+    src/BloomAndFlares.hpp
+    src/BloomAndFlares.cpp)
 
 # this should fix the execution path to load local files from the project (for MSVC)
 if(MSVC)
diff --git a/projects/voxelization/resources/shaders/bloomDownsample.comp b/projects/voxelization/resources/shaders/bloomDownsample.comp
new file mode 100644
index 0000000000000000000000000000000000000000..2ab00c7c92798769153634f3479c5b7f3fb61d94
--- /dev/null
+++ b/projects/voxelization/resources/shaders/bloomDownsample.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/voxelization/resources/shaders/bloomFlaresComposite.comp b/projects/voxelization/resources/shaders/bloomFlaresComposite.comp
new file mode 100644
index 0000000000000000000000000000000000000000..190bed0657d70e0217bf654820d0b2b2c58f12c2
--- /dev/null
+++ b/projects/voxelization/resources/shaders/bloomFlaresComposite.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.25f;
+    float lens_weight  = 0.25f;
+    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/voxelization/resources/shaders/bloomUpsample.comp b/projects/voxelization/resources/shaders/bloomUpsample.comp
new file mode 100644
index 0000000000000000000000000000000000000000..0ddeedb5b5af9e476dc19012fed6430544006c0e
--- /dev/null
+++ b/projects/voxelization/resources/shaders/bloomUpsample.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/voxelization/resources/shaders/lensFlares.comp b/projects/voxelization/resources/shaders/lensFlares.comp
new file mode 100644
index 0000000000000000000000000000000000000000..ce27d8850b709f61332d467914ddc944dc63109f
--- /dev/null
+++ b/projects/voxelization/resources/shaders/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/voxelization/src/BloomAndFlares.cpp b/projects/voxelization/src/BloomAndFlares.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..71a09fcac55756cd616e9b55b5a3b239e1c8b5d0
--- /dev/null
+++ b/projects/voxelization/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,
+                     "resources/shaders/bloomDownsample.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,
+                     "resources/shaders/bloomUpsample.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,
+                     "resources/shaders/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,
+                     "resources/shaders/bloomFlaresComposite.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 < m_DownsampleDescSets.size(); 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>(5)
+	);
+
+    // 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/voxelization/src/BloomAndFlares.hpp b/projects/voxelization/src/BloomAndFlares.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..756b1ca154ea5232df04eb09a88bb743c5bd28aa
--- /dev/null
+++ b/projects/voxelization/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/voxelization/src/main.cpp b/projects/voxelization/src/main.cpp
index 059f8a8cbad2ca0e509ebf7e539036afd07d2e21..9ba4b8b78ba0d982f9c560e27a83326959a24320 100644
--- a/projects/voxelization/src/main.cpp
+++ b/projects/voxelization/src/main.cpp
@@ -10,6 +10,7 @@
 #include <glm/glm.hpp>
 #include "vkcv/gui/GUI.hpp"
 #include "ShadowMapping.hpp"
+#include "BloomAndFlares.hpp"
 
 int main(int argc, const char** argv) {
 	const char* applicationName = "Voxelization";
@@ -448,6 +449,8 @@ int main(int argc, const char** argv) {
 		voxelSampler,
 		msaa);
 
+	BloomAndFlares bloomFlares(&core, colorBufferFormat, windowWidth, windowHeight);
+
 	vkcv::Buffer<glm::vec3> cameraPosBuffer = core.createBuffer<glm::vec3>(vkcv::BufferType::UNIFORM, 1);
 
 	struct VolumetricSettings {
@@ -514,6 +517,8 @@ int main(int argc, const char** argv) {
 
 			windowWidth = swapchainWidth;
 			windowHeight = swapchainHeight;
+
+			bloomFlares.updateImageDimensions(windowWidth, windowHeight);
 		}
 
 		auto end = std::chrono::system_clock::now();
@@ -649,6 +654,8 @@ int main(int argc, const char** argv) {
 			}
 		}
 
+		bloomFlares.execWholePipeline(cmdStream, resolvedColorBuffer);
+
 		core.prepareImageForStorage(cmdStream, swapchainInput);
 		core.prepareImageForSampling(cmdStream, resolvedColorBuffer);