diff --git a/CMakeLists.txt b/CMakeLists.txt
index bff486150f082c2e96e543436d977cf3112403ba..2ae078a428a8e5e640ed8dc7bcc2f4e58e159c6b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -44,6 +44,9 @@ endif()
 # configure everything to use the required dependencies
 include(${vkcv_config}/Libraries.cmake)
 
+# set the compile definitions aka preprocessor variables
+add_compile_definitions(${vkcv_definitions})
+
 # add modules as targets
 add_subdirectory(modules)
 
@@ -56,9 +59,6 @@ message("-- Flags: [ ${vkcv_flags} ]")
 # set the compiler flags for the framework
 set(CMAKE_CXX_FLAGS ${vkcv_flags})
 
-# set the compile definitions aka preprocessor variables
-add_compile_definitions(${vkcv_definitions})
-
 # create VkCV framework as static library using all source files
 add_library(vkcv STATIC ${vkcv_sources})
 
diff --git a/config/Libraries.cmake b/config/Libraries.cmake
index e04aa3575a34632eb75c929bf4640305cd93e298..ec014f84c820abf4988b070d5b733be08c377319 100644
--- a/config/Libraries.cmake
+++ b/config/Libraries.cmake
@@ -10,6 +10,8 @@ if(NOT WIN32)
 	list(APPEND vkcv_flags -fopenmp)
 endif()
 
+list(APPEND vkcv_definitions _USE_MATH_DEFINES)
+
 # some formatted printing
 set(vkcv_config_msg " - Library: ")
 
diff --git a/include/vkcv/DescriptorWrites.hpp b/include/vkcv/DescriptorWrites.hpp
index 016bdab7c337d3ebada5ed8e9a035606eb937211..f28a6c91e189b13413ffefec0f05e5a0a358ee26 100644
--- a/include/vkcv/DescriptorWrites.hpp
+++ b/include/vkcv/DescriptorWrites.hpp
@@ -4,9 +4,12 @@
 
 namespace vkcv {
 	struct SampledImageDescriptorWrite {
-		inline SampledImageDescriptorWrite(uint32_t binding, ImageHandle image) : binding(binding), image(image) {};
+		inline SampledImageDescriptorWrite(uint32_t binding, ImageHandle image, uint32_t mipLevel = 0, bool useGeneralLayout = false)
+		    : binding(binding), image(image), mipLevel(mipLevel), useGeneralLayout(useGeneralLayout) {};
 		uint32_t	binding;
 		ImageHandle	image;
+		uint32_t    mipLevel;
+		bool        useGeneralLayout;
 	};
 
 	struct StorageImageDescriptorWrite {
@@ -38,8 +41,8 @@ namespace vkcv {
 	struct DescriptorWrites {
 		std::vector<SampledImageDescriptorWrite>		sampledImageWrites;
 		std::vector<StorageImageDescriptorWrite>		storageImageWrites;
-		std::vector<UniformBufferDescriptorWrite>	uniformBufferWrites;
-		std::vector<StorageBufferDescriptorWrite>	storageBufferWrites;
-		std::vector<SamplerDescriptorWrite>			samplerWrites;
+		std::vector<UniformBufferDescriptorWrite>	    uniformBufferWrites;
+		std::vector<StorageBufferDescriptorWrite>	    storageBufferWrites;
+		std::vector<SamplerDescriptorWrite>			    samplerWrites;
 	};
 }
\ No newline at end of file
diff --git a/include/vkcv/Image.hpp b/include/vkcv/Image.hpp
index 9e1f9708c6318aa6deb750097c414358ffde2c65..eac96975886a92e0043bbb97cbe64f76943efa7e 100644
--- a/include/vkcv/Image.hpp
+++ b/include/vkcv/Image.hpp
@@ -42,8 +42,10 @@ namespace vkcv {
 		void generateMipChainImmediate();
 		void recordMipChainGeneration(const vkcv::CommandStreamHandle& cmdStream);
 	private:
-		ImageManager* const m_manager;
-		const ImageHandle   m_handle;
+	    // TODO: const qualifier removed, very hacky!!!
+	    //  Else you cannot recreate an image. Pls fix.
+		ImageManager*       m_manager;
+		ImageHandle   m_handle;
 
 		Image(ImageManager* manager, const ImageHandle& handle);
 		
diff --git a/modules/asset_loader/CMakeLists.txt b/modules/asset_loader/CMakeLists.txt
index d2a9b817ea68c7851fd2123f76b378d8a4d85ac0..c5a1fd0eb9620d3a95af1c52a9e7456337047c7b 100644
--- a/modules/asset_loader/CMakeLists.txt
+++ b/modules/asset_loader/CMakeLists.txt
@@ -38,3 +38,5 @@ target_include_directories(vkcv_asset_loader SYSTEM BEFORE PRIVATE ${vkcv_asset_
 
 # add the own include directory for public headers
 target_include_directories(vkcv_asset_loader BEFORE PUBLIC ${vkcv_asset_loader_include})
+
+target_compile_definitions(vkcv_asset_loader PUBLIC ${vkcv_asset_loader_definitions})
diff --git a/modules/asset_loader/config/STB.cmake b/modules/asset_loader/config/STB.cmake
index da20d3ec07f98c865b4c6e38518f668b226cbfb9..1287d0a9ddda559e061ddd680bc815e24b3e2075 100644
--- a/modules/asset_loader/config/STB.cmake
+++ b/modules/asset_loader/config/STB.cmake
@@ -1,6 +1,10 @@
 
 if (EXISTS "${vkcv_asset_loader_lib_path}/stb")
 	list(APPEND vkcv_asset_loader_includes ${vkcv_asset_loader_lib}/stb)
+	
+	list(APPEND vkcv_asset_loader_definitions STB_IMAGE_IMPLEMENTATION)
+	list(APPEND vkcv_asset_loader_definitions STBI_ONLY_JPEG)
+	list(APPEND vkcv_asset_loader_definitions STBI_ONLY_PNG)
 else()
 	message(WARNING "STB is required..! Update the submodules!")
 endif ()
diff --git a/modules/asset_loader/src/vkcv/asset/asset_loader.cpp b/modules/asset_loader/src/vkcv/asset/asset_loader.cpp
index 97fd39515290ac9235b3936d44d3e40a584ef84f..c21d0c9f70bc81561e1078b15b8372e6dd4730f5 100644
--- a/modules/asset_loader/src/vkcv/asset/asset_loader.cpp
+++ b/modules/asset_loader/src/vkcv/asset/asset_loader.cpp
@@ -3,9 +3,6 @@
 #include <string.h>	// memcpy(3)
 #include <stdlib.h>	// calloc(3)
 #include <fx/gltf.h>
-#define STB_IMAGE_IMPLEMENTATION
-#define STBI_ONLY_JPEG
-#define STBI_ONLY_PNG
 #include <stb_image.h>
 #include <vkcv/Logger.hpp>
 #include <algorithm>
diff --git a/modules/camera/CMakeLists.txt b/modules/camera/CMakeLists.txt
index 73f2dd1c81be9c6cadf563f7936bfaba8c1d0025..60cfca4cf97cef30d989bdab064e20547764041c 100644
--- a/modules/camera/CMakeLists.txt
+++ b/modules/camera/CMakeLists.txt
@@ -36,3 +36,4 @@ target_include_directories(vkcv_camera SYSTEM BEFORE PRIVATE ${vkcv_camera_inclu
 # add the own include directory for public headers
 target_include_directories(vkcv_camera BEFORE PUBLIC ${vkcv_camera_include} ${vkcv_camera_includes})
 
+target_compile_definitions(vkcv_camera PUBLIC ${vkcv_camera_definitions})
diff --git a/modules/camera/config/GLM.cmake b/modules/camera/config/GLM.cmake
index c4d14392ef0ea24243a45b19cd8583d90d3267be..efd6444451100b912aa0b5b4a532dc8f448b0b40 100644
--- a/modules/camera/config/GLM.cmake
+++ b/modules/camera/config/GLM.cmake
@@ -4,11 +4,17 @@ find_package(glm QUIET)
 if (glm_FOUND)
     list(APPEND vkcv_camera_includes ${GLM_INCLUDE_DIRS})
     list(APPEND vkcv_camera_libraries glm)
+
+    list(APPEND vkcv_camera_definitions GLM_DEPTH_ZERO_TO_ONE)
+    list(APPEND vkcv_camera_definitions GLM_FORCE_LEFT_HANDED)
 else()
     if (EXISTS "${vkcv_camera_lib_path}/glm")
         add_subdirectory(${vkcv_camera_lib}/glm)
         
         list(APPEND vkcv_camera_libraries glm)
+        
+        list(APPEND vkcv_camera_definitions GLM_DEPTH_ZERO_TO_ONE)
+        list(APPEND vkcv_camera_definitions GLM_FORCE_LEFT_HANDED)
     else()
         message(WARNING "GLM is required..! Update the submodules!")
     endif ()
diff --git a/modules/camera/include/vkcv/camera/Camera.hpp b/modules/camera/include/vkcv/camera/Camera.hpp
index dc9f2dcb3038655f51fb2404abc21f98a2120399..ce32d3f8a0c6ee3e0dd882f24a9ac2d12c14a024 100644
--- a/modules/camera/include/vkcv/camera/Camera.hpp
+++ b/modules/camera/include/vkcv/camera/Camera.hpp
@@ -1,7 +1,5 @@
 #pragma once
 
-#define GLM_DEPTH_ZERO_TO_ONE
-#define GLM_FORCE_LEFT_HANDED
 #include <glm/glm.hpp>
 #include <glm/gtc/matrix_transform.hpp>
 #include <glm/gtc/matrix_access.hpp>
diff --git a/modules/camera/src/vkcv/camera/Camera.cpp b/modules/camera/src/vkcv/camera/Camera.cpp
index e7ba79006e61e48439fb498af6d88bf978f15e3f..5c8892d2ff799209719347c53c7ebff9d734e090 100644
--- a/modules/camera/src/vkcv/camera/Camera.cpp
+++ b/modules/camera/src/vkcv/camera/Camera.cpp
@@ -1,6 +1,5 @@
 #include "vkcv/camera/Camera.hpp"
 
-#define _USE_MATH_DEFINES
 #include <math.h>
 
 namespace vkcv::camera {
diff --git a/modules/gui/CMakeLists.txt b/modules/gui/CMakeLists.txt
index ce03f16e1f8d421f5b8e6c2fe913c0da04d34598..3b5202ccfe454f38745c53ac711cc05095ef88a1 100644
--- a/modules/gui/CMakeLists.txt
+++ b/modules/gui/CMakeLists.txt
@@ -32,3 +32,5 @@ target_include_directories(vkcv_gui SYSTEM BEFORE PRIVATE ${vkcv_gui_includes} $
 
 # add the own include directory for public headers
 target_include_directories(vkcv_gui BEFORE PUBLIC ${vkcv_gui_include} ${vkcv_imgui_includes})
+
+target_compile_definitions(vkcv_gui PUBLIC ${vkcv_gui_defines})
diff --git a/modules/gui/config/ImGui.cmake b/modules/gui/config/ImGui.cmake
index 3f55ad05c34783ba0e82c41d2cbc4e5b204d60e7..90cdafdeee355af9e63723632572799e135b04da 100644
--- a/modules/gui/config/ImGui.cmake
+++ b/modules/gui/config/ImGui.cmake
@@ -1,17 +1,27 @@
 
 if (EXISTS "${vkcv_gui_lib_path}/imgui")
 	list(APPEND vkcv_imgui_sources ${vkcv_gui_lib_path}/imgui/backends/imgui_impl_glfw.cpp)
-	list(APPEND vkcv_imgui_sources ${vkcv_gui_lib_path}/imgui/backends/imgui_impl_vulkan.cpp )
+	list(APPEND vkcv_imgui_sources ${vkcv_gui_lib_path}/imgui/backends/imgui_impl_glfw.h)
+	list(APPEND vkcv_imgui_sources ${vkcv_gui_lib_path}/imgui/backends/imgui_impl_vulkan.cpp)
+	list(APPEND vkcv_imgui_sources ${vkcv_gui_lib_path}/imgui/backends/imgui_impl_vulkan.h)
+	list(APPEND vkcv_imgui_sources ${vkcv_gui_lib_path}/imgui/imconfig.h)
 	list(APPEND vkcv_imgui_sources ${vkcv_gui_lib_path}/imgui/imgui.cpp)
+	list(APPEND vkcv_imgui_sources ${vkcv_gui_lib_path}/imgui/imgui.h)
 	list(APPEND vkcv_imgui_sources ${vkcv_gui_lib_path}/imgui/imgui_draw.cpp)
 	list(APPEND vkcv_imgui_sources ${vkcv_gui_lib_path}/imgui/imgui_demo.cpp)
+	list(APPEND vkcv_imgui_sources ${vkcv_gui_lib_path}/imgui/imgui_internal.h)
 	list(APPEND vkcv_imgui_sources ${vkcv_gui_lib_path}/imgui/imgui_tables.cpp)
 	list(APPEND vkcv_imgui_sources ${vkcv_gui_lib_path}/imgui/imgui_widgets.cpp)
+	list(APPEND vkcv_imgui_sources ${vkcv_gui_lib_path}/imgui/imstb_rectpack.h)
+	list(APPEND vkcv_imgui_sources ${vkcv_gui_lib_path}/imgui/imstb_textedit.h)
+	list(APPEND vkcv_imgui_sources ${vkcv_gui_lib_path}/imgui/imstb_truetype.h)
 	
 	list(APPEND vkcv_imgui_includes ${vkcv_gui_lib}/imgui)
 	list(APPEND vkcv_imgui_includes ${vkcv_gui_lib}/imgui/backend)
 	
 	list(APPEND vkcv_gui_include ${vkcv_gui_lib})
+	
+	list(APPEND vkcv_gui_defines IMGUI_DISABLE_WIN32_FUNCTIONS=1)
 else()
 	message(WARNING "IMGUI is required..! Update the submodules!")
 endif ()
diff --git a/projects/CMakeLists.txt b/projects/CMakeLists.txt
index 2136eac0ace801dd478899638bd70b9612eec8d2..1c6e3afe2347f6ef8ea8a62be7acbe0ea750497d 100644
--- a/projects/CMakeLists.txt
+++ b/projects/CMakeLists.txt
@@ -1,5 +1,6 @@
 
 # Add new projects/examples here:
+add_subdirectory(bloom)
 add_subdirectory(first_triangle)
 add_subdirectory(first_mesh)
 add_subdirectory(particle_simulation)
diff --git a/projects/bloom/.gitignore b/projects/bloom/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..3643183e0628e666abab193e1dd1d92c1774ac61
--- /dev/null
+++ b/projects/bloom/.gitignore
@@ -0,0 +1 @@
+bloom
\ No newline at end of file
diff --git a/projects/bloom/CMakeLists.txt b/projects/bloom/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..8171938e7cb430aacce5562af44f628c11c97c54
--- /dev/null
+++ b/projects/bloom/CMakeLists.txt
@@ -0,0 +1,32 @@
+cmake_minimum_required(VERSION 3.16)
+project(bloom)
+
+# 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(bloom src/main.cpp)
+
+target_sources(bloom PRIVATE
+		src/BloomAndFlares.cpp
+		src/BloomAndFlares.hpp)
+
+# this should fix the execution path to load local files from the project (for MSVC)
+if(MSVC)
+	set_target_properties(bloom PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+	set_target_properties(bloom 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(bloom PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+endif()
+
+# including headers of dependencies and the VkCV framework
+target_include_directories(bloom SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_asset_loader_include} ${vkcv_camera_include} ${vkcv_shader_compiler_include})
+
+# linking with libraries from all dependencies and the VkCV framework
+target_link_libraries(bloom vkcv ${vkcv_libraries} vkcv_asset_loader ${vkcv_asset_loader_libraries} vkcv_camera vkcv_shader_compiler)
diff --git a/projects/bloom/resources/Sponza/Sponza.bin b/projects/bloom/resources/Sponza/Sponza.bin
new file mode 100644
index 0000000000000000000000000000000000000000..cfedd26ca5a67b6d0a47d44d13a75e14a141717a
--- /dev/null
+++ b/projects/bloom/resources/Sponza/Sponza.bin
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4b809f7a17687dc99e6f41ca1ea32c06eded8779bf34d16f1f565d750b0ffd68
+size 6347696
diff --git a/projects/bloom/resources/Sponza/Sponza.gltf b/projects/bloom/resources/Sponza/Sponza.gltf
new file mode 100644
index 0000000000000000000000000000000000000000..172ea07e21c94465211c860cd805355704cef230
--- /dev/null
+++ b/projects/bloom/resources/Sponza/Sponza.gltf
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5cc0ecad5c4694088ff820e663619c370421afc1323ac487406e8e9b4735d787
+size 713962
diff --git a/projects/bloom/resources/Sponza/background.png b/projects/bloom/resources/Sponza/background.png
new file mode 100644
index 0000000000000000000000000000000000000000..b64def129da38f4e23d89e21b4af1039008a4327
--- /dev/null
+++ b/projects/bloom/resources/Sponza/background.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f5b5f900ff8ed83a31750ec8e428b5b91273794ddcbfc4e4b8a6a7e781f8c686
+size 1417666
diff --git a/projects/bloom/resources/Sponza/chain_texture.png b/projects/bloom/resources/Sponza/chain_texture.png
new file mode 100644
index 0000000000000000000000000000000000000000..c1e1768cff78e0614ad707eca8602a4c4edab5e5
--- /dev/null
+++ b/projects/bloom/resources/Sponza/chain_texture.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d8362cfd472880daeaea37439326a4651d1338680ae69bb2513fc6b17c8de7d4
+size 490895
diff --git a/projects/bloom/resources/Sponza/lion.png b/projects/bloom/resources/Sponza/lion.png
new file mode 100644
index 0000000000000000000000000000000000000000..c49c7f0ed31e762e19284d0d3624fbc47664e56b
--- /dev/null
+++ b/projects/bloom/resources/Sponza/lion.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9f882f746c3a9cd51a9c6eedc1189b97668721d91a3fe49232036e789912c652
+size 2088728
diff --git a/projects/bloom/resources/Sponza/spnza_bricks_a_diff.png b/projects/bloom/resources/Sponza/spnza_bricks_a_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..cde4c7a6511e9a5f03c63ad996437fcdba3ce2df
--- /dev/null
+++ b/projects/bloom/resources/Sponza/spnza_bricks_a_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b94219c2f5f943f3f4715c74e7d1038bf0ab3b3b3216a758eaee67f875df0851
+size 1928829
diff --git a/projects/bloom/resources/Sponza/sponza_arch_diff.png b/projects/bloom/resources/Sponza/sponza_arch_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..bcd9bda2918d226039f9e2d03902d377b706fab6
--- /dev/null
+++ b/projects/bloom/resources/Sponza/sponza_arch_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c0df2c8a01b2843b1c792b494f7173cdbc4f834840fc2177af3e5d690fceda57
+size 1596151
diff --git a/projects/bloom/resources/Sponza/sponza_ceiling_a_diff.png b/projects/bloom/resources/Sponza/sponza_ceiling_a_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..59de631ffac4414cabf69b2dc794c46fc187d6cb
--- /dev/null
+++ b/projects/bloom/resources/Sponza/sponza_ceiling_a_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ab6c187a81aa68f4eba30119e17fce2e4882a9ec320f70c90482dbe9da82b1c6
+size 1872074
diff --git a/projects/bloom/resources/Sponza/sponza_column_a_diff.png b/projects/bloom/resources/Sponza/sponza_column_a_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..01a82432d3f9939bbefe850bdb900f1ff9a3f6db
--- /dev/null
+++ b/projects/bloom/resources/Sponza/sponza_column_a_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2c291507e2808bb83e160ab4b020689817df273baad3713a9ad19ac15fac6826
+size 1840992
diff --git a/projects/bloom/resources/Sponza/sponza_column_b_diff.png b/projects/bloom/resources/Sponza/sponza_column_b_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..10a660cce2a5a9b8997772c746058ce23e7d45d7
--- /dev/null
+++ b/projects/bloom/resources/Sponza/sponza_column_b_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2820b0267c4289c6cedbb42721792a57ef244ec2d0935941011c2a7d3fe88a9b
+size 2170433
diff --git a/projects/bloom/resources/Sponza/sponza_column_c_diff.png b/projects/bloom/resources/Sponza/sponza_column_c_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..bc46fd979044a938d3adca7601689e71504e48bf
--- /dev/null
+++ b/projects/bloom/resources/Sponza/sponza_column_c_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a0bc993ff59865468ef4530798930c7dfefb07482d71db45bc2a520986b27735
+size 2066950
diff --git a/projects/bloom/resources/Sponza/sponza_curtain_blue_diff.png b/projects/bloom/resources/Sponza/sponza_curtain_blue_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..384c8c2c051160d530eb3ac8b05c9c60752a2d2b
--- /dev/null
+++ b/projects/bloom/resources/Sponza/sponza_curtain_blue_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b85c6bb3cd5105f48d3812ec8e7a1068521ce69e917300d79e136e19d45422fb
+size 9510905
diff --git a/projects/bloom/resources/Sponza/sponza_curtain_diff.png b/projects/bloom/resources/Sponza/sponza_curtain_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..af842e9f5fe18c1f609875e00899a6770fa4488b
--- /dev/null
+++ b/projects/bloom/resources/Sponza/sponza_curtain_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:563c56bdbbee395a6ef7f0c51c8ac9223c162e517b4cdba0d4654e8de27c98d8
+size 9189263
diff --git a/projects/bloom/resources/Sponza/sponza_curtain_green_diff.png b/projects/bloom/resources/Sponza/sponza_curtain_green_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..6c9b6391a199407637fa71033d79fb58b8b4f0d7
--- /dev/null
+++ b/projects/bloom/resources/Sponza/sponza_curtain_green_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:238fe1c7f481388d1c1d578c2da8d411b99e8f0030ab62060a306db333124476
+size 8785458
diff --git a/projects/bloom/resources/Sponza/sponza_details_diff.png b/projects/bloom/resources/Sponza/sponza_details_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..12656686362c3e0a297e060491f33bd7351551f9
--- /dev/null
+++ b/projects/bloom/resources/Sponza/sponza_details_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cb1223b3bb82f8757e7df25a6891f1239cdd7ec59990340e952fb2d6b7ea570c
+size 1522643
diff --git a/projects/bloom/resources/Sponza/sponza_fabric_blue_diff.png b/projects/bloom/resources/Sponza/sponza_fabric_blue_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..879d16ef84722a4fc13e83a771778de326e4bc54
--- /dev/null
+++ b/projects/bloom/resources/Sponza/sponza_fabric_blue_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:467d290bf5d4b2a017da140ba9e244ed8a8a9be5418a9ac9bcb4ad572ae2d7ab
+size 2229440
diff --git a/projects/bloom/resources/Sponza/sponza_fabric_diff.png b/projects/bloom/resources/Sponza/sponza_fabric_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..3311287a219d2148620b87fe428fea071688d051
--- /dev/null
+++ b/projects/bloom/resources/Sponza/sponza_fabric_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1594f59cc2848db26add47361f4e665e3d8afa147760ed915d839fea42b20287
+size 2267382
diff --git a/projects/bloom/resources/Sponza/sponza_fabric_green_diff.png b/projects/bloom/resources/Sponza/sponza_fabric_green_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..de110f369004388dae4cd5067c63428db3a07834
--- /dev/null
+++ b/projects/bloom/resources/Sponza/sponza_fabric_green_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:902b87faab221173bf370cea7c74cb9060b4d870ac6316b190dafded1cb12993
+size 2258220
diff --git a/projects/bloom/resources/Sponza/sponza_flagpole_diff.png b/projects/bloom/resources/Sponza/sponza_flagpole_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..5f6e0812a0df80346318baa3cb50a6888afc58f8
--- /dev/null
+++ b/projects/bloom/resources/Sponza/sponza_flagpole_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bfffb62e770959c725d0f3db6dc7dbdd46a380ec55ef884dab94d44ca017b438
+size 1425673
diff --git a/projects/bloom/resources/Sponza/sponza_floor_a_diff.png b/projects/bloom/resources/Sponza/sponza_floor_a_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..788ed764f79ba724f04a2d603076a5b85013e188
--- /dev/null
+++ b/projects/bloom/resources/Sponza/sponza_floor_a_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a16f9230fa91f9f31dfca6216ce205f1ef132d44f3b012fbf6efc0fba69770ab
+size 1996838
diff --git a/projects/bloom/resources/Sponza/sponza_roof_diff.png b/projects/bloom/resources/Sponza/sponza_roof_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..c5b84261fdd1cc776a94b3ce398c7806b895f9a3
--- /dev/null
+++ b/projects/bloom/resources/Sponza/sponza_roof_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7fc412138c20da19f8173e53545e771f4652558dff624d4dc67143e40efe562b
+size 2320533
diff --git a/projects/bloom/resources/Sponza/sponza_thorn_diff.png b/projects/bloom/resources/Sponza/sponza_thorn_diff.png
new file mode 100644
index 0000000000000000000000000000000000000000..7a9142674a7d4a6f94a48c5152cf0300743b597a
--- /dev/null
+++ b/projects/bloom/resources/Sponza/sponza_thorn_diff.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a73a17c883cd0d0d67cfda2dc4118400a916366c05b9a5ac465f0c8b30fd9c8e
+size 635001
diff --git a/projects/bloom/resources/Sponza/vase_dif.png b/projects/bloom/resources/Sponza/vase_dif.png
new file mode 100644
index 0000000000000000000000000000000000000000..61236a81cb324af8797b05099cd264cefe189e56
--- /dev/null
+++ b/projects/bloom/resources/Sponza/vase_dif.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:53d06f52bf9e59df4cf00237707cca76c4f692bda61a62b06a30d321311d6dd9
+size 1842101
diff --git a/projects/bloom/resources/Sponza/vase_hanging.png b/projects/bloom/resources/Sponza/vase_hanging.png
new file mode 100644
index 0000000000000000000000000000000000000000..36a3cee71d8213225090c74f8c0dce33b9d44378
--- /dev/null
+++ b/projects/bloom/resources/Sponza/vase_hanging.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a9d10b4f27a3c9a78d5bac882fdd4b6a6987c262f48fa490670fe5e235951e31
+size 1432804
diff --git a/projects/bloom/resources/Sponza/vase_plant.png b/projects/bloom/resources/Sponza/vase_plant.png
new file mode 100644
index 0000000000000000000000000000000000000000..7ad95e702e229f1ebd803e5203a266d15f2c07b9
--- /dev/null
+++ b/projects/bloom/resources/Sponza/vase_plant.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d2087371ff02212fb7014b6daefa191cf5676d2227193fff261a5d02f554cb8e
+size 998089
diff --git a/projects/bloom/resources/Sponza/vase_round.png b/projects/bloom/resources/Sponza/vase_round.png
new file mode 100644
index 0000000000000000000000000000000000000000..c17953abc000c44b8991e23c136c2b67348f3d1b
--- /dev/null
+++ b/projects/bloom/resources/Sponza/vase_round.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:aa23d48d492d5d4ada2ddb27d1ef22952b214e6eb3b301c65f9d88442723d20a
+size 1871399
diff --git a/projects/bloom/resources/shaders/comp.spv b/projects/bloom/resources/shaders/comp.spv
new file mode 100644
index 0000000000000000000000000000000000000000..85c7e74cfc0a89917bf6dd1a7ec449368274c1d3
Binary files /dev/null and b/projects/bloom/resources/shaders/comp.spv differ
diff --git a/projects/bloom/resources/shaders/composite.comp b/projects/bloom/resources/shaders/composite.comp
new file mode 100644
index 0000000000000000000000000000000000000000..190bed0657d70e0217bf654820d0b2b2c58f12c2
--- /dev/null
+++ b/projects/bloom/resources/shaders/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.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/bloom/resources/shaders/downsample.comp b/projects/bloom/resources/shaders/downsample.comp
new file mode 100644
index 0000000000000000000000000000000000000000..2ab00c7c92798769153634f3479c5b7f3fb61d94
--- /dev/null
+++ b/projects/bloom/resources/shaders/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/bloom/resources/shaders/gammaCorrection.comp b/projects/bloom/resources/shaders/gammaCorrection.comp
new file mode 100644
index 0000000000000000000000000000000000000000..f89ad167c846cca8e80f69d33eda83bd6ed00d46
--- /dev/null
+++ b/projects/bloom/resources/shaders/gammaCorrection.comp
@@ -0,0 +1,20 @@
+#version 440
+
+layout(set=0, binding=0, r11f_g11f_b10f)    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;
+    // cheap Reinhard tone mapping
+    linearColor = linearColor/(linearColor + 1.0f);
+    vec3 gammaCorrected = pow(linearColor, vec3(1.f / 2.2f));
+    imageStore(outImage, uv, vec4(gammaCorrected, 0.f));
+}
\ No newline at end of file
diff --git a/projects/bloom/resources/shaders/lensFlares.comp b/projects/bloom/resources/shaders/lensFlares.comp
new file mode 100644
index 0000000000000000000000000000000000000000..ce27d8850b709f61332d467914ddc944dc63109f
--- /dev/null
+++ b/projects/bloom/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/bloom/resources/shaders/perMeshResources.inc b/projects/bloom/resources/shaders/perMeshResources.inc
new file mode 100644
index 0000000000000000000000000000000000000000..95e4fb7c27009965659d14a9c72acfec950c37e3
--- /dev/null
+++ b/projects/bloom/resources/shaders/perMeshResources.inc
@@ -0,0 +1,2 @@
+layout(set=1, binding=0) uniform texture2D  albedoTexture;
+layout(set=1, binding=1) uniform sampler    textureSampler;
\ No newline at end of file
diff --git a/projects/bloom/resources/shaders/shader.frag b/projects/bloom/resources/shaders/shader.frag
new file mode 100644
index 0000000000000000000000000000000000000000..3e95b4508f112c1ed9aa4a7050a98fa789dccd09
--- /dev/null
+++ b/projects/bloom/resources/shaders/shader.frag
@@ -0,0 +1,45 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+#extension GL_GOOGLE_include_directive : enable
+
+#include "perMeshResources.inc"
+
+layout(location = 0) in vec3 passNormal;
+layout(location = 1) in vec2 passUV;
+layout(location = 2) in vec3 passPos;
+
+layout(location = 0) out vec3 outColor;
+
+layout(set=0, binding=0) uniform sunBuffer {
+    vec3 L; float padding;
+    mat4 lightMatrix;
+};
+layout(set=0, binding=1) uniform texture2D  shadowMap;
+layout(set=0, binding=2) uniform sampler    shadowMapSampler;
+
+float shadowTest(vec3 worldPos){
+    vec4 lightPos = lightMatrix * vec4(worldPos, 1);
+    lightPos /= lightPos.w;
+    lightPos.xy = lightPos.xy * 0.5 + 0.5;
+    
+    if(any(lessThan(lightPos.xy, vec2(0))) || any(greaterThan(lightPos.xy, vec2(1)))){
+        return 1;
+    }
+    
+    lightPos.z = clamp(lightPos.z, 0, 1);
+    
+    float shadowMapSample = texture(sampler2D(shadowMap, shadowMapSampler), lightPos.xy).r;
+    float bias = 0.01f;
+    shadowMapSample += bias;
+    return shadowMapSample < lightPos.z ? 0 : 1;
+}
+
+void main()	{
+    vec3 N = normalize(passNormal);
+    vec3 sunColor = vec3(10);
+    vec3 sun = sunColor * clamp(dot(N, L), 0, 1);
+    sun *= shadowTest(passPos);
+    vec3 ambient = vec3(0.05);
+    vec3 albedo = texture(sampler2D(albedoTexture, textureSampler), passUV).rgb;
+	outColor = albedo * (sun + ambient);
+}
\ No newline at end of file
diff --git a/projects/bloom/resources/shaders/shader.vert b/projects/bloom/resources/shaders/shader.vert
new file mode 100644
index 0000000000000000000000000000000000000000..926f86af2860cb57c44d2d5ee78712b6ae155e5c
--- /dev/null
+++ b/projects/bloom/resources/shaders/shader.vert
@@ -0,0 +1,22 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(location = 0) in vec3 inPosition;
+layout(location = 1) in vec3 inNormal;
+layout(location = 2) in vec2 inUV;
+
+layout(location = 0) out vec3 passNormal;
+layout(location = 1) out vec2 passUV;
+layout(location = 2) out vec3 passPos;
+
+layout( push_constant ) uniform constants{
+    mat4 mvp;
+    mat4 model;
+};
+
+void main()	{
+	gl_Position = mvp * vec4(inPosition, 1.0);
+	passNormal  = mat3(model) * inNormal;    // assuming no weird stuff like shearing or non-uniform scaling
+    passUV      = inUV;
+    passPos     = (model * vec4(inPosition, 1)).xyz;
+}
\ No newline at end of file
diff --git a/projects/bloom/resources/shaders/shadow.frag b/projects/bloom/resources/shaders/shadow.frag
new file mode 100644
index 0000000000000000000000000000000000000000..848f853f556660b4900b5db7fb6fc98d57c1cd5b
--- /dev/null
+++ b/projects/bloom/resources/shaders/shadow.frag
@@ -0,0 +1,6 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+void main()	{
+
+}
\ No newline at end of file
diff --git a/projects/bloom/resources/shaders/shadow.vert b/projects/bloom/resources/shaders/shadow.vert
new file mode 100644
index 0000000000000000000000000000000000000000..e0f41d42d575fa64fedbfa04adf89ac0f4aeebe8
--- /dev/null
+++ b/projects/bloom/resources/shaders/shadow.vert
@@ -0,0 +1,12 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(location = 0) in vec3 inPosition;
+
+layout( push_constant ) uniform constants{
+    mat4 mvp;
+};
+
+void main()	{
+	gl_Position = mvp * vec4(inPosition, 1.0);
+}
\ No newline at end of file
diff --git a/projects/bloom/resources/shaders/upsample.comp b/projects/bloom/resources/shaders/upsample.comp
new file mode 100644
index 0000000000000000000000000000000000000000..0ddeedb5b5af9e476dc19012fed6430544006c0e
--- /dev/null
+++ b/projects/bloom/resources/shaders/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/bloom/src/BloomAndFlares.cpp b/projects/bloom/src/BloomAndFlares.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6f26db9de0f2c8334b6dd7e5dd6cf4b6f48baedc
--- /dev/null
+++ b/projects/bloom/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/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,
+                     "resources/shaders/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,
+                     "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/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 < 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/bloom/src/BloomAndFlares.hpp b/projects/bloom/src/BloomAndFlares.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..756b1ca154ea5232df04eb09a88bb743c5bd28aa
--- /dev/null
+++ b/projects/bloom/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/bloom/src/main.cpp b/projects/bloom/src/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..7a17a51f1c7d638575c0b5aafcdca49b589533ef
--- /dev/null
+++ b/projects/bloom/src/main.cpp
@@ -0,0 +1,419 @@
+#include <iostream>
+#include <vkcv/Core.hpp>
+#include <GLFW/glfw3.h>
+#include <vkcv/camera/CameraManager.hpp>
+#include <chrono>
+#include <vkcv/asset/asset_loader.hpp>
+#include <vkcv/shader/GLSLCompiler.hpp>
+#include <vkcv/Logger.hpp>
+#include "BloomAndFlares.hpp"
+#include <glm/glm.hpp>
+
+int main(int argc, const char** argv) {
+	const char* applicationName = "Bloom";
+
+	uint32_t windowWidth = 1920;
+	uint32_t windowHeight = 1080;
+	
+	vkcv::Window window = vkcv::Window::create(
+		applicationName,
+		windowWidth,
+		windowHeight,
+		true
+	);
+
+    vkcv::camera::CameraManager cameraManager(window);
+    uint32_t camIndex = cameraManager.addCamera(vkcv::camera::ControllerType::PILOT);
+    uint32_t camIndex2 = cameraManager.addCamera(vkcv::camera::ControllerType::TRACKBALL);
+    
+    cameraManager.getCamera(camIndex).setPosition(glm::vec3(0.f, 0.f, 3.f));
+    cameraManager.getCamera(camIndex).setNearFar(0.1f, 30.0f);
+	cameraManager.getCamera(camIndex).setYaw(180.0f);
+	
+	cameraManager.getCamera(camIndex2).setNearFar(0.1f, 30.0f);
+
+	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" }
+	);
+
+	const char* path = argc > 1 ? argv[1] : "resources/Sponza/Sponza.gltf";
+	vkcv::asset::Scene scene;
+	int result = vkcv::asset::loadScene(path, scene);
+
+	if (result == 1) {
+		std::cout << "Scene loading successful!" << std::endl;
+	}
+	else {
+		std::cout << "Scene loading failed: " << result << std::endl;
+		return 1;
+	}
+
+	// build index and vertex buffers
+	assert(!scene.vertexGroups.empty());
+	std::vector<std::vector<uint8_t>> vBuffers;
+	std::vector<std::vector<uint8_t>> iBuffers;
+
+	std::vector<vkcv::VertexBufferBinding> vBufferBindings;
+	std::vector<std::vector<vkcv::VertexBufferBinding>> vertexBufferBindings;
+	std::vector<vkcv::asset::VertexAttribute> vAttributes;
+
+	for (int i = 0; i < scene.vertexGroups.size(); i++) {
+
+		vBuffers.push_back(scene.vertexGroups[i].vertexBuffer.data);
+		iBuffers.push_back(scene.vertexGroups[i].indexBuffer.data);
+
+		auto& attributes = scene.vertexGroups[i].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);
+		});
+	}
+
+	std::vector<vkcv::Buffer<uint8_t>> vertexBuffers;
+	for (const vkcv::asset::VertexGroup& group : scene.vertexGroups) {
+		vertexBuffers.push_back(core.createBuffer<uint8_t>(
+			vkcv::BufferType::VERTEX,
+			group.vertexBuffer.data.size()));
+		vertexBuffers.back().fill(group.vertexBuffer.data);
+	}
+
+	std::vector<vkcv::Buffer<uint8_t>> indexBuffers;
+	for (const auto& dataBuffer : iBuffers) {
+		indexBuffers.push_back(core.createBuffer<uint8_t>(
+			vkcv::BufferType::INDEX,
+			dataBuffer.size()));
+		indexBuffers.back().fill(dataBuffer);
+	}
+
+	int vertexBufferIndex = 0;
+	for (const auto& vertexGroup : scene.vertexGroups) {
+		for (const auto& attribute : vertexGroup.vertexBuffer.attributes) {
+			vAttributes.push_back(attribute);
+			vBufferBindings.push_back(vkcv::VertexBufferBinding(attribute.offset, vertexBuffers[vertexBufferIndex].getVulkanHandle()));
+		}
+		vertexBufferBindings.push_back(vBufferBindings);
+		vBufferBindings.clear();
+		vertexBufferIndex++;
+	}
+
+	const vk::Format colorBufferFormat = vk::Format::eB10G11R11UfloatPack32;
+	const vkcv::AttachmentDescription color_attachment(
+		vkcv::AttachmentOperation::STORE,
+		vkcv::AttachmentOperation::CLEAR,
+		colorBufferFormat
+	);
+	
+	const vk::Format depthBufferFormat = vk::Format::eD32Sfloat;
+	const vkcv::AttachmentDescription depth_attachment(
+		vkcv::AttachmentOperation::STORE,
+		vkcv::AttachmentOperation::CLEAR,
+		depthBufferFormat
+	);
+
+	vkcv::PassConfig forwardPassDefinition({ color_attachment, depth_attachment });
+	vkcv::PassHandle forwardPass = core.createPass(forwardPassDefinition);
+
+	vkcv::shader::GLSLCompiler compiler;
+
+	vkcv::ShaderProgram forwardProgram;
+	compiler.compile(vkcv::ShaderStage::VERTEX, std::filesystem::path("resources/shaders/shader.vert"), 
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		forwardProgram.addShader(shaderStage, path);
+	});
+	compiler.compile(vkcv::ShaderStage::FRAGMENT, std::filesystem::path("resources/shaders/shader.frag"),
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		forwardProgram.addShader(shaderStage, path);
+	});
+
+	const std::vector<vkcv::VertexAttachment> vertexAttachments = forwardProgram.getVertexAttachments();
+
+	std::vector<vkcv::VertexBinding> vertexBindings;
+	for (size_t i = 0; i < vertexAttachments.size(); i++) {
+		vertexBindings.push_back(vkcv::VertexBinding(i, { vertexAttachments[i] }));
+	}
+	const vkcv::VertexLayout vertexLayout (vertexBindings);
+
+	// shadow map
+	vkcv::SamplerHandle shadowSampler = core.createSampler(
+		vkcv::SamplerFilterType::NEAREST,
+		vkcv::SamplerFilterType::NEAREST,
+		vkcv::SamplerMipmapMode::NEAREST,
+		vkcv::SamplerAddressMode::CLAMP_TO_EDGE
+	);
+	const vk::Format shadowMapFormat = vk::Format::eD16Unorm;
+	const uint32_t shadowMapResolution = 1024;
+	const vkcv::Image shadowMap = core.createImage(shadowMapFormat, shadowMapResolution, shadowMapResolution, 1);
+
+	// light info buffer
+	struct LightInfo {
+		glm::vec3 direction;
+		float padding;
+		glm::mat4 lightMatrix;
+	};
+	LightInfo lightInfo;
+	vkcv::Buffer lightBuffer = core.createBuffer<LightInfo>(vkcv::BufferType::UNIFORM, sizeof(glm::vec3));
+
+	vkcv::DescriptorSetHandle forwardShadingDescriptorSet = 
+		core.createDescriptorSet({ forwardProgram.getReflectedDescriptors()[0] });
+
+	vkcv::DescriptorWrites forwardDescriptorWrites;
+	forwardDescriptorWrites.uniformBufferWrites = { vkcv::UniformBufferDescriptorWrite(0, lightBuffer.getHandle()) };
+	forwardDescriptorWrites.sampledImageWrites  = { vkcv::SampledImageDescriptorWrite(1, shadowMap.getHandle()) };
+	forwardDescriptorWrites.samplerWrites       = { vkcv::SamplerDescriptorWrite(2, shadowSampler) };
+	core.writeDescriptorSet(forwardShadingDescriptorSet, forwardDescriptorWrites);
+
+	vkcv::SamplerHandle colorSampler = core.createSampler(
+		vkcv::SamplerFilterType::LINEAR,
+		vkcv::SamplerFilterType::LINEAR,
+		vkcv::SamplerMipmapMode::LINEAR,
+		vkcv::SamplerAddressMode::REPEAT
+	);
+
+	// prepare per mesh descriptor sets
+	std::vector<vkcv::DescriptorSetHandle> perMeshDescriptorSets;
+	std::vector<vkcv::Image> sceneImages;
+	for (const auto& vertexGroup : scene.vertexGroups) {
+		perMeshDescriptorSets.push_back(core.createDescriptorSet(forwardProgram.getReflectedDescriptors()[1]));
+
+		const auto& material = scene.materials[vertexGroup.materialIndex];
+
+		int baseColorIndex = material.baseColor;
+		if (baseColorIndex < 0) {
+			vkcv_log(vkcv::LogLevel::WARNING, "Material lacks base color");
+			baseColorIndex = 0;
+		}
+
+		vkcv::asset::Texture& sceneTexture = scene.textures[baseColorIndex];
+
+		sceneImages.push_back(core.createImage(vk::Format::eR8G8B8A8Srgb, sceneTexture.w, sceneTexture.h));
+		sceneImages.back().fill(sceneTexture.data.data());
+
+		vkcv::DescriptorWrites setWrites;
+		setWrites.sampledImageWrites = {
+			vkcv::SampledImageDescriptorWrite(0, sceneImages.back().getHandle())
+		};
+		setWrites.samplerWrites = {
+			vkcv::SamplerDescriptorWrite(1, colorSampler),
+		};
+		core.writeDescriptorSet(perMeshDescriptorSets.back(), setWrites);
+	}
+
+	const vkcv::PipelineConfig forwardPipelineConfig {
+		forwardProgram,
+		windowWidth,
+		windowHeight,
+		forwardPass,
+		vertexLayout,
+		{	core.getDescriptorSet(forwardShadingDescriptorSet).layout, 
+			core.getDescriptorSet(perMeshDescriptorSets[0]).layout },
+		true
+	};
+	
+	vkcv::PipelineHandle forwardPipeline = core.createGraphicsPipeline(forwardPipelineConfig);
+	
+	if (!forwardPipeline) {
+		std::cout << "Error. Could not create graphics pipeline. Exiting." << std::endl;
+		return EXIT_FAILURE;
+	}
+
+	vkcv::ImageHandle depthBuffer       = core.createImage(depthBufferFormat, windowWidth, windowHeight).getHandle();
+	vkcv::ImageHandle colorBuffer       = core.createImage(colorBufferFormat, windowWidth, windowHeight, 1, false, true, true).getHandle();
+
+	const vkcv::ImageHandle swapchainInput = vkcv::ImageHandle::createSwapchainImageHandle();
+
+	vkcv::ShaderProgram shadowShader;
+	compiler.compile(vkcv::ShaderStage::VERTEX, "resources/shaders/shadow.vert",
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		shadowShader.addShader(shaderStage, path);
+	});
+	compiler.compile(vkcv::ShaderStage::FRAGMENT, "resources/shaders/shadow.frag",
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		shadowShader.addShader(shaderStage, path);
+	});
+
+	const std::vector<vkcv::AttachmentDescription> shadowAttachments = {
+		vkcv::AttachmentDescription(vkcv::AttachmentOperation::STORE, vkcv::AttachmentOperation::CLEAR, shadowMapFormat)
+	};
+	const vkcv::PassConfig shadowPassConfig(shadowAttachments);
+	const vkcv::PassHandle shadowPass = core.createPass(shadowPassConfig);
+	const vkcv::PipelineConfig shadowPipeConfig{
+		shadowShader,
+		shadowMapResolution,
+		shadowMapResolution,
+		shadowPass,
+		vertexLayout,
+		{},
+		false
+	};
+	const vkcv::PipelineHandle shadowPipe = core.createGraphicsPipeline(shadowPipeConfig);
+
+	std::vector<std::array<glm::mat4, 2>> mainPassMatrices;
+	std::vector<glm::mat4> mvpLight;
+
+	// gamma correction compute shader
+	vkcv::ShaderProgram gammaCorrectionProgram;
+	compiler.compile(vkcv::ShaderStage::COMPUTE, "resources/shaders/gammaCorrection.comp",
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		gammaCorrectionProgram.addShader(shaderStage, path);
+	});
+	vkcv::DescriptorSetHandle gammaCorrectionDescriptorSet = core.createDescriptorSet(gammaCorrectionProgram.getReflectedDescriptors()[0]);
+	vkcv::PipelineHandle gammaCorrectionPipeline = core.createComputePipeline(gammaCorrectionProgram,
+		{ core.getDescriptorSet(gammaCorrectionDescriptorSet).layout });
+
+    BloomAndFlares baf(&core, colorBufferFormat, windowWidth, windowHeight);
+
+
+	// model matrices per mesh
+	std::vector<glm::mat4> modelMatrices;
+	modelMatrices.resize(scene.vertexGroups.size(), glm::mat4(1.f));
+	for (const auto& mesh : scene.meshes) {
+		const glm::mat4 m = *reinterpret_cast<const glm::mat4*>(&mesh.modelMatrix[0]);
+		for (const auto& vertexGroupIndex : mesh.vertexGroups) {
+			modelMatrices[vertexGroupIndex] = m;
+		}
+	}
+
+	// prepare drawcalls
+	std::vector<vkcv::Mesh> meshes;
+	for (int i = 0; i < scene.vertexGroups.size(); i++) {
+		vkcv::Mesh mesh(
+			vertexBufferBindings[i], 
+			indexBuffers[i].getVulkanHandle(), 
+			scene.vertexGroups[i].numIndices);
+		meshes.push_back(mesh);
+	}
+
+	std::vector<vkcv::DrawcallInfo> drawcalls;
+	std::vector<vkcv::DrawcallInfo> shadowDrawcalls;
+	for (int i = 0; i < meshes.size(); i++) {
+		drawcalls.push_back(vkcv::DrawcallInfo(meshes[i], { 
+			vkcv::DescriptorSetUsage(0, core.getDescriptorSet(forwardShadingDescriptorSet).vulkanHandle),
+			vkcv::DescriptorSetUsage(1, core.getDescriptorSet(perMeshDescriptorSets[i]).vulkanHandle) }));
+		shadowDrawcalls.push_back(vkcv::DrawcallInfo(meshes[i], {}));
+	}
+
+	auto start = std::chrono::system_clock::now();
+	const auto appStartTime = start;
+	while (window.isWindowOpen()) {
+		vkcv::Window::pollEvents();
+
+		uint32_t swapchainWidth, swapchainHeight;
+		if (!core.beginFrame(swapchainWidth, swapchainHeight)) {
+			continue;
+		}
+
+		if ((swapchainWidth != windowWidth) || ((swapchainHeight != windowHeight))) {
+			depthBuffer = core.createImage(depthBufferFormat, swapchainWidth, swapchainHeight).getHandle();
+			colorBuffer = core.createImage(colorBufferFormat, swapchainWidth, swapchainHeight, 1, false, true, true).getHandle();
+
+			baf.updateImageDimensions(swapchainWidth, swapchainHeight);
+
+			windowWidth = swapchainWidth;
+			windowHeight = swapchainHeight;
+		}
+
+		auto end = std::chrono::system_clock::now();
+		auto deltatime = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
+
+		start = end;
+		cameraManager.update(0.000001 * static_cast<double>(deltatime.count()));
+
+		auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - appStartTime);
+		
+		const float sunTheta = 0.0001f * static_cast<float>(duration.count());
+		lightInfo.direction = glm::normalize(glm::vec3(std::cos(sunTheta), 1, std::sin(sunTheta)));
+
+		const float shadowProjectionSize = 20.f;
+		glm::mat4 projectionLight = glm::ortho(
+			-shadowProjectionSize,
+			shadowProjectionSize,
+			-shadowProjectionSize,
+			shadowProjectionSize,
+			-shadowProjectionSize,
+			shadowProjectionSize);
+
+		glm::mat4 vulkanCorrectionMatrix(1.f);
+		vulkanCorrectionMatrix[2][2] = 0.5;
+		vulkanCorrectionMatrix[3][2] = 0.5;
+		projectionLight = vulkanCorrectionMatrix * projectionLight;
+
+		const glm::mat4 viewLight = glm::lookAt(glm::vec3(0), -lightInfo.direction, glm::vec3(0, -1, 0));
+
+		lightInfo.lightMatrix = projectionLight * viewLight;
+		lightBuffer.fill({ lightInfo });
+
+		const glm::mat4 viewProjectionCamera = cameraManager.getActiveCamera().getMVP();
+
+		mainPassMatrices.clear();
+		mvpLight.clear();
+		for (const auto& m : modelMatrices) {
+			mainPassMatrices.push_back({ viewProjectionCamera * m, m });
+			mvpLight.push_back(lightInfo.lightMatrix * m);
+		}
+
+		vkcv::PushConstantData pushConstantData((void*)mainPassMatrices.data(), 2 * sizeof(glm::mat4));
+		const std::vector<vkcv::ImageHandle> renderTargets = { colorBuffer, depthBuffer };
+
+		const vkcv::PushConstantData shadowPushConstantData((void*)mvpLight.data(), sizeof(glm::mat4));
+
+		auto cmdStream = core.createCommandStream(vkcv::QueueType::Graphics);
+
+		// shadow map
+		core.recordDrawcallsToCmdStream(
+			cmdStream,
+			shadowPass,
+			shadowPipe,
+			shadowPushConstantData,
+			shadowDrawcalls,
+			{ shadowMap.getHandle() });
+		core.prepareImageForSampling(cmdStream, shadowMap.getHandle());
+
+		// main pass
+		core.recordDrawcallsToCmdStream(
+			cmdStream,
+            forwardPass,
+            forwardPipeline,
+			pushConstantData,
+			drawcalls,
+			renderTargets);
+
+        const uint32_t gammaCorrectionLocalGroupSize = 8;
+        const uint32_t gammaCorrectionDispatchCount[3] = {
+                static_cast<uint32_t>(glm::ceil(static_cast<float>(windowWidth) / static_cast<float>(gammaCorrectionLocalGroupSize))),
+                static_cast<uint32_t>(glm::ceil(static_cast<float>(windowHeight) / static_cast<float>(gammaCorrectionLocalGroupSize))),
+                1
+        };
+
+        baf.execWholePipeline(cmdStream, colorBuffer);
+
+        core.prepareImageForStorage(cmdStream, swapchainInput);
+        
+        // gamma correction descriptor write
+        vkcv::DescriptorWrites gammaCorrectionDescriptorWrites;
+        gammaCorrectionDescriptorWrites.storageImageWrites = {
+                vkcv::StorageImageDescriptorWrite(0, colorBuffer),
+                vkcv::StorageImageDescriptorWrite(1, swapchainInput) };
+        core.writeDescriptorSet(gammaCorrectionDescriptorSet, gammaCorrectionDescriptorWrites);
+
+        // gamma correction dispatch
+        core.recordComputeDispatchToCmdStream(
+			cmdStream, 
+			gammaCorrectionPipeline, 
+			gammaCorrectionDispatchCount,
+			{ vkcv::DescriptorSetUsage(0, core.getDescriptorSet(gammaCorrectionDescriptorSet).vulkanHandle) },
+			vkcv::PushConstantData(nullptr, 0));
+
+		// present and end
+		core.prepareSwapchainImageForPresent(cmdStream);
+		core.submitCommandStream(cmdStream);
+
+		core.endFrame();
+	}
+	
+	return 0;
+}
diff --git a/src/vkcv/DescriptorManager.cpp b/src/vkcv/DescriptorManager.cpp
index 265532232304106f7271fdd445d52074b7c011a1..8e565a766cd407dc33c0291d3d07b01d6d3066e7 100644
--- a/src/vkcv/DescriptorManager.cpp
+++ b/src/vkcv/DescriptorManager.cpp
@@ -107,10 +107,11 @@ namespace vkcv
 		std::vector<WriteDescriptorSetInfo> writeInfos;
 
 		for (const auto& write : writes.sampledImageWrites) {
+		    vk::ImageLayout layout = write.useGeneralLayout ? vk::ImageLayout::eGeneral : vk::ImageLayout::eShaderReadOnlyOptimal;
 			const vk::DescriptorImageInfo imageInfo(
 				nullptr,
-				imageManager.getVulkanImageView(write.image),
-				vk::ImageLayout::eShaderReadOnlyOptimal
+				imageManager.getVulkanImageView(write.image, write.mipLevel),
+                layout
 			);
 			
 			imageInfos.push_back(imageInfo);
diff --git a/src/vkcv/Swapchain.cpp b/src/vkcv/Swapchain.cpp
index 33714adac7cec7c1b5e0013387424c4f865454ab..2c5b3530c396bc3532aa94cb59a120e3555291bf 100644
--- a/src/vkcv/Swapchain.cpp
+++ b/src/vkcv/Swapchain.cpp
@@ -1,7 +1,6 @@
 #include <vkcv/Swapchain.hpp>
 #include <utility>
 
-#define GLFW_INCLUDE_VULKAN
 #include <GLFW/glfw3.h>
 
 namespace vkcv