diff --git a/.gitmodules b/.gitmodules
index cc3bf1fcd2e1eb8117cbcc7222b04f7041fea520..e270bddb0a91069af1eff869560e8b817f7cc844 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -28,3 +28,12 @@
 [submodule "modules/upscaling/lib/FidelityFX-FSR"]
 	path = modules/upscaling/lib/FidelityFX-FSR
 	url = https://github.com/GPUOpen-Effects/FidelityFX-FSR.git
+[submodule "lib/Vulkan-Headers"]
+	path = lib/Vulkan-Headers
+	url = https://github.com/KhronosGroup/Vulkan-Headers.git
+[submodule "lib/Vulkan-Hpp"]
+	path = lib/Vulkan-Hpp
+	url = https://github.com/KhronosGroup/Vulkan-Hpp
+[submodule "modules/upscaling/lib/NVIDIAImageScaling"]
+	path = modules/upscaling/lib/NVIDIAImageScaling
+	url = https://github.com/NVIDIAGameWorks/NVIDIAImageScaling.git
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 121d499fb2cd337c28524b89ecf1ab9d12607bdf..d47706415b6274a135a4c85aa1010fde2e959105 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -100,6 +100,7 @@ endif()
 include(${vkcv_config}/Sources.cmake)
 
 message(STATUS "Framework:")
+message(" - Includes: [ ${vkcv_includes} ]")
 message(" - Libraries: [ ${vkcv_libraries} ]")
 message(" - Flags: [ ${vkcv_flags} ]")
 
diff --git a/Doxyfile b/Doxyfile
index 1bc2863556f9d9de0c0a54d590c4a58c9f9ca2e3..728050856484c09c072b16c96116fabe2125917f 100644
--- a/Doxyfile
+++ b/Doxyfile
@@ -1,4 +1,4 @@
-# Doxyfile 1.9.2
+# Doxyfile 1.9.3
 
 # This file describes the settings to be used by the documentation system
 # doxygen (www.doxygen.org) for a project.
@@ -463,7 +463,7 @@ LOOKUP_CACHE_SIZE      = 0
 # DOT_NUM_THREADS setting.
 # Minimum value: 0, maximum value: 32, default value: 1.
 
-NUM_PROC_THREADS       = 1
+NUM_PROC_THREADS       = 0
 
 #---------------------------------------------------------------------------
 # Build related configuration options
@@ -857,7 +857,10 @@ WARN_FORMAT            = "$file:$line: $text"
 
 # The WARN_LOGFILE tag can be used to specify a file to which warning and error
 # messages should be written. If left blank the output is written to standard
-# error (stderr).
+# error (stderr). In case the file specified cannot be opened for writing the
+# warning and error messages are written to standard error. When as file - is
+# specified the warning and error messages are written to standard output
+# (stdout).
 
 WARN_LOGFILE           =
 
@@ -983,7 +986,7 @@ EXCLUDE_PATTERNS       = */lib/*
 # (namespaces, classes, functions, etc.) that should be excluded from the
 # output. The symbol name can be a fully qualified name, a word, or if the
 # wildcard * is used, a substring. Examples: ANamespace, AClass,
-# AClass::ANamespace, ANamespace::*Test
+# ANamespace::AClass, ANamespace::*Test
 #
 # Note that the wildcards are matched against the file with absolute path, so to
 # exclude all test directories use the pattern */test/*
@@ -1360,6 +1363,13 @@ GENERATE_DOCSET        = NO
 
 DOCSET_FEEDNAME        = "Doxygen generated docs"
 
+# This tag determines the URL of the docset feed. A documentation feed provides
+# an umbrella under which multiple documentation sets from a single provider
+# (such as a company or product suite) can be grouped.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_FEEDURL         =
+
 # This tag specifies a string that should uniquely identify the documentation
 # set bundle. This should be a reverse domain-name style string, e.g.
 # com.mycompany.MyDocSet. Doxygen will append .docset to the name.
@@ -1564,7 +1574,7 @@ GENERATE_TREEVIEW      = YES
 # area (value NO) or if it should extend to the full height of the window (value
 # YES). Setting this to YES gives a layout similar to
 # https://docs.readthedocs.io with more room for contents, but less room for the
-# project logo, title, and description. If either GENERATOR_TREEVIEW or
+# project logo, title, and description. If either GENERATE_TREEVIEW or
 # DISABLE_INDEX is set to NO, this option has no effect.
 # The default value is: NO.
 # This tag requires that the tag GENERATE_HTML is set to YES.
@@ -1595,6 +1605,13 @@ TREEVIEW_WIDTH         = 250
 
 EXT_LINKS_IN_WINDOW    = NO
 
+# If the OBFUSCATE_EMAILS tag is set to YES, doxygen will obfuscate email
+# addresses.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+OBFUSCATE_EMAILS       = YES
+
 # If the HTML_FORMULA_FORMAT option is set to svg, doxygen will use the pdf2svg
 # tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see
 # https://inkscape.org) to generate formulas as SVG images instead of PNGs for
@@ -2308,15 +2325,6 @@ EXTERNAL_PAGES         = YES
 # Configuration options related to the dot tool
 #---------------------------------------------------------------------------
 
-# If the CLASS_DIAGRAMS tag is set to YES, doxygen will generate a class diagram
-# (in HTML and LaTeX) for classes with base or super classes. Setting the tag to
-# NO turns the diagrams off. Note that this option also works with HAVE_DOT
-# disabled, but it is recommended to install and use dot, since it yields more
-# powerful graphs.
-# The default value is: YES.
-
-CLASS_DIAGRAMS         = NO
-
 # You can include diagrams made with dia in doxygen documentation. Doxygen will
 # then run dia to produce the diagram and insert it in the documentation. The
 # DIA_PATH tag allows you to specify the directory where the dia binary resides.
@@ -2373,11 +2381,14 @@ DOT_FONTSIZE           = 10
 
 DOT_FONTPATH           =
 
-# If the CLASS_GRAPH tag is set to YES then doxygen will generate a graph for
-# each documented class showing the direct and indirect inheritance relations.
-# Setting this tag to YES will force the CLASS_DIAGRAMS tag to NO.
+# If the CLASS_GRAPH tag is set to YES (or GRAPH) then doxygen will generate a
+# graph for each documented class showing the direct and indirect inheritance
+# relations. In case HAVE_DOT is set as well dot will be used to draw the graph,
+# otherwise the built-in generator will be used. If the CLASS_GRAPH tag is set
+# to TEXT the direct and indirect inheritance relations will be shown as texts /
+# links.
+# Possible values are: NO, YES, TEXT and GRAPH.
 # The default value is: YES.
-# This tag requires that the tag HAVE_DOT is set to YES.
 
 CLASS_GRAPH            = YES
 
@@ -2506,6 +2517,13 @@ GRAPHICAL_HIERARCHY    = YES
 
 DIRECTORY_GRAPH        = YES
 
+# The DIR_GRAPH_MAX_DEPTH tag can be used to limit the maximum number of levels
+# of child directories generated in directory dependency graphs by dot.
+# Minimum value: 1, maximum value: 25, default value: 1.
+# This tag requires that the tag DIRECTORY_GRAPH is set to YES.
+
+DIR_GRAPH_MAX_DEPTH    = 1
+
 # The DOT_IMAGE_FORMAT tag can be used to set the image format of the images
 # generated by dot. For an explanation of the image formats see the section
 # output formats in the documentation of the dot tool (Graphviz (see:
@@ -2559,10 +2577,10 @@ MSCFILE_DIRS           =
 DIAFILE_DIRS           =
 
 # When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the
-# path where java can find the plantuml.jar file. If left blank, it is assumed
-# PlantUML is not used or called during a preprocessing step. Doxygen will
-# generate a warning when it encounters a \startuml command in this case and
-# will not generate output for the diagram.
+# path where java can find the plantuml.jar file or to the filename of jar file
+# to be used. If left blank, it is assumed PlantUML is not used or called during
+# a preprocessing step. Doxygen will generate a warning when it encounters a
+# \startuml command in this case and will not generate output for the diagram.
 
 PLANTUML_JAR_PATH      =
 
@@ -2624,6 +2642,8 @@ DOT_MULTI_TARGETS      = NO
 # If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page
 # explaining the meaning of the various boxes and arrows in the dot generated
 # graphs.
+# Note: This tag requires that UML_LOOK isn't set, i.e. the doxygen internal
+# graphical representation for inheritance and collaboration diagrams is used.
 # The default value is: YES.
 # This tag requires that the tag HAVE_DOT is set to YES.
 
diff --git a/README.md b/README.md
index 62efc4df4a929963f8581c1c2cc66796f4c6a80d..fa0d833ea8ba66c6851296a4e12b5d06d7ab0406 100644
--- a/README.md
+++ b/README.md
@@ -39,6 +39,7 @@ The following modules will be provided in this repository and they will automati
  - [Asset-Loader](modules/asset_loader/README.md)
  - [Camera](modules/asset_loader/README.md)
  - [GUI](modules/gui/README.md)
+ - [Effects](modules/effects/README.md)
  - [Material](modules/material/README.md)
  - [Meshlet](modules/meshlet/README.md)
  - [Scene](modules/scene/README.md)
diff --git a/config/lib/Vulkan.cmake b/config/lib/Vulkan.cmake
index e8fe4aee3a949fa7bf9906558f8a0c558eb47cf2..5100d1b78986a7a4b4b6180b1862046cb8b4fc8c 100644
--- a/config/lib/Vulkan.cmake
+++ b/config/lib/Vulkan.cmake
@@ -2,7 +2,26 @@
 find_package(Vulkan REQUIRED)
 
 if (Vulkan_FOUND)
-    list(APPEND vkcv_includes ${Vulkan_INCLUDE_DIR})
+    if (NOT EXISTS ${Vulkan_INCLUDE_DIR}/vulkan/vulkan.h)
+        use_git_submodule("${vkcv_lib_path}/Vulkan-Headers" vulkan_headers_status)
+        
+        if (${vulkan_headers_status})
+            list(APPEND vkcv_includes ${vkcv_lib}/Vulkan-Headers/include)
+        endif()
+    else()
+        list(APPEND vkcv_includes ${Vulkan_INCLUDE_DIR})
+    endif()
+
+    if (NOT EXISTS ${Vulkan_INCLUDE_DIR}/vulkan/vulkan.hpp)
+        use_git_submodule("${vkcv_lib_path}/Vulkan-Hpp" vulkan_hpp_status)
+    
+        if (${vulkan_hpp_status})
+            list(APPEND vkcv_includes ${vkcv_lib}/Vulkan-Hpp)
+        endif()
+    else()
+        list(APPEND vkcv_includes ${Vulkan_INCLUDE_DIR})
+    endif()
+    
     list(APPEND vkcv_libraries ${Vulkan_LIBRARIES})
 
     message(${vkcv_config_msg} " Vulkan  -   ")
diff --git a/include/vkcv/Core.hpp b/include/vkcv/Core.hpp
index 6343bf070fbc1003c4813d30cca30e4d32e19864..35c4756f0a32fc7ea789abbf872125437b208810 100644
--- a/include/vkcv/Core.hpp
+++ b/include/vkcv/Core.hpp
@@ -269,7 +269,7 @@ namespace vkcv
 		 * @return swapchain
 		 */
 		[[nodiscard]]
-		Swapchain& getSwapchain(const SwapchainHandle& handle);
+		Swapchain& getSwapchain(const SwapchainHandle &handle);
 
 		/**
 		 * Gets the swapchain handle from the window
@@ -277,7 +277,7 @@ namespace vkcv
 		 * @return the swapchain from getSwapchain( SwapchainHandle )
 		 */
 		[[nodiscard]]
-		Swapchain& getSwapchain(const WindowHandle& handle);
+		Swapchain& getSwapchain(const WindowHandle &handle);
 
 		/**
 		 * Returns the image width
@@ -285,7 +285,7 @@ namespace vkcv
 		 * @return imageWidth
 		 */
         [[nodiscard]]
-        uint32_t getImageWidth(const ImageHandle& image);
+        uint32_t getImageWidth(const ImageHandle &image);
 
         /**
          * Returns the image height
@@ -293,7 +293,7 @@ namespace vkcv
          * @return imageHeight
          */
         [[nodiscard]]
-        uint32_t getImageHeight(const ImageHandle& image);
+        uint32_t getImageHeight(const ImageHandle &image);
 
         /**
          * Returns the image format of the image
@@ -301,7 +301,16 @@ namespace vkcv
          * @return imageFormat
          */
 		[[nodiscard]]
-		vk::Format getImageFormat(const ImageHandle& image);
+		vk::Format getImageFormat(const ImageHandle &image);
+		
+		/**
+		 * @brief Returns the images amount of mip levels.
+		 *
+		 * @param image Image handle
+		 * @return Amount of mip levels
+		 */
+		[[nodiscard]]
+		uint32_t getImageMipLevels(const ImageHandle &image);
 
 		/**
 		 * @brief Creates a descriptor set layout handle by a set of descriptor bindings.
diff --git a/lib/Vulkan-Headers b/lib/Vulkan-Headers
new file mode 160000
index 0000000000000000000000000000000000000000..8ba8294c86d0e99fcb457bedbd573dd678ccc9b3
--- /dev/null
+++ b/lib/Vulkan-Headers
@@ -0,0 +1 @@
+Subproject commit 8ba8294c86d0e99fcb457bedbd573dd678ccc9b3
diff --git a/lib/Vulkan-Hpp b/lib/Vulkan-Hpp
new file mode 160000
index 0000000000000000000000000000000000000000..ae1b0c36df0943795cd621a37e7f7bfd00ac958a
--- /dev/null
+++ b/lib/Vulkan-Hpp
@@ -0,0 +1 @@
+Subproject commit ae1b0c36df0943795cd621a37e7f7bfd00ac958a
diff --git a/lib/VulkanMemoryAllocator-Hpp b/lib/VulkanMemoryAllocator-Hpp
index c6c3c665b6a29ae546bdec60606a3ef0757ea108..da6ea76eecf12a1decc76f58a3e096bcc555bd94 160000
--- a/lib/VulkanMemoryAllocator-Hpp
+++ b/lib/VulkanMemoryAllocator-Hpp
@@ -1 +1 @@
-Subproject commit c6c3c665b6a29ae546bdec60606a3ef0757ea108
+Subproject commit da6ea76eecf12a1decc76f58a3e096bcc555bd94
diff --git a/modules/CMakeLists.txt b/modules/CMakeLists.txt
index 4d89c52a039e9d5de9efb276396158e987f52118..0a7f0f0ab1b71f78c6a548caf3d82442a05dcb2b 100644
--- a/modules/CMakeLists.txt
+++ b/modules/CMakeLists.txt
@@ -5,6 +5,7 @@ set(vkcv_modules_libraries)
 # Add new modules here:
 add_subdirectory(asset_loader)
 add_subdirectory(camera)
+add_subdirectory(effects)
 add_subdirectory(gui)
 add_subdirectory(material)
 add_subdirectory(meshlet)
diff --git a/modules/asset_loader/src/vkcv/asset/asset_loader.cpp b/modules/asset_loader/src/vkcv/asset/asset_loader.cpp
index f3f55f6514a58e776abf226fd80805fbf656a809..110a941ff6703e989174eaf311118b10b41491d4 100644
--- a/modules/asset_loader/src/vkcv/asset/asset_loader.cpp
+++ b/modules/asset_loader/src/vkcv/asset/asset_loader.cpp
@@ -171,7 +171,7 @@ namespace vkcv::asset {
 	static std::array<float, 16> calculateModelMatrix(const std::array<float, 3>& translation,
 													  const std::array<float, 3>& scale,
 													  const std::array<float, 4>& rotation,
-													  const std::array<float, 16>& matrix){
+													  const std::array<float, 16>& matrix) {
 		std::array<float, 16> modelMatrix = {
 				1,0,0,0,
 				0,1,0,0,
@@ -185,32 +185,55 @@ namespace vkcv::asset {
 			// translation
 			modelMatrix[3] = translation[0];
 			modelMatrix[7] = translation[1];
-			modelMatrix[11] = translation[2];
+			modelMatrix[11] = -translation[2]; // flip Z to convert from GLTF to Vulkan
 			
 			// rotation and scale
-			auto a = rotation[0];
-			auto q1 = rotation[1];
-			auto q2 = rotation[2];
-			auto q3 = rotation[3];
-	
-			modelMatrix[0] = (2 * (a * a + q1 * q1) - 1) * scale[0];
-			modelMatrix[1] = (2 * (q1 * q2 - a * q3)) * scale[1];
-			modelMatrix[2] = (2 * (q1 * q3 + a * q2)) * scale[2];
-	
-			modelMatrix[4] = (2 * (q1 * q2 + a * q3)) * scale[0];
-			modelMatrix[5] = (2 * (a * a + q2 * q2) - 1) * scale[1];
-			modelMatrix[6] = (2 * (q2 * q3 - a * q1)) * scale[2];
-	
-			modelMatrix[8] = (2 * (q1 * q3 - a * q2)) * scale[0];
-			modelMatrix[9] = (2 * (q2 * q3 + a * q1)) * scale[1];
-			modelMatrix[10] = (2 * (a * a + q3 * q3) - 1) * scale[2];
-	
-			// flip y, because GLTF uses y up, but vulkan -y up
-			modelMatrix[5] *= -1;
+			auto a = rotation[3];
+			auto q1 = rotation[0];
+			auto q2 = rotation[1];
+			auto q3 = rotation[2];
+			
+			auto s = 2 / (a * a + q1 * q1 + q2 * q2 + q3 * q3);
+			
+			auto s1 = scale[0];
+			auto s2 = scale[1];
+			auto s3 = -scale[2]; // flip Z to convert from GLTF to Vulkan
+			
+			modelMatrix[0]  = (1 - s * (q2 * q2 + q3 * q3)) * s1;
+			modelMatrix[1]  = (    s * (q1 * q2 - q3 *  a)) * s2;
+			modelMatrix[2]  = (    s * (q1 * q3 + q2 *  a)) * s3;
+			
+			modelMatrix[4]  = (    s * (q1 * q2 + q3 *  a)) * s1;
+			modelMatrix[5]  = (1 - s * (q1 * q1 + q3 * q3)) * s2;
+			modelMatrix[6]  = (    s * (q2 * q3 - q1 *  a)) * s3;
+			
+			modelMatrix[8]  = (    s * (q1 * q3 - q2 *  a)) * s1;
+			modelMatrix[9]  = (    s * (q2 * q3 + q1 *  a)) * s2;
+			modelMatrix[10] = (1 - s * (q1 * q1 + q2 * q2)) * s3;
 	
 			return modelMatrix;
 		}
 	}
+	
+	static std::array<float, 16> multiplyMatrix(const std::array<float, 16>& matrix,
+												const std::array<float, 16>& other) {
+		std::array<float, 16> result;
+		size_t i, j, k;
+		
+		for (i = 0; i < 4; i++) {
+			for (j = 0; j < 4; j++) {
+				float sum = 0.0f;
+				
+				for (k = 0; k < 4; k++) {
+					sum += matrix[i * 4 + k] * other[k * 4 + j];
+				}
+				
+				result[i * 4 + j] = sum;
+			}
+		}
+		
+		return result;
+	}
 
 	bool Material::hasTexture(const PBRTextureTarget target) const {
 		return textureMask & bitflag(target);
@@ -496,9 +519,23 @@ namespace vkcv::asset {
 	
 		try {
 			if (path.extension() == ".glb") {
-				sceneObjects = fx::gltf::LoadFromBinary(path.string());
+				sceneObjects = fx::gltf::LoadFromBinary(
+						path.string(),
+						{
+								fx::gltf::detail::DefaultMaxBufferCount,
+								fx::gltf::detail::DefaultMaxMemoryAllocation * 16,
+								fx::gltf::detail::DefaultMaxMemoryAllocation * 8
+						}
+				);
 			} else {
-				sceneObjects = fx::gltf::LoadFromText(path.string());
+				sceneObjects = fx::gltf::LoadFromText(
+						path.string(),
+						{
+							fx::gltf::detail::DefaultMaxBufferCount,
+							fx::gltf::detail::DefaultMaxMemoryAllocation,
+							fx::gltf::detail::DefaultMaxMemoryAllocation * 8
+						}
+				);
 			}
 		} catch (const std::system_error& err) {
 			recurseExceptionPrint(err, path.string());
@@ -536,15 +573,54 @@ namespace vkcv::asset {
 				scene.meshes.push_back(mesh);
 			}
 			
-			// This only works if the node has a mesh and it only loads the meshes and ignores cameras and lights
-			for (const auto& node : sceneObjects.nodes) {
+			std::vector< std::array<float, 16> > matrices;
+			std::vector< int32_t > parents;
+			
+			matrices.reserve(sceneObjects.nodes.size());
+			parents.resize(sceneObjects.nodes.size(), -1);
+			
+			for (size_t i = 0; i < sceneObjects.nodes.size(); i++) {
+				const auto &node = sceneObjects.nodes[i];
+				
+				matrices.push_back(calculateModelMatrix(
+						node.translation,
+						node.scale,
+						node.rotation,
+						node.matrix
+				));
+				
+				for (int32_t child : node.children)
+					if ((child >= 0) && (child < parents.size()))
+						parents[child] = static_cast<int32_t>(i);
+			}
+			
+			std::vector< std::array<float, 16> > final_matrices;
+			final_matrices.reserve(matrices.size());
+			
+			for (size_t i = 0; i < matrices.size(); i++) {
+				std::vector<int> order;
+				order.push_back(static_cast<int32_t>(i));
+				
+				while (parents[ order[order.size() - 1] ] >= 0)
+					order.push_back(parents[ order[order.size() - 1] ]);
+				
+				std::array<float, 16> matrix = matrices[ order[order.size() - 1] ];
+				
+				for (size_t j = order.size() - 1; j > 0; j--) {
+					const auto id = order[j - 1];
+					const std::array<float, 16> matrix_other = matrices[ id ];
+					
+					matrix = multiplyMatrix(matrix, matrix_other);
+				}
+				
+				final_matrices.push_back(matrix);
+			}
+			
+			for (size_t i = 0; i < sceneObjects.nodes.size(); i++) {
+				const auto &node = sceneObjects.nodes[i];
+				
 				if ((node.mesh >= 0) && (node.mesh < scene.meshes.size())) {
-					scene.meshes[node.mesh].modelMatrix = calculateModelMatrix(
-							node.translation,
-							node.scale,
-							node.rotation,
-							node.matrix
-					);
+					scene.meshes[node.mesh].modelMatrix = final_matrices[i];
 				}
 			}
 		}
diff --git a/modules/effects/CMakeLists.txt b/modules/effects/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..8de9ea539cfbe9c22607a9b6deb07f683a56bbdd
--- /dev/null
+++ b/modules/effects/CMakeLists.txt
@@ -0,0 +1,54 @@
+cmake_minimum_required(VERSION 3.16)
+project(vkcv_effects)
+
+# setting c++ standard for the project
+set(CMAKE_CXX_STANDARD 20)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+set(vkcv_effects_source ${PROJECT_SOURCE_DIR}/src)
+set(vkcv_effects_include ${PROJECT_SOURCE_DIR}/include)
+
+set(vkcv_effects_sources
+		${vkcv_effects_include}/vkcv/effects/Effect.hpp
+		${vkcv_effects_source}/vkcv/effects/Effect.cpp
+		
+		${vkcv_effects_include}/vkcv/effects/BloomAndFlaresEffect.hpp
+		${vkcv_effects_source}/vkcv/effects/BloomAndFlaresEffect.cpp
+)
+
+set(vkcv_effects_shaders ${PROJECT_SOURCE_DIR}/shaders)
+
+include_shader(${vkcv_effects_shaders}/bloomDownsample.comp ${vkcv_effects_include} ${vkcv_effects_source})
+include_shader(${vkcv_effects_shaders}/bloomFlaresComposite.comp ${vkcv_effects_include} ${vkcv_effects_source})
+include_shader(${vkcv_effects_shaders}/bloomUpsample.comp ${vkcv_effects_include} ${vkcv_effects_source})
+include_shader(${vkcv_effects_shaders}/lensFlares.comp ${vkcv_effects_include} ${vkcv_effects_source})
+
+list(APPEND vkcv_effects_sources ${vkcv_effects_source}/bloomDownsample.comp.cxx)
+list(APPEND vkcv_effects_sources ${vkcv_effects_source}/bloomFlaresComposite.comp.cxx)
+list(APPEND vkcv_effects_sources ${vkcv_effects_source}/bloomUpsample.comp.cxx)
+list(APPEND vkcv_effects_sources ${vkcv_effects_source}/lensFlares.comp.cxx)
+
+list(APPEND vkcv_effects_sources ${vkcv_effects_include}/bloomDownsample.comp.hxx)
+list(APPEND vkcv_effects_sources ${vkcv_effects_include}/bloomFlaresComposite.comp.hxx)
+list(APPEND vkcv_effects_sources ${vkcv_effects_include}/bloomUpsample.comp.hxx)
+list(APPEND vkcv_effects_sources ${vkcv_effects_include}/lensFlares.comp.hxx)
+
+# adding source files to the project
+add_library(vkcv_effects ${vkcv_build_attribute} ${vkcv_effects_sources})
+
+# link the required libraries to the module
+target_link_libraries(vkcv_effects ${vkcv_effects_libraries} vkcv vkcv_shader_compiler vkcv_camera vkcv_asset_loader)
+
+# including headers of dependencies and the VkCV framework
+target_include_directories(vkcv_effects SYSTEM BEFORE PRIVATE ${vkcv_effects_includes} ${vkcv_include} ${vkcv_shader_compiler_include} ${vkcv_camera_include} {vkcv_asset_loader_include})
+
+# add the own include directory for public headers
+target_include_directories(vkcv_effects BEFORE PUBLIC ${vkcv_effects_include})
+
+if (vkcv_parent_scope)
+	list(APPEND vkcv_modules_includes ${vkcv_effects_include})
+	list(APPEND vkcv_modules_libraries vkcv_effects)
+	
+	set(vkcv_modules_includes ${vkcv_modules_includes} PARENT_SCOPE)
+	set(vkcv_modules_libraries ${vkcv_modules_libraries} PARENT_SCOPE)
+endif()
diff --git a/modules/effects/README.md b/modules/effects/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..4e035831381f50a66cf405c06fadbb63f4273bc0
--- /dev/null
+++ b/modules/effects/README.md
@@ -0,0 +1,15 @@
+# Effects
+
+A VkCV module to add post-processing effects to images in realtime after rendering
+
+## Build
+
+### Dependencies (required):
+
+| Name of dependency | Used as submodule |
+|--------------------|-------------------|
+|                    |                   |
+
+## Docs
+
+Here is a [link](https://vkcv.de/develop/group__vkcv__effects.html) to this module.
diff --git a/modules/effects/include/vkcv/effects/BloomAndFlaresEffect.hpp b/modules/effects/include/vkcv/effects/BloomAndFlaresEffect.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..f4a9d03d8afcaf3532570a0807b246a0d641e02d
--- /dev/null
+++ b/modules/effects/include/vkcv/effects/BloomAndFlaresEffect.hpp
@@ -0,0 +1,73 @@
+#pragma once
+
+#include <vector>
+#include <vkcv/camera/Camera.hpp>
+
+#include "Effect.hpp"
+
+namespace vkcv::effects {
+	
+	class BloomAndFlaresEffect : public Effect {
+	private:
+		bool m_advanced;
+		
+		ComputePipelineHandle m_downsamplePipeline;
+		ComputePipelineHandle m_upsamplePipeline;
+		ComputePipelineHandle m_lensFlaresPipeline;
+		ComputePipelineHandle m_compositePipeline;
+		
+		DescriptorSetLayoutHandle m_downsampleDescriptorSetLayout;
+		std::vector<DescriptorSetHandle> m_downsampleDescriptorSets;
+		
+		DescriptorSetLayoutHandle m_upsampleDescriptorSetLayout;
+		std::vector<DescriptorSetHandle> m_upsampleDescriptorSets;
+		std::vector<DescriptorSetHandle> m_flaresDescriptorSets;
+		
+		DescriptorSetLayoutHandle m_lensFlaresDescriptorSetLayout;
+		DescriptorSetHandle m_lensFlaresDescriptorSet;
+		
+		DescriptorSetLayoutHandle m_compositeDescriptorSetLayout;
+		DescriptorSetHandle m_compositeDescriptorSet;
+		
+		ImageHandle m_blurImage;
+		ImageHandle m_flaresImage;
+		
+		SamplerHandle m_linearSampler;
+		SamplerHandle m_radialLutSampler;
+		
+		ImageHandle m_radialLut;
+		ImageHandle m_lensDirt;
+		
+		glm::vec3 m_cameraDirection;
+		uint32_t m_upsampleLimit;
+		
+		void recordDownsampling(const CommandStreamHandle &cmdStream,
+								const ImageHandle &input,
+								const ImageHandle &sample,
+								const std::vector<DescriptorSetHandle> &mipDescriptorSets);
+		
+		void recordUpsampling(const CommandStreamHandle &cmdStream,
+							  const ImageHandle &sample,
+							  const std::vector<DescriptorSetHandle> &mipDescriptorSets);
+		
+		void recordLensFlares(const CommandStreamHandle &cmdStream,
+							  uint32_t mipLevel);
+		
+		void recordComposition(const CommandStreamHandle &cmdStream,
+							   const ImageHandle &output);
+		
+	public:
+		BloomAndFlaresEffect(Core& core,
+							 bool advanced = false);
+		
+		void recordEffect(const CommandStreamHandle &cmdStream,
+						  const ImageHandle &input,
+						  const ImageHandle &output) override;
+		
+		void updateCameraDirection(const camera::Camera &camera);
+		
+		void setUpsamplingLimit(uint32_t limit);
+		
+	};
+	
+}
diff --git a/modules/effects/include/vkcv/effects/Effect.hpp b/modules/effects/include/vkcv/effects/Effect.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..d068fb59404080aa421e49f6a183844e731c1c5e
--- /dev/null
+++ b/modules/effects/include/vkcv/effects/Effect.hpp
@@ -0,0 +1,23 @@
+#pragma once
+
+#include <vkcv/Core.hpp>
+#include <vkcv/Handles.hpp>
+
+namespace vkcv::effects {
+	
+	class Effect {
+	protected:
+		Core& m_core;
+	
+	public:
+		explicit Effect(Core& core);
+		
+		~Effect() = default;
+		
+		virtual void recordEffect(const CommandStreamHandle& cmdStream,
+								  const ImageHandle& input,
+								  const ImageHandle& output) = 0;
+		
+	};
+	
+}
diff --git a/projects/voxelization/assets/shaders/bloomDownsample.comp b/modules/effects/shaders/bloomDownsample.comp
similarity index 100%
rename from projects/voxelization/assets/shaders/bloomDownsample.comp
rename to modules/effects/shaders/bloomDownsample.comp
diff --git a/projects/voxelization/assets/shaders/bloomFlaresComposite.comp b/modules/effects/shaders/bloomFlaresComposite.comp
similarity index 97%
rename from projects/voxelization/assets/shaders/bloomFlaresComposite.comp
rename to modules/effects/shaders/bloomFlaresComposite.comp
index 57174b73ae3b58023d01defd26f636e13cb4709c..21d67393b634c8e639c0b81669e96070c380e6f6 100644
--- a/projects/voxelization/assets/shaders/bloomFlaresComposite.comp
+++ b/modules/effects/shaders/bloomFlaresComposite.comp
@@ -5,12 +5,17 @@ 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;
+
+#ifdef ADVANCED_FEATURES
 layout(set=0, binding=4) uniform texture2D                          radialLUT;
 layout(set=0, binding=5) uniform sampler                            radialLUTSampler;
 layout(set=0, binding=6) uniform texture2D                          dirtTexture;
+#endif
 
 layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
 
+#ifdef ADVANCED_FEATURES
+
 layout( push_constant ) uniform constants{
     vec3 cameraForward;
 };
@@ -52,6 +57,8 @@ float getLensDirtWeight(vec2 uv){
     return mix(1, dirt, dirtStrength);
 }
 
+#endif
+
 void main()
 {
     if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(colorBuffer)))){
@@ -73,17 +80,17 @@ void main()
     float lens_weight  = 0.02f;
     float main_weight = 1 - (bloom_weight + lens_weight);
 
+#ifdef ADVANCED_FEATURES
     lens_color *= starburst(UV);
     
     float lensDirtWeight = getLensDirtWeight(UV);
     bloom_weight        *= lensDirtWeight;
     lens_weight         *= lensDirtWeight;
+#endif
     
     composite_color.rgb = blur_color * bloom_weight +
                           lens_color * lens_weight  +
                           main_color * main_weight;
-                          
-    //composite_color.rgb = vec3(1) * starburst(UV);
 
     imageStore(colorBuffer, pixel_coord, composite_color);
 }
\ No newline at end of file
diff --git a/projects/voxelization/assets/shaders/bloomUpsample.comp b/modules/effects/shaders/bloomUpsample.comp
similarity index 100%
rename from projects/voxelization/assets/shaders/bloomUpsample.comp
rename to modules/effects/shaders/bloomUpsample.comp
diff --git a/projects/voxelization/assets/shaders/lensFlares.comp b/modules/effects/shaders/lensFlares.comp
similarity index 100%
rename from projects/voxelization/assets/shaders/lensFlares.comp
rename to modules/effects/shaders/lensFlares.comp
diff --git a/modules/effects/src/vkcv/effects/BloomAndFlaresEffect.cpp b/modules/effects/src/vkcv/effects/BloomAndFlaresEffect.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..78456da209233c67735b3fb1abe1a11ad8be5736
--- /dev/null
+++ b/modules/effects/src/vkcv/effects/BloomAndFlaresEffect.cpp
@@ -0,0 +1,558 @@
+
+#include "vkcv/effects/BloomAndFlaresEffect.hpp"
+
+#include <vkcv/DrawcallRecording.hpp>
+#include <vkcv/PushConstants.hpp>
+
+#include <vkcv/shader/GLSLCompiler.hpp>
+#include <vkcv/asset/asset_loader.hpp>
+
+#include "bloomDownsample.comp.hxx"
+#include "bloomFlaresComposite.comp.hxx"
+#include "bloomUpsample.comp.hxx"
+#include "lensFlares.comp.hxx"
+
+namespace vkcv::effects {
+	
+	static DescriptorBindings getDescriptorBindings() {
+		DescriptorBindings descriptorBindings = {};
+		
+		auto binding_0 = DescriptorBinding {
+				0,
+				DescriptorType::IMAGE_SAMPLED,
+				1,
+				ShaderStage::COMPUTE,
+				false
+		};
+		
+		auto binding_1 = DescriptorBinding {
+				1,
+				DescriptorType::SAMPLER,
+				1,
+				ShaderStage::COMPUTE,
+				false
+		};
+		
+		auto binding_2 = DescriptorBinding{
+				2,
+				DescriptorType::IMAGE_STORAGE,
+				1,
+				ShaderStage::COMPUTE,
+				false
+		};
+		
+		descriptorBindings.insert(std::make_pair(0, binding_0));
+		descriptorBindings.insert(std::make_pair(1, binding_1));
+		descriptorBindings.insert(std::make_pair(2, binding_2));
+		
+		return descriptorBindings;
+	}
+	
+	static DescriptorBindings getCompositeDescriptorBindings(bool advanced) {
+		DescriptorBindings descriptorBindings = {};
+		
+		auto binding_0 = DescriptorBinding {
+				0,
+				DescriptorType::IMAGE_SAMPLED,
+				1,
+				ShaderStage::COMPUTE,
+				false
+		};
+		
+		auto binding_1 = DescriptorBinding {
+				1,
+				DescriptorType::IMAGE_SAMPLED,
+				1,
+				ShaderStage::COMPUTE,
+				false
+		};
+		
+		auto binding_2 = DescriptorBinding{
+				2,
+				DescriptorType::SAMPLER,
+				1,
+				ShaderStage::COMPUTE,
+				false
+		};
+		
+		auto binding_3 = DescriptorBinding{
+				3,
+				DescriptorType::IMAGE_STORAGE,
+				1,
+				ShaderStage::COMPUTE,
+				false
+		};
+		
+		descriptorBindings.insert(std::make_pair(0, binding_0));
+		descriptorBindings.insert(std::make_pair(1, binding_1));
+		descriptorBindings.insert(std::make_pair(2, binding_2));
+		descriptorBindings.insert(std::make_pair(3, binding_3));
+		
+		if (advanced) {
+			auto binding_4 = DescriptorBinding{
+					4,
+					DescriptorType::IMAGE_SAMPLED,
+					1,
+					ShaderStage::COMPUTE,
+					false
+			};
+			
+			auto binding_5 = DescriptorBinding{
+					5,
+					DescriptorType::SAMPLER,
+					1,
+					ShaderStage::COMPUTE,
+					false
+			};
+			
+			auto binding_6 = DescriptorBinding{
+					6,
+					DescriptorType::IMAGE_SAMPLED,
+					1,
+					ShaderStage::COMPUTE,
+					false
+			};
+			
+			descriptorBindings.insert(std::make_pair(4, binding_4));
+			descriptorBindings.insert(std::make_pair(5, binding_5));
+			descriptorBindings.insert(std::make_pair(6, binding_6));
+		}
+		
+		return descriptorBindings;
+	}
+	
+	static ImageHandle loadTexture(Core &core,
+								   const std::string &texturePath) {
+		const auto texture = vkcv::asset::loadTexture(texturePath);
+		
+		Image image = core.createImage(
+				vk::Format::eR8G8B8A8Unorm,
+				texture.width,
+				texture.height
+		);
+		
+		image.fill(texture.data.data(), texture.data.size());
+		return image.getHandle();
+	}
+	
+	static ComputePipelineHandle compilePipeline(Core &core,
+												 const std::string &shaderSource,
+												 const DescriptorSetLayoutHandle &descriptorSetLayout,
+												 bool advanced = false) {
+		vkcv::shader::GLSLCompiler compiler;
+		
+		if (advanced) {
+			compiler.setDefine("ADVANCED_FEATURES", "1");
+		}
+		
+		ShaderProgram program;
+		compiler.compileSource(
+				vkcv::ShaderStage::COMPUTE,
+				shaderSource.c_str(),
+				[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+					program.addShader(shaderStage, path);
+				},
+				""
+		);
+		
+		ComputePipelineHandle pipeline = core.createComputePipeline({
+			program, { descriptorSetLayout }
+		});
+		
+		return pipeline;
+	}
+	
+	BloomAndFlaresEffect::BloomAndFlaresEffect(Core &core,
+											   bool advanced) :
+	Effect(core),
+	m_advanced(advanced),
+	
+	m_downsamplePipeline(),
+	m_upsamplePipeline(),
+	m_lensFlaresPipeline(),
+	m_compositePipeline(),
+	
+	m_downsampleDescriptorSetLayout(m_core.createDescriptorSetLayout(getDescriptorBindings())),
+	m_downsampleDescriptorSets({}),
+	
+	m_upsampleDescriptorSetLayout(m_core.createDescriptorSetLayout(getDescriptorBindings())),
+	m_upsampleDescriptorSets({}),
+	m_flaresDescriptorSets({}),
+	
+	m_lensFlaresDescriptorSetLayout(m_core.createDescriptorSetLayout(getDescriptorBindings())),
+	m_lensFlaresDescriptorSet(m_core.createDescriptorSet(m_lensFlaresDescriptorSetLayout)),
+	
+	m_compositeDescriptorSetLayout(),
+	m_compositeDescriptorSet(),
+	
+	m_blurImage(),
+	m_flaresImage(),
+	
+	m_linearSampler(m_core.createSampler(
+			vkcv::SamplerFilterType::LINEAR,
+			vkcv::SamplerFilterType::LINEAR,
+			vkcv::SamplerMipmapMode::LINEAR,
+			vkcv::SamplerAddressMode::CLAMP_TO_EDGE
+	)),
+	
+	m_radialLutSampler(),
+	
+	m_radialLut(),
+	m_lensDirt(),
+	
+	m_cameraDirection(),
+	m_upsampleLimit(5) {
+		m_downsamplePipeline = compilePipeline(
+				m_core,
+				BLOOMDOWNSAMPLE_COMP_SHADER,
+				m_downsampleDescriptorSetLayout
+		);
+		
+		m_upsamplePipeline = compilePipeline(
+				m_core,
+				BLOOMUPSAMPLE_COMP_SHADER,
+				m_upsampleDescriptorSetLayout
+		);
+		
+		m_lensFlaresPipeline = compilePipeline(
+				m_core,
+				LENSFLARES_COMP_SHADER,
+				m_lensFlaresDescriptorSetLayout
+		);
+		
+		m_compositeDescriptorSetLayout = m_core.createDescriptorSetLayout(
+				getCompositeDescriptorBindings(m_advanced)
+		);
+		
+		m_compositeDescriptorSet = m_core.createDescriptorSet(
+				m_compositeDescriptorSetLayout
+		);
+		
+		m_compositePipeline = compilePipeline(
+				m_core,
+				BLOOMFLARESCOMPOSITE_COMP_SHADER,
+				m_compositeDescriptorSetLayout,
+				m_advanced
+		);
+		
+		if (m_advanced) {
+			m_radialLutSampler = m_core.createSampler(
+					vkcv::SamplerFilterType::LINEAR,
+					vkcv::SamplerFilterType::LINEAR,
+					vkcv::SamplerMipmapMode::LINEAR,
+					vkcv::SamplerAddressMode::REPEAT
+			);
+			
+			m_radialLut = loadTexture(m_core, "assets/RadialLUT.png");
+			m_lensDirt = loadTexture(m_core, "assets/lensDirt.jpg");
+		}
+	}
+	
+	static uint32_t calcDispatchSize(float sampleSizeDim, uint32_t threadGroupWorkRegionDim) {
+		return std::max<uint32_t>(
+				static_cast<uint32_t>(std::ceil(
+						sampleSizeDim / static_cast<float>(threadGroupWorkRegionDim)
+				)),
+				1
+		);
+	}
+	
+	void BloomAndFlaresEffect::recordDownsampling(const CommandStreamHandle &cmdStream,
+												  const ImageHandle &input,
+												  const ImageHandle &sample,
+												  const std::vector<DescriptorSetHandle> &mipDescriptorSets) {
+		const uint32_t sampleWidth = m_core.getImageWidth(sample);
+		const uint32_t sampleHeight = m_core.getImageHeight(sample);
+		
+		m_core.prepareImageForSampling(cmdStream, input);
+		m_core.prepareImageForStorage(cmdStream, m_blurImage);
+		
+		for (uint32_t mipLevel = 0; mipLevel < mipDescriptorSets.size(); mipLevel++) {
+			// mip descriptor writes
+			DescriptorWrites mipDownsampleWrites;
+			
+			if (mipLevel > 0) {
+				mipDownsampleWrites.sampledImageWrites = {SampledImageDescriptorWrite(0, sample, mipLevel - 1, true)};
+			} else {
+				mipDownsampleWrites.sampledImageWrites = {SampledImageDescriptorWrite(0, input)};
+			}
+			
+			mipDownsampleWrites.samplerWrites      = {SamplerDescriptorWrite(1, m_linearSampler)};
+			mipDownsampleWrites.storageImageWrites = {StorageImageDescriptorWrite(2, sample, mipLevel) };
+			
+			m_core.writeDescriptorSet(mipDescriptorSets[mipLevel], mipDownsampleWrites);
+			
+			float mipDivisor = 1.0f;
+			
+			for (uint32_t i = 0; i < mipLevel; i++) {
+				mipDivisor *= 2.0f;
+			}
+			
+			const auto downsampleSizeX  = static_cast<float>(sampleWidth) / mipDivisor;
+			const auto downsampleSizeY  = static_cast<float>(sampleHeight) / mipDivisor;
+			
+			static const uint32_t threadGroupWorkRegionDim = 8;
+			
+			uint32_t dispatch[3];
+			dispatch[0] = calcDispatchSize(downsampleSizeX, threadGroupWorkRegionDim);
+			dispatch[1] = calcDispatchSize(downsampleSizeY, threadGroupWorkRegionDim);
+			dispatch[2] = 1;
+			
+			// mip blur dispatch
+			m_core.recordComputeDispatchToCmdStream(
+					cmdStream,
+					m_downsamplePipeline,
+					dispatch,
+					{ DescriptorSetUsage(0, mipDescriptorSets[mipLevel]) },
+					PushConstants(0)
+			);
+			
+			// image barrier between mips
+			m_core.recordImageMemoryBarrier(cmdStream, sample);
+		}
+	}
+	
+	void BloomAndFlaresEffect::recordUpsampling(const CommandStreamHandle &cmdStream,
+												const ImageHandle &sample,
+												const std::vector<DescriptorSetHandle> &mipDescriptorSets) {
+		// upsample dispatch
+		m_core.prepareImageForStorage(cmdStream, sample);
+		
+		const uint32_t sampleWidth = m_core.getImageWidth(sample);
+		const uint32_t sampleHeight = m_core.getImageHeight(sample);
+		
+		// upsample dispatch for each mip map
+		for(uint32_t mipLevel = mipDescriptorSets.size(); mipLevel > 0; mipLevel--) {
+			// mip descriptor writes
+			DescriptorWrites mipUpsampleWrites;
+			mipUpsampleWrites.sampledImageWrites = {SampledImageDescriptorWrite(0, sample, mipLevel, true)};
+			mipUpsampleWrites.samplerWrites      = {SamplerDescriptorWrite(1, m_linearSampler)};
+			mipUpsampleWrites.storageImageWrites = {StorageImageDescriptorWrite(2, sample, mipLevel - 1) };
+			
+			m_core.writeDescriptorSet(mipDescriptorSets[mipLevel - 1], mipUpsampleWrites);
+			
+			float mipDivisor = 1.0f;
+			
+			for (uint32_t i = 0; i < mipLevel - 1; i++) {
+				mipDivisor *= 2.0f;
+			}
+			
+			const auto upsampleSizeX  = static_cast<float>(sampleWidth) / mipDivisor;
+			const auto upsampleSizeY  = static_cast<float>(sampleHeight) / mipDivisor;
+			
+			static const uint32_t threadGroupWorkRegionDim = 8;
+			
+			uint32_t dispatch[3];
+			dispatch[0] = calcDispatchSize(upsampleSizeX, threadGroupWorkRegionDim);
+			dispatch[1] = calcDispatchSize(upsampleSizeY, threadGroupWorkRegionDim);
+			dispatch[2] = 1;
+			
+			m_core.recordComputeDispatchToCmdStream(
+					cmdStream,
+					m_upsamplePipeline,
+					dispatch,
+					{ DescriptorSetUsage(0, mipDescriptorSets[mipLevel - 1]) },
+					PushConstants(0)
+			);
+			
+			// image barrier between mips
+			m_core.recordImageMemoryBarrier(cmdStream, sample);
+		}
+	}
+	
+	void BloomAndFlaresEffect::recordLensFlares(const CommandStreamHandle &cmdStream,
+												uint32_t mipLevel) {
+		// lens feature generation descriptor writes
+		m_core.prepareImageForSampling(cmdStream, m_blurImage);
+		m_core.prepareImageForStorage(cmdStream, m_flaresImage);
+		
+		const uint32_t flaresWidth = m_core.getImageWidth(m_flaresImage);
+		const uint32_t flaresHeight = m_core.getImageHeight(m_flaresImage);
+		
+		DescriptorWrites lensFlaresWrites;
+		lensFlaresWrites.sampledImageWrites = {SampledImageDescriptorWrite(0, m_blurImage, 0)};
+		lensFlaresWrites.samplerWrites = {SamplerDescriptorWrite(1, m_linearSampler)};
+		lensFlaresWrites.storageImageWrites = {StorageImageDescriptorWrite(2, m_flaresImage, mipLevel)};
+		
+		m_core.writeDescriptorSet(m_lensFlaresDescriptorSet, lensFlaresWrites);
+		
+		const auto sampleSizeX = static_cast<float>(flaresWidth);
+		const auto sampleSizeY = static_cast<float>(flaresHeight);
+		
+		float mipDivisor = 1.0f;
+		
+		for (uint32_t i = 0; i < mipLevel - 1; i++) {
+			mipDivisor *= 2.0f;
+		}
+		
+		static const uint32_t threadGroupWorkRegionDim = 8.0f;
+		
+		// lens feature generation dispatch
+		uint32_t dispatch[3];
+		dispatch[0] = calcDispatchSize(sampleSizeX / mipDivisor, threadGroupWorkRegionDim);
+		dispatch[1] = calcDispatchSize(sampleSizeY / mipDivisor, threadGroupWorkRegionDim);
+		dispatch[2] = 1;
+		
+		m_core.recordComputeDispatchToCmdStream(
+				cmdStream,
+				m_lensFlaresPipeline,
+				dispatch,
+				{ DescriptorSetUsage(0, m_lensFlaresDescriptorSet) },
+				PushConstants(0)
+		);
+	}
+	
+	void BloomAndFlaresEffect::recordComposition(const CommandStreamHandle &cmdStream,
+												 const ImageHandle &output) {
+		const uint32_t outputWidth = m_core.getImageWidth(output);
+		const uint32_t outputHeight = m_core.getImageHeight(output);
+		
+		m_core.prepareImageForSampling(cmdStream, m_blurImage);
+		m_core.prepareImageForSampling(cmdStream, m_flaresImage);
+		m_core.prepareImageForStorage(cmdStream, output);
+		
+		// bloom composite descriptor write
+		vkcv::DescriptorWrites compositeWrites;
+		
+		if (m_advanced) {
+			compositeWrites.sampledImageWrites = {
+					SampledImageDescriptorWrite(0, m_blurImage),
+					SampledImageDescriptorWrite(1, m_flaresImage),
+					SampledImageDescriptorWrite(4, m_radialLut),
+					SampledImageDescriptorWrite(6, m_lensDirt)
+			};
+			
+			compositeWrites.samplerWrites = {
+					SamplerDescriptorWrite(2, m_linearSampler),
+					SamplerDescriptorWrite(5, m_radialLutSampler)
+			};
+			
+			compositeWrites.storageImageWrites = {
+					StorageImageDescriptorWrite(3, output)
+			};
+		} else {
+			compositeWrites.sampledImageWrites = {
+					SampledImageDescriptorWrite(0, m_blurImage),
+					SampledImageDescriptorWrite(1, m_flaresImage)
+			};
+			
+			compositeWrites.samplerWrites = {
+					SamplerDescriptorWrite(2, m_linearSampler)
+			};
+			
+			compositeWrites.storageImageWrites = {
+					StorageImageDescriptorWrite(3, output)
+			};
+		}
+		
+		m_core.writeDescriptorSet(m_compositeDescriptorSet, compositeWrites);
+		
+		const auto sampleWidth = static_cast<float>(outputWidth);
+		const auto sampleHeight = static_cast<float>(outputHeight);
+		
+		static const uint32_t threadGroupWorkRegionDim = 8;
+		
+		uint32_t dispatch[3];
+		dispatch[0] = calcDispatchSize(sampleWidth, threadGroupWorkRegionDim);
+		dispatch[1] = calcDispatchSize(sampleHeight, threadGroupWorkRegionDim);
+		dispatch[2] = 1;
+		
+		PushConstants pushConstants (sizeof(m_cameraDirection));
+		pushConstants.appendDrawcall(m_cameraDirection);
+		
+		// bloom composite dispatch
+		m_core.recordComputeDispatchToCmdStream(
+				cmdStream,
+				m_compositePipeline,
+				dispatch,
+				{ DescriptorSetUsage(0, m_compositeDescriptorSet) },
+				m_advanced? pushConstants : PushConstants(0)
+		);
+	}
+	
+	void BloomAndFlaresEffect::recordEffect(const CommandStreamHandle &cmdStream,
+											const ImageHandle &input,
+											const ImageHandle &output) {
+		m_core.recordBeginDebugLabel(cmdStream, "vkcv::post_processing::BloomAndFlaresEffect", {
+				0.0f, 1.0f, 1.0f, 1.0f
+		});
+		
+		const uint32_t halfWidth = static_cast<uint32_t>(std::ceil(
+				static_cast<float>(m_core.getImageWidth(output)) * 0.5f
+		));
+		
+		const uint32_t halfHeight = static_cast<uint32_t>(std::ceil(
+				static_cast<float>(m_core.getImageHeight(output)) * 0.5f
+		));
+		
+		if ((!m_blurImage) ||
+			(halfWidth != m_core.getImageWidth(m_blurImage)) ||
+			(halfHeight != m_core.getImageHeight(m_blurImage))) {
+			m_blurImage = m_core.createImage(
+					m_core.getImageFormat(output),
+					halfWidth,
+					halfHeight,
+					1,
+					true,
+					true
+			).getHandle();
+			
+			m_downsampleDescriptorSets.clear();
+			m_upsampleDescriptorSets.clear();
+			
+			const uint32_t mipLevels = m_core.getImageMipLevels(m_blurImage);
+			
+			for (uint32_t i = 0; i < mipLevels; i++) {
+				m_downsampleDescriptorSets.push_back(m_core.createDescriptorSet(
+						m_downsampleDescriptorSetLayout
+				));
+			}
+			
+			for (uint32_t i = 0; i < std::min<uint32_t>(m_upsampleLimit, mipLevels); i++) {
+				m_upsampleDescriptorSets.push_back(m_core.createDescriptorSet(
+						m_upsampleDescriptorSetLayout
+				));
+			}
+		}
+		
+		if ((!m_flaresImage) ||
+			(halfWidth != m_core.getImageWidth(m_flaresImage)) ||
+			(halfHeight != m_core.getImageHeight(m_flaresImage))) {
+			m_flaresImage = m_core.createImage(
+					m_core.getImageFormat(output),
+					halfWidth,
+					halfHeight,
+					1,
+					true,
+					true
+			).getHandle();
+			
+			m_flaresDescriptorSets.clear();
+			
+			const uint32_t mipLevels = m_core.getImageMipLevels(m_flaresImage);
+			
+			for (uint32_t i = 0; i < std::min<uint32_t>(2, mipLevels); i++) {
+				m_flaresDescriptorSets.push_back(m_core.createDescriptorSet(
+						m_upsampleDescriptorSetLayout
+				));
+			}
+		}
+		
+		recordDownsampling(cmdStream, input, m_blurImage, m_downsampleDescriptorSets);
+		recordUpsampling(cmdStream, m_blurImage, m_upsampleDescriptorSets);
+		recordLensFlares(cmdStream, m_flaresDescriptorSets.size());
+		recordUpsampling(cmdStream, m_flaresImage, m_flaresDescriptorSets);
+		recordComposition(cmdStream, output);
+		
+		m_core.recordEndDebugLabel(cmdStream);
+	}
+	
+	void BloomAndFlaresEffect::updateCameraDirection(const camera::Camera &camera) {
+		m_cameraDirection = camera.getFront();
+	}
+	
+	void BloomAndFlaresEffect::setUpsamplingLimit(uint32_t limit) {
+		m_upsampleLimit = limit;
+	}
+	
+}
diff --git a/modules/effects/src/vkcv/effects/Effect.cpp b/modules/effects/src/vkcv/effects/Effect.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..3246db15144bcafcc253df34f358594c85868647
--- /dev/null
+++ b/modules/effects/src/vkcv/effects/Effect.cpp
@@ -0,0 +1,8 @@
+
+#include "vkcv/effects/Effect.hpp"
+
+namespace vkcv::effects {
+	
+	Effect::Effect(Core &core) : m_core(core) {}
+	
+}
diff --git a/modules/scene/src/vkcv/scene/MeshPart.cpp b/modules/scene/src/vkcv/scene/MeshPart.cpp
index f9c87f0a0587bd8a897e42d5b8536fe07c61c749..50d14ed49d43496ada3853034be1455b044bd902 100644
--- a/modules/scene/src/vkcv/scene/MeshPart.cpp
+++ b/modules/scene/src/vkcv/scene/MeshPart.cpp
@@ -71,8 +71,23 @@ namespace vkcv::scene {
 		if (*this) {
 			const auto& material = getMaterial();
 			
+			IndexBitCount indexBitCount;
+			
+			switch (vertexGroup.indexBuffer.type) {
+				case asset::IndexType::UINT16:
+					indexBitCount = IndexBitCount::Bit16;
+					break;
+				case asset::IndexType::UINT32:
+					indexBitCount = IndexBitCount::Bit32;
+					break;
+				default:
+					indexBitCount = IndexBitCount::Bit16;
+					vkcv_log(LogLevel::WARNING, "Unsupported index type!");
+					break;
+			}
+			
 			drawcalls.push_back(DrawcallInfo(
-					vkcv::Mesh(m_vertexBindings, indexBuffer.getVulkanHandle(), m_indexCount),
+					vkcv::Mesh(m_vertexBindings, indexBuffer.getVulkanHandle(), m_indexCount, indexBitCount),
 					{ DescriptorSetUsage(0, material.getDescriptorSet()) }
 			));
 		}
diff --git a/modules/shader_compiler/include/vkcv/shader/Compiler.hpp b/modules/shader_compiler/include/vkcv/shader/Compiler.hpp
index 937c43ef58227d6c2ed29a7ac66681f9afadd025..f62190753ec440c7d732551b6229276c5a42cfc1 100644
--- a/modules/shader_compiler/include/vkcv/shader/Compiler.hpp
+++ b/modules/shader_compiler/include/vkcv/shader/Compiler.hpp
@@ -93,6 +93,7 @@ namespace vkcv::shader {
          * @param[in] value Macro definition value
          */
 		void setDefine(const std::string& name, const std::string& value);
+		
 	};
 
     /** @} */
diff --git a/modules/upscaling/CMakeLists.txt b/modules/upscaling/CMakeLists.txt
index dec392573d31a7348f8440162410b4fc91757b51..6d6e7b987e2e7e448f1e8c5925dc731aafc4f3d0 100644
--- a/modules/upscaling/CMakeLists.txt
+++ b/modules/upscaling/CMakeLists.txt
@@ -17,6 +17,9 @@ set(vkcv_upscaling_sources
 		
 		${vkcv_upscaling_include}/vkcv/upscaling/FSRUpscaling.hpp
 		${vkcv_upscaling_source}/vkcv/upscaling/FSRUpscaling.cpp
+		
+		${vkcv_upscaling_include}/vkcv/upscaling/NISUpscaling.hpp
+		${vkcv_upscaling_source}/vkcv/upscaling/NISUpscaling.cpp
 )
 
 # Setup some path variables to load libraries
@@ -26,6 +29,9 @@ set(vkcv_upscaling_lib_path ${PROJECT_SOURCE_DIR}/${vkcv_upscaling_lib})
 # Check and load FidelityFX_FSR
 include(config/FidelityFX_FSR.cmake)
 
+# Check and load NVIDIAImageScaling
+include(config/NVIDIAImageScaling.cmake)
+
 # adding source files to the project
 add_library(vkcv_upscaling ${vkcv_build_attribute} ${vkcv_upscaling_sources})
 
diff --git a/modules/upscaling/config/NVIDIAImageScaling.cmake b/modules/upscaling/config/NVIDIAImageScaling.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..083891c67e369dc783c79f5ac4386ae15e6e3261
--- /dev/null
+++ b/modules/upscaling/config/NVIDIAImageScaling.cmake
@@ -0,0 +1,15 @@
+
+use_git_submodule("${vkcv_upscaling_lib_path}/NVIDIAImageScaling" nvidia_nis_status)
+
+if (${nvidia_nis_status})
+	include_shader(${vkcv_upscaling_lib_path}/NVIDIAImageScaling/NIS/NIS_Scaler.h ${vkcv_upscaling_include} ${vkcv_upscaling_source})
+	include_shader(${vkcv_upscaling_lib_path}/NVIDIAImageScaling/NIS/NIS_Main.glsl ${vkcv_upscaling_include} ${vkcv_upscaling_source})
+	
+	list(APPEND vkcv_upscaling_includes ${vkcv_upscaling_lib}/NVIDIAImageScaling/NIS)
+	
+	list(APPEND vkcv_upscaling_sources ${vkcv_upscaling_source}/NIS_Scaler.h.cxx)
+	list(APPEND vkcv_upscaling_sources ${vkcv_upscaling_source}/NIS_Main.glsl.cxx)
+	
+	list(APPEND vkcv_upscaling_sources ${vkcv_upscaling_include}/NIS_Scaler.h.hxx)
+	list(APPEND vkcv_upscaling_sources ${vkcv_upscaling_include}/NIS_Main.glsl.hxx)
+endif ()
diff --git a/modules/upscaling/include/vkcv/upscaling/NISUpscaling.hpp b/modules/upscaling/include/vkcv/upscaling/NISUpscaling.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..efab311d4fb452285bb3d88262ddbf3fbd04a810
--- /dev/null
+++ b/modules/upscaling/include/vkcv/upscaling/NISUpscaling.hpp
@@ -0,0 +1,46 @@
+#pragma once
+
+#include "Upscaling.hpp"
+
+#include <vkcv/ShaderProgram.hpp>
+
+namespace vkcv::upscaling {
+	
+	class NISUpscaling : public Upscaling {
+	private:
+		ComputePipelineHandle m_scalerPipeline;
+		
+		DescriptorSetLayoutHandle m_scalerDescriptorSetLayout;
+		DescriptorSetHandle m_scalerDescriptorSet;
+		
+		Buffer<uint8_t> m_scalerConstants;
+		SamplerHandle m_sampler;
+		ImageHandle m_coefScaleImage;
+		ImageHandle m_coefUsmImage;
+		
+		uint32_t m_blockWidth;
+		uint32_t m_blockHeight;
+		
+		bool m_hdr;
+		float m_sharpness;
+		
+	public:
+		explicit NISUpscaling(Core &core);
+		
+		void recordUpscaling(const CommandStreamHandle &cmdStream,
+							 const ImageHandle &input,
+							 const ImageHandle &output) override;
+		
+		[[nodiscard]]
+		bool isHdrEnabled() const;
+		
+		void setHdrEnabled(bool enabled);
+		
+		[[nodiscard]]
+		float getSharpness() const;
+		
+		void setSharpness(float sharpness);
+		
+	};
+	
+}
diff --git a/modules/upscaling/lib/NVIDIAImageScaling b/modules/upscaling/lib/NVIDIAImageScaling
new file mode 160000
index 0000000000000000000000000000000000000000..7a468267104585ce5cd683aebd8e4cb74f826807
--- /dev/null
+++ b/modules/upscaling/lib/NVIDIAImageScaling
@@ -0,0 +1 @@
+Subproject commit 7a468267104585ce5cd683aebd8e4cb74f826807
diff --git a/modules/upscaling/src/vkcv/upscaling/FSRUpscaling.cpp b/modules/upscaling/src/vkcv/upscaling/FSRUpscaling.cpp
index 752d81e65eeb1fc901abf498b8330b7ae6102c80..5a2c96419ce2415737216a31ea26ce3d67012ee9 100644
--- a/modules/upscaling/src/vkcv/upscaling/FSRUpscaling.cpp
+++ b/modules/upscaling/src/vkcv/upscaling/FSRUpscaling.cpp
@@ -1,3 +1,4 @@
+
 #include "vkcv/upscaling/FSRUpscaling.hpp"
 
 #include <cstdint>
@@ -104,7 +105,6 @@ namespace vkcv::upscaling {
 	    descriptorBindings.insert(std::make_pair(3, binding_3));
 
 	    return descriptorBindings;
-
 	}
 	
 	template<typename T>
@@ -159,10 +159,11 @@ namespace vkcv::upscaling {
 			return false;
 		}
 		
-		return compiler.compileSource(vkcv::ShaderStage::COMPUTE,
-									  FSR_PASS_GLSL_SHADER.c_str(),
-									  [&directory, &compiled] (vkcv::ShaderStage shaderStage,
-									  		const std::filesystem::path& path) {
+		return compiler.compileSource(
+				vkcv::ShaderStage::COMPUTE,
+				FSR_PASS_GLSL_SHADER.c_str(),
+				[&directory, &compiled] (vkcv::ShaderStage shaderStage,
+										 const std::filesystem::path& path) {
 				if (compiled) {
 					compiled(shaderStage, path);
 				}
@@ -198,6 +199,7 @@ namespace vkcv::upscaling {
 			SamplerMipmapMode::NEAREST,
 			SamplerAddressMode::CLAMP_TO_EDGE
 	)),
+	
 	m_hdr(false),
 	m_sharpness(0.875f) {
 		vkcv::shader::GLSLCompiler easuCompiler;
diff --git a/modules/upscaling/src/vkcv/upscaling/NISUpscaling.cpp b/modules/upscaling/src/vkcv/upscaling/NISUpscaling.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..998d7f83e7a05d456dfe544a87953769181eada4
--- /dev/null
+++ b/modules/upscaling/src/vkcv/upscaling/NISUpscaling.cpp
@@ -0,0 +1,278 @@
+
+#include "vkcv/upscaling/NISUpscaling.hpp"
+
+#include <NIS_Config.h>
+
+#include "NIS_Main.glsl.hxx"
+#include "NIS_Scaler.h.hxx"
+
+#include <vkcv/File.hpp>
+#include <vkcv/Logger.hpp>
+#include <vkcv/shader/GLSLCompiler.hpp>
+
+namespace vkcv::upscaling {
+	
+	static DescriptorBindings getDescriptorBindings() {
+		DescriptorBindings descriptorBindings = {};
+		
+		auto binding_0 = DescriptorBinding {
+				0,
+				DescriptorType::UNIFORM_BUFFER_DYNAMIC,
+				1,
+				ShaderStage::COMPUTE,
+				false
+		};
+		
+		auto binding_1 = DescriptorBinding{
+				1,
+				DescriptorType::SAMPLER,
+				1,
+				ShaderStage::COMPUTE
+		};
+		
+		auto binding_2 = DescriptorBinding {
+				2,
+				DescriptorType::IMAGE_SAMPLED,
+				1,
+				ShaderStage::COMPUTE,
+				false
+		};
+		
+		auto binding_3 = DescriptorBinding{
+				3,
+				DescriptorType::IMAGE_STORAGE,
+				1,
+				ShaderStage::COMPUTE,
+				false
+		};
+		
+		auto binding_4 = DescriptorBinding {
+				4,
+				DescriptorType::IMAGE_SAMPLED,
+				1,
+				ShaderStage::COMPUTE,
+				false
+		};
+		
+		auto binding_5 = DescriptorBinding {
+				5,
+				DescriptorType::IMAGE_SAMPLED,
+				1,
+				ShaderStage::COMPUTE,
+				false
+		};
+		
+		descriptorBindings.insert(std::make_pair(0, binding_0));
+		descriptorBindings.insert(std::make_pair(1, binding_1));
+		descriptorBindings.insert(std::make_pair(2, binding_2));
+		descriptorBindings.insert(std::make_pair(3, binding_3));
+		descriptorBindings.insert(std::make_pair(4, binding_4));
+		descriptorBindings.insert(std::make_pair(5, binding_5));
+		
+		return descriptorBindings;
+	}
+	
+	static ImageHandle createFilterImage(Core &core,
+										 const void* data) {
+		const size_t rowPitch = kFilterSize * 2;
+		const size_t imageSize = rowPitch * kPhaseCount;
+		
+		Image image = core.createImage(
+				vk::Format::eR16G16B16A16Sfloat,
+				kFilterSize / 4,
+				kPhaseCount
+		);
+		
+		image.fill(data, imageSize);
+		
+		return image.getHandle();
+	}
+	
+	static bool writeShaderCode(const std::filesystem::path &shaderPath, const std::string& code) {
+		std::ofstream file (shaderPath.string(), std::ios::out);
+		
+		if (!file.is_open()) {
+			vkcv_log(LogLevel::ERROR, "The file could not be opened (%s)", shaderPath.string().c_str());
+			return false;
+		}
+		
+		file.seekp(0);
+		file.write(code.c_str(), static_cast<std::streamsize>(code.length()));
+		file.close();
+		
+		return true;
+	}
+	
+	static bool compileNISShader(vkcv::shader::GLSLCompiler& compiler,
+								 const shader::ShaderCompiledFunction& compiled) {
+		std::filesystem::path directory = generateTemporaryDirectoryPath();
+		
+		if (!std::filesystem::create_directory(directory)) {
+			vkcv_log(LogLevel::ERROR, "The directory could not be created (%s)", directory.string().c_str());
+			return false;
+		}
+		
+		if (!writeShaderCode(directory / "NIS_Scaler.h", NIS_SCALER_H_SHADER)) {
+			return false;
+		}
+		
+		return compiler.compileSource(
+				vkcv::ShaderStage::COMPUTE,
+				NIS_MAIN_GLSL_SHADER.c_str(),
+				[&directory, &compiled] (vkcv::ShaderStage shaderStage,
+										 const std::filesystem::path& path) {
+				if (compiled) {
+					compiled(shaderStage, path);
+				}
+				
+				std::filesystem::remove_all(directory);
+			}, directory
+		);
+	}
+	
+	NISUpscaling::NISUpscaling(Core &core) :
+	Upscaling(core),
+	m_scalerPipeline(),
+	
+	m_scalerDescriptorSetLayout(m_core.createDescriptorSetLayout(getDescriptorBindings())),
+	m_scalerDescriptorSet(m_core.createDescriptorSet(m_scalerDescriptorSetLayout)),
+	
+	m_scalerConstants(m_core.createBuffer<uint8_t>(
+			BufferType::UNIFORM, sizeof(NISConfig),
+			BufferMemoryType::HOST_VISIBLE
+	)),
+	m_sampler(m_core.createSampler(
+			SamplerFilterType::LINEAR,
+			SamplerFilterType::LINEAR,
+			SamplerMipmapMode::NEAREST,
+			SamplerAddressMode::CLAMP_TO_EDGE
+	)),
+	
+	m_coefScaleImage(createFilterImage(m_core, coef_scale_fp16)),
+	m_coefUsmImage(createFilterImage(m_core, coef_usm_fp16)),
+	
+	m_blockWidth(0),
+	m_blockHeight(0),
+	
+	m_hdr(false),
+	m_sharpness(0.875f) {
+		vkcv::shader::GLSLCompiler scalerCompiler;
+		
+		scalerCompiler.setDefine("NIS_SCALER", "1");
+		scalerCompiler.setDefine("NIS_GLSL", "1");
+		
+		NISOptimizer optimizer (true, NISGPUArchitecture::NVIDIA_Generic);
+		
+		m_blockWidth = optimizer.GetOptimalBlockWidth();
+		m_blockHeight = optimizer.GetOptimalBlockHeight();
+		
+		scalerCompiler.setDefine("NIS_BLOCK_WIDTH", std::to_string(m_blockWidth));
+		scalerCompiler.setDefine("NIS_BLOCK_HEIGHT", std::to_string(m_blockHeight));
+		
+		const uint32_t threadGroupSize = optimizer.GetOptimalThreadGroupSize();
+		
+		scalerCompiler.setDefine("NIS_THREAD_GROUP_SIZE", std::to_string(threadGroupSize));
+		
+		{
+			ShaderProgram program;
+			compileNISShader(scalerCompiler, [&program](vkcv::ShaderStage shaderStage,
+														const std::filesystem::path& path) {
+				program.addShader(shaderStage, path);
+			});
+			
+			m_scalerPipeline = m_core.createComputePipeline({program,{
+					m_scalerDescriptorSetLayout
+			}});
+			
+			
+			DescriptorWrites writes;
+			writes.uniformBufferWrites.emplace_back(
+					0, m_scalerConstants.getHandle(), true
+			);
+			
+			writes.samplerWrites.emplace_back(1, m_sampler);
+			writes.sampledImageWrites.emplace_back(4, m_coefScaleImage);
+			writes.sampledImageWrites.emplace_back(5, m_coefUsmImage);
+			
+			m_core.writeDescriptorSet(m_scalerDescriptorSet, writes);
+		}
+	}
+	
+	void NISUpscaling::recordUpscaling(const CommandStreamHandle &cmdStream,
+									   const ImageHandle &input,
+									   const ImageHandle &output) {
+		m_core.recordBeginDebugLabel(cmdStream, "vkcv::upscaling::NISUpscaling", {
+				0.0f, 1.0f, 0.0f, 1.0f
+		});
+		
+		const uint32_t inputWidth = m_core.getImageWidth(input);
+		const uint32_t inputHeight = m_core.getImageHeight(input);
+		
+		const uint32_t outputWidth = m_core.getImageWidth(output);
+		const uint32_t outputHeight = m_core.getImageHeight(output);
+		
+		NISConfig config {};
+		NVScalerUpdateConfig(
+				config,
+				m_sharpness,
+				0, 0,
+				inputWidth,
+				inputHeight,
+				inputWidth,
+				inputHeight,
+				0, 0,
+				outputWidth,
+				outputHeight,
+				outputWidth,
+				outputHeight,
+				m_hdr? NISHDRMode::PQ : NISHDRMode::None
+		);
+		
+		m_scalerConstants.fill(
+				reinterpret_cast<uint8_t*>(&config),
+				sizeof(config)
+		);
+		
+		uint32_t dispatch[3];
+		dispatch[0] = (outputWidth + (m_blockWidth - 1)) / m_blockWidth;
+		dispatch[1] = (outputHeight + (m_blockHeight - 1)) / m_blockHeight;
+		dispatch[2] = 1;
+		
+		m_core.recordBufferMemoryBarrier(cmdStream, m_scalerConstants.getHandle());
+		
+		{
+			DescriptorWrites writes;
+			writes.sampledImageWrites.emplace_back(2, input);
+			writes.storageImageWrites.emplace_back(3, output);
+			
+			m_core.writeDescriptorSet(m_scalerDescriptorSet, writes);
+		}
+		
+		m_core.recordComputeDispatchToCmdStream(
+				cmdStream,
+				m_scalerPipeline,
+				dispatch,
+				{DescriptorSetUsage(0, m_scalerDescriptorSet, { 0 })},
+				PushConstants(0)
+		);
+		
+		m_core.recordEndDebugLabel(cmdStream);
+	}
+	
+	bool NISUpscaling::isHdrEnabled() const {
+		return m_hdr;
+	}
+	
+	void NISUpscaling::setHdrEnabled(bool enabled) {
+		m_hdr = enabled;
+	}
+	
+	float NISUpscaling::getSharpness() const {
+		return m_sharpness;
+	}
+	
+	void NISUpscaling::setSharpness(float sharpness) {
+		m_sharpness = (sharpness < 0.0f ? 0.0f : (sharpness > 1.0f ? 1.0f : sharpness));
+	}
+
+}
diff --git a/projects/CMakeLists.txt b/projects/CMakeLists.txt
index ffe8546c961697869354993539697b118fa8d6be..997c907fb26db2344f028ff5eaec8de3f69038b7 100644
--- a/projects/CMakeLists.txt
+++ b/projects/CMakeLists.txt
@@ -5,6 +5,7 @@ include(${vkcv_config_ext}/ProjectFix.cmake)
 add_subdirectory(first_triangle)
 add_subdirectory(first_mesh)
 add_subdirectory(first_scene)
+add_subdirectory(head_demo)
 add_subdirectory(particle_simulation)
 add_subdirectory(rtx_ambient_occlusion)
 add_subdirectory(sph)
diff --git a/projects/bindless_textures/src/main.cpp b/projects/bindless_textures/src/main.cpp
index 6aa0a9d6106dd06c08dd5219c9570430b49ce229..f8fa8db91fb9566771862b59ad9314c9fae2e288 100644
--- a/projects/bindless_textures/src/main.cpp
+++ b/projects/bindless_textures/src/main.cpp
@@ -7,10 +7,7 @@
 #include <vkcv/shader/GLSLCompiler.hpp>
 
 int main(int argc, const char** argv) {
-	const char* applicationName = "First Mesh";
-
-	uint32_t windowWidth = 800;
-	uint32_t windowHeight = 600;
+	const char* applicationName = "Bindless Textures";
 
 	vkcv::Features features;
 	features.requireExtension(VK_KHR_SWAPCHAIN_EXTENSION_NAME);
@@ -48,7 +45,7 @@ int main(int argc, const char** argv) {
 		features
 	);
 
-	vkcv::WindowHandle windowHandle = core.createWindow(applicationName, windowWidth,windowHeight,false);
+	vkcv::WindowHandle windowHandle = core.createWindow(applicationName, 800, 600, true);
 
 	vkcv::asset::Scene mesh;
 
@@ -206,7 +203,7 @@ int main(int argc, const char** argv) {
 
 	core.writeDescriptorSet(descriptorSet, setWrites);
 
-	vkcv::ImageHandle depthBuffer = core.createImage(vk::Format::eD32Sfloat, windowWidth, windowHeight, 1, false).getHandle();
+	vkcv::ImageHandle depthBuffer;
 
 	const vkcv::ImageHandle swapchainInput = vkcv::ImageHandle::createSwapchainImageHandle();
 
@@ -234,11 +231,14 @@ int main(int argc, const char** argv) {
 			continue;
 		}
 		
-		if ((swapchainWidth != windowWidth) || ((swapchainHeight != windowHeight))) {
-			depthBuffer = core.createImage(vk::Format::eD32Sfloat, swapchainWidth, swapchainHeight).getHandle();
-			
-			windowWidth = swapchainWidth;
-			windowHeight = swapchainHeight;
+		if ((!depthBuffer) ||
+			(swapchainWidth != core.getImageWidth(depthBuffer)) ||
+			(swapchainHeight != core.getImageHeight(depthBuffer))) {
+			depthBuffer = core.createImage(
+					vk::Format::eD32Sfloat,
+					swapchainWidth,
+					swapchainHeight
+			).getHandle();
 		}
   
 		auto end = std::chrono::system_clock::now();
diff --git a/projects/first_mesh/src/main.cpp b/projects/first_mesh/src/main.cpp
index 3f4378a6a2187ba33b7965fd6d008577541f7351..c8da98abd87579c2b91b2422dda544dd257b03af 100644
--- a/projects/first_mesh/src/main.cpp
+++ b/projects/first_mesh/src/main.cpp
@@ -8,9 +8,6 @@
 int main(int argc, const char** argv) {
 	const char* applicationName = "First Mesh";
 
-	uint32_t windowWidth = 800;
-	uint32_t windowHeight = 600;
-
 	vkcv::Core core = vkcv::Core::create(
 		applicationName,
 		VK_MAKE_VERSION(0, 0, 1),
@@ -18,7 +15,7 @@ int main(int argc, const char** argv) {
 		{ VK_KHR_SWAPCHAIN_EXTENSION_NAME }
 	);
 
-	vkcv::WindowHandle windowHandle = core.createWindow(applicationName, windowWidth, windowHeight, false);
+	vkcv::WindowHandle windowHandle = core.createWindow(applicationName, 800, 600, true);
 
 	vkcv::asset::Scene mesh;
 
@@ -148,14 +145,7 @@ int main(int argc, const char** argv) {
 
 	core.writeDescriptorSet(descriptorSet, setWrites);
 	
-	auto swapchainExtent = core.getSwapchain(windowHandle).getExtent();
-	
-	vkcv::ImageHandle depthBuffer = core.createImage(
-			vk::Format::eD32Sfloat,
-			swapchainExtent.width,
-			swapchainExtent.height,
-			1, false
-	).getHandle();
+	vkcv::ImageHandle depthBuffer;
 
 	const vkcv::ImageHandle swapchainInput = vkcv::ImageHandle::createSwapchainImageHandle();
 
@@ -182,11 +172,14 @@ int main(int argc, const char** argv) {
 			continue;
 		}
 		
-		if ((swapchainWidth != swapchainExtent.width) || ((swapchainHeight != swapchainExtent.height))) {
-			depthBuffer = core.createImage(vk::Format::eD32Sfloat, swapchainWidth, swapchainHeight).getHandle();
-			
-			swapchainExtent.width = swapchainWidth;
-			swapchainExtent.height = swapchainHeight;
+		if ((!depthBuffer) ||
+			(swapchainWidth != core.getImageWidth(depthBuffer)) ||
+			(swapchainHeight != core.getImageHeight(depthBuffer))) {
+			depthBuffer = core.createImage(
+					vk::Format::eD32Sfloat,
+					swapchainWidth,
+					swapchainHeight
+			).getHandle();
 		}
   
 		auto end = std::chrono::system_clock::now();
diff --git a/projects/first_scene/src/main.cpp b/projects/first_scene/src/main.cpp
index 3dd6fc40d516c6f1ae3358d60c02fc4ed219245a..3e424919d2289c45e129c6b476ff026455403982 100644
--- a/projects/first_scene/src/main.cpp
+++ b/projects/first_scene/src/main.cpp
@@ -20,7 +20,7 @@ int main(int argc, const char** argv) {
 			{vk::QueueFlagBits::eTransfer, vk::QueueFlagBits::eGraphics, vk::QueueFlagBits::eCompute},
 			{ VK_KHR_SWAPCHAIN_EXTENSION_NAME }
 	);
-	vkcv::WindowHandle windowHandle = core.createWindow(applicationName, windowWidth, windowHeight, false);
+	vkcv::WindowHandle windowHandle = core.createWindow(applicationName, windowWidth, windowHeight, true);
 	vkcv::Window& window = core.getWindow(windowHandle);
 	vkcv::camera::CameraManager cameraManager(window);
 
@@ -92,13 +92,7 @@ int main(int argc, const char** argv) {
 		return EXIT_FAILURE;
 	}
 	
-	auto swapchainExtent = core.getSwapchain(windowHandle).getExtent();
-
-	vkcv::ImageHandle depthBuffer = core.createImage(
-			vk::Format::eD32Sfloat,
-			swapchainExtent.width,
-			swapchainExtent.height
-	).getHandle();
+	vkcv::ImageHandle depthBuffer;
 
 	const vkcv::ImageHandle swapchainInput = vkcv::ImageHandle::createSwapchainImageHandle();
 	
@@ -114,11 +108,14 @@ int main(int argc, const char** argv) {
 			continue;
 		}
 		
-		if ((swapchainWidth != swapchainExtent.width) || ((swapchainHeight != swapchainExtent.height))) {
-			depthBuffer = core.createImage(vk::Format::eD32Sfloat, swapchainWidth, swapchainHeight).getHandle();
-			
-			swapchainExtent.width = swapchainWidth;
-			swapchainExtent.height = swapchainHeight;
+		if ((!depthBuffer) ||
+			(swapchainWidth != core.getImageWidth(depthBuffer)) ||
+			(swapchainHeight != core.getImageHeight(depthBuffer))) {
+			depthBuffer = core.createImage(
+					vk::Format::eD32Sfloat,
+					swapchainWidth,
+					swapchainHeight
+			).getHandle();
 		}
   
 		auto end = std::chrono::system_clock::now();
diff --git a/projects/first_triangle/src/main.cpp b/projects/first_triangle/src/main.cpp
index b4ba9046e07ecd3534844b493642098aa5847307..3bdefe3ae55fc89879461ed37025f8fe00e39a8a 100644
--- a/projects/first_triangle/src/main.cpp
+++ b/projects/first_triangle/src/main.cpp
@@ -56,12 +56,12 @@ int main(int argc, const char** argv) {
 
 	const vkcv::GraphicsPipelineConfig trianglePipelineDefinition {
 		triangleShaderProgram,
-		swapchainExtent.width,
-		swapchainExtent.height,
+		UINT32_MAX,
+		UINT32_MAX,
 		trianglePass,
 		{},
 		{},
-		false
+		true
 	};
 
 	vkcv::GraphicsPipelineHandle trianglePipeline = core.createGraphicsPipeline(trianglePipelineDefinition);
diff --git a/projects/head_demo/.gitignore b/projects/head_demo/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..151f56c5bb7098a3beb7b8e049c0ade02914cd91
--- /dev/null
+++ b/projects/head_demo/.gitignore
@@ -0,0 +1 @@
+head_demo
\ No newline at end of file
diff --git a/projects/head_demo/CMakeLists.txt b/projects/head_demo/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..bd7e48000320d4c1304a3b9f3b26fe54421f818c
--- /dev/null
+++ b/projects/head_demo/CMakeLists.txt
@@ -0,0 +1,37 @@
+cmake_minimum_required(VERSION 3.16)
+project(head_demo)
+
+# setting c++ standard for the project
+set(CMAKE_CXX_STANDARD 20)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+# this should fix the execution path to load local files from the project
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
+
+# adding source files to the project
+add_executable(head_demo src/main.cpp)
+fix_project(head_demo)
+
+# including headers of dependencies and the VkCV framework
+target_include_directories(head_demo SYSTEM BEFORE PRIVATE
+		${vkcv_include}
+		${vkcv_includes}
+		${vkcv_asset_loader_include}
+		${vkcv_camera_include}
+		${vkcv_scene_include}
+		${vkcv_shader_compiler_include}
+		${vkcv_gui_include}
+		${vkcv_effects_include}
+		${vkcv_upscaling_include})
+
+# linking with libraries from all dependencies and the VkCV framework
+target_link_libraries(head_demo
+		vkcv
+		${vkcv_libraries}
+		vkcv_asset_loader
+		${vkcv_asset_loader_libraries}
+		vkcv_camera vkcv_scene
+		vkcv_shader_compiler
+		vkcv_gui
+		vkcv_effects
+		vkcv_upscaling)
diff --git a/projects/head_demo/assets/shaders/clip.inc b/projects/head_demo/assets/shaders/clip.inc
new file mode 100644
index 0000000000000000000000000000000000000000..edb5ac3ccfe5d7f6ad1e59dd88bee4446b85118f
--- /dev/null
+++ b/projects/head_demo/assets/shaders/clip.inc
@@ -0,0 +1,19 @@
+
+#define CLIP_SCALE 10000.0f
+
+vec4 clipPosition(vec4 pos) {
+    return vec4(
+        max(clipX, pos.x),
+        max(clipY, pos.y),
+        max(clipZ, pos.z),
+        1.0f / CLIP_SCALE
+    );
+}
+
+vec4 clipByLimit(vec4 pos) {
+    if (pos.x / pos.w < clipLimit) {
+        return vec4(pos.xyz / pos.w, 1.0f);
+    } else {
+        return vec4(clipLimit, pos.y / pos.w, pos.z / pos.w, 1.0f);
+    }
+}
diff --git a/projects/head_demo/assets/shaders/red.frag b/projects/head_demo/assets/shaders/red.frag
new file mode 100644
index 0000000000000000000000000000000000000000..35735be779edd0bf88890638329dc53f6b691f57
--- /dev/null
+++ b/projects/head_demo/assets/shaders/red.frag
@@ -0,0 +1,11 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(location = 0) in vec3 passNormal;
+layout(location = 1) in vec3 passEdge;
+
+layout(location = 0) out vec3 outColor;
+
+void main()	{
+    outColor = (0.1f + max(dot(passNormal, vec3(1.0f, -1.0f, 0.5f)), 0.0f) * 0.9f) * (passEdge + 0.5f);
+}
\ No newline at end of file
diff --git a/projects/head_demo/assets/shaders/shader.frag b/projects/head_demo/assets/shaders/shader.frag
new file mode 100644
index 0000000000000000000000000000000000000000..b8756a6b84af4e93f9ac932ec01d28e4f2e9638b
--- /dev/null
+++ b/projects/head_demo/assets/shaders/shader.frag
@@ -0,0 +1,10 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(location = 0) in vec3 passNormal;
+
+layout(location = 0) out vec3 outColor;
+
+void main()	{
+	outColor = (vec3(0.3f) + max(dot(passNormal, vec3(1.0f, -1.0f, 0.5f)), 0.0f) * vec3(0.7f));
+}
\ No newline at end of file
diff --git a/projects/head_demo/assets/shaders/shader.geom b/projects/head_demo/assets/shaders/shader.geom
new file mode 100644
index 0000000000000000000000000000000000000000..275b300ee3466e117876aa46a6ea1cde11f6f17d
--- /dev/null
+++ b/projects/head_demo/assets/shaders/shader.geom
@@ -0,0 +1,53 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+#extension GL_GOOGLE_include_directive : enable
+
+layout(triangles) in;
+layout(triangle_strip, max_vertices = 3) out;
+
+layout(location = 0) in vec3 geomNormal[];
+
+layout(location = 0) out vec3 passNormal;
+
+layout(set=1, binding=0) uniform clipBuffer {
+    float clipLimit;
+    float clipX;
+    float clipY;
+    float clipZ;
+};
+
+layout( push_constant ) uniform constants{
+    mat4 mvp;
+};
+
+#include "clip.inc"
+
+void main()	{
+    vec4 v0 = gl_in[0].gl_Position;
+    vec4 v1 = gl_in[1].gl_Position;
+    vec4 v2 = gl_in[2].gl_Position;
+
+    v0 = clipPosition(v0 / CLIP_SCALE);
+    v1 = clipPosition(v1 / CLIP_SCALE);
+    v2 = clipPosition(v2 / CLIP_SCALE);
+
+    float dx = abs(v0.x - clipX) + abs(v1.x - clipX) + abs(v2.x - clipX);
+    float dy = abs(v0.y - clipY) + abs(v1.y - clipY) + abs(v2.y - clipY);
+    float dz = abs(v0.z - clipZ) + abs(v1.z - clipZ) + abs(v2.z - clipZ);
+
+    if (dx * dy * dz > 0.0f) {
+        gl_Position = mvp * (v0 * CLIP_SCALE);
+        passNormal = geomNormal[0];
+        EmitVertex();
+
+        gl_Position = mvp * (v1 * CLIP_SCALE);
+        passNormal = geomNormal[1];
+        EmitVertex();
+
+        gl_Position = mvp * (v2 * CLIP_SCALE);
+        passNormal = geomNormal[2];
+        EmitVertex();
+
+        EndPrimitive();
+    }
+}
diff --git a/projects/head_demo/assets/shaders/shader.vert b/projects/head_demo/assets/shaders/shader.vert
new file mode 100644
index 0000000000000000000000000000000000000000..26e43e9c89dc2ffb2355d60be8288fa9832f8baa
--- /dev/null
+++ b/projects/head_demo/assets/shaders/shader.vert
@@ -0,0 +1,12 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(location = 0) in vec3 inPosition;
+layout(location = 1) in vec3 inNormal;
+
+layout(location = 0) out vec3 geomNormal;
+
+void main()	{
+	gl_Position = vec4(inPosition, 1.0);
+	geomNormal  = inNormal;
+}
\ No newline at end of file
diff --git a/projects/head_demo/assets/shaders/wired.geom b/projects/head_demo/assets/shaders/wired.geom
new file mode 100644
index 0000000000000000000000000000000000000000..689e073dde7fd993d164cfea0b7ca777d79f8508
--- /dev/null
+++ b/projects/head_demo/assets/shaders/wired.geom
@@ -0,0 +1,53 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+#extension GL_GOOGLE_include_directive : enable
+
+layout(triangles) in;
+layout(points, max_vertices = 1) out;
+
+layout(location = 0) in vec3 geomNormal[];
+
+layout(location = 0) out vec3 passNormal;
+layout(location = 1) out vec3 passEdge;
+
+layout(set=1, binding=0) uniform clipBuffer {
+    float clipLimit;
+    float clipX;
+    float clipY;
+    float clipZ;
+};
+
+layout( push_constant ) uniform constants{
+    mat4 mvp;
+};
+
+#include "clip.inc"
+
+void main()	{
+    vec4 v0 = gl_in[0].gl_Position;
+    vec4 v1 = gl_in[1].gl_Position;
+    vec4 v2 = gl_in[2].gl_Position;
+
+    v0 = clipPosition(v0 / CLIP_SCALE);
+    v1 = clipPosition(v1 / CLIP_SCALE);
+    v2 = clipPosition(v2 / CLIP_SCALE);
+
+    float dx = abs(v0.x - clipX) + abs(v1.x - clipX) + abs(v2.x - clipX);
+    float dy = abs(v0.y - clipY) + abs(v1.y - clipY) + abs(v2.y - clipY);
+    float dz = abs(v0.z - clipZ) + abs(v1.z - clipZ) + abs(v2.z - clipZ);
+
+    if (dx * dy * dz <= 0.0f) {
+        v0 = clipByLimit(mvp * gl_in[0].gl_Position);
+        v1 = clipByLimit(mvp * gl_in[1].gl_Position);
+        v2 = clipByLimit(mvp * gl_in[2].gl_Position);
+
+        if ((v0.x < clipLimit) || (v1.x < clipLimit) || (v2.x < clipLimit)) {
+            gl_Position = (v0 + v1 + v2) / 3;
+            passNormal = (geomNormal[0] + geomNormal[1] + geomNormal[2]) / 3;
+            passEdge = vec3(dx, dy, dz);
+            EmitVertex();
+
+            EndPrimitive();
+        }
+    }
+}
diff --git a/projects/head_demo/assets/skull/license.txt b/projects/head_demo/assets/skull/license.txt
new file mode 100644
index 0000000000000000000000000000000000000000..6816d6d659087b54dfc6b8e0c2c0e158daa001f0
--- /dev/null
+++ b/projects/head_demo/assets/skull/license.txt
@@ -0,0 +1,11 @@
+Model Information:
+* title:	The Anatomy of the Human Skull
+* source:	https://sketchfab.com/3d-models/the-anatomy-of-the-human-skull-baf6ac7b781a46218dca2b59dee58817
+* author:	HannahNewey (https://sketchfab.com/HannahNewey)
+
+Model License:
+* license type:	CC-BY-NC-SA-4.0 (http://creativecommons.org/licenses/by-nc-sa/4.0/)
+* requirements:	Author must be credited. No commercial use. Modified versions must have the same license.
+
+If you use this 3D model in your project be sure to copy paste this credit wherever you share it:
+This work is based on "The Anatomy of the Human Skull" (https://sketchfab.com/3d-models/the-anatomy-of-the-human-skull-baf6ac7b781a46218dca2b59dee58817) by HannahNewey (https://sketchfab.com/HannahNewey) licensed under CC-BY-NC-SA-4.0 (http://creativecommons.org/licenses/by-nc-sa/4.0/)
\ No newline at end of file
diff --git a/projects/head_demo/assets/skull/scene.bin b/projects/head_demo/assets/skull/scene.bin
new file mode 100644
index 0000000000000000000000000000000000000000..e995091c9d2f10b15959a65d3b0e0de3c9edb0e4
--- /dev/null
+++ b/projects/head_demo/assets/skull/scene.bin
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:df3cb5e715426a8e7a3c4883eb7c53022e09011ded9ae385cd6bc59824129ed8
+size 78176672
diff --git a/projects/head_demo/assets/skull/scene.gltf b/projects/head_demo/assets/skull/scene.gltf
new file mode 100644
index 0000000000000000000000000000000000000000..6a2b75fa5af1e826bf3851db576dd3051e7b77c3
--- /dev/null
+++ b/projects/head_demo/assets/skull/scene.gltf
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8cd4f673e0a555dc138b96e07be60546a799f9ef11837870eae27b16ffd5c1c7
+size 36442
diff --git a/projects/head_demo/assets/skull_scaled/scene.bin b/projects/head_demo/assets/skull_scaled/scene.bin
new file mode 100644
index 0000000000000000000000000000000000000000..3edc8865ed5465c33cb8c9d2f2b0eb83b1a5b599
--- /dev/null
+++ b/projects/head_demo/assets/skull_scaled/scene.bin
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:685e971ce4e99be286464c56a373e528dc507ce18b5bfdff45f90db27e49176b
+size 56596296
diff --git a/projects/head_demo/assets/skull_scaled/scene.gltf b/projects/head_demo/assets/skull_scaled/scene.gltf
new file mode 100644
index 0000000000000000000000000000000000000000..72087f182e4cd1ce1a73f7af9db1d36608df2029
--- /dev/null
+++ b/projects/head_demo/assets/skull_scaled/scene.gltf
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f05a4c8f979dab2793540bafe2b392133d64af028a0ff97f0159dbc52fc20230
+size 4843
diff --git a/projects/head_demo/src/main.cpp b/projects/head_demo/src/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..3eeec03a47d8051385523e8f3e3e146e0829aeda
--- /dev/null
+++ b/projects/head_demo/src/main.cpp
@@ -0,0 +1,287 @@
+#include <iostream>
+#include <vkcv/Core.hpp>
+#include <GLFW/glfw3.h>
+#include <vkcv/camera/CameraManager.hpp>
+#include <vkcv/gui/GUI.hpp>
+#include <chrono>
+#include <vkcv/asset/asset_loader.hpp>
+#include <vkcv/shader/GLSLCompiler.hpp>
+#include <vkcv/scene/Scene.hpp>
+#include <vkcv/effects/BloomAndFlaresEffect.hpp>
+#include <vkcv/upscaling/FSRUpscaling.hpp>
+
+int main(int argc, const char** argv) {
+	const char* applicationName = "First Scene";
+	
+	uint32_t windowWidth = 800;
+	uint32_t windowHeight = 600;
+	
+	vkcv::Core core = vkcv::Core::create(
+			applicationName,
+			VK_MAKE_VERSION(0, 0, 1),
+			{vk::QueueFlagBits::eTransfer, vk::QueueFlagBits::eGraphics, vk::QueueFlagBits::eCompute},
+			{ VK_KHR_SWAPCHAIN_EXTENSION_NAME }
+	);
+	vkcv::WindowHandle windowHandle = core.createWindow(applicationName, windowWidth, windowHeight, true);
+	vkcv::Window& window = core.getWindow(windowHandle);
+	vkcv::camera::CameraManager cameraManager(window);
+	
+	vkcv::gui::GUI gui (core, windowHandle);
+	
+	uint32_t camIndex0 = cameraManager.addCamera(vkcv::camera::ControllerType::PILOT);
+	uint32_t camIndex1 = cameraManager.addCamera(vkcv::camera::ControllerType::TRACKBALL);
+	
+	cameraManager.getCamera(camIndex0).setPosition(glm::vec3(15.5f, 0, 0));
+	cameraManager.getCamera(camIndex0).setNearFar(0.1f, 30.0f);
+	
+	cameraManager.getCamera(camIndex1).setNearFar(0.1f, 30.0f);
+	
+	vkcv::scene::Scene scene = vkcv::scene::Scene::load(core, std::filesystem::path(
+			argc > 1 ? argv[1] : "assets/skull_scaled/scene.gltf"
+	));
+	
+	vk::Format colorFormat = vk::Format::eR16G16B16A16Sfloat;
+	
+	const vkcv::AttachmentDescription color_attachment0(
+			vkcv::AttachmentOperation::STORE,
+			vkcv::AttachmentOperation::CLEAR,
+			colorFormat
+	);
+	
+	const vkcv::AttachmentDescription depth_attachment0(
+			vkcv::AttachmentOperation::STORE,
+			vkcv::AttachmentOperation::CLEAR,
+			vk::Format::eD32Sfloat
+	);
+	
+	const vkcv::AttachmentDescription color_attachment1(
+			vkcv::AttachmentOperation::STORE,
+			vkcv::AttachmentOperation::LOAD,
+			colorFormat
+	);
+	
+	const vkcv::AttachmentDescription depth_attachment1(
+			vkcv::AttachmentOperation::STORE,
+			vkcv::AttachmentOperation::LOAD,
+			vk::Format::eD32Sfloat
+	);
+	
+	vkcv::PassConfig linePassDefinition({ color_attachment0, depth_attachment0 });
+	vkcv::PassHandle linePass = core.createPass(linePassDefinition);
+	
+	vkcv::PassConfig scenePassDefinition({ color_attachment1, depth_attachment1 });
+	vkcv::PassHandle scenePass = core.createPass(scenePassDefinition);
+	
+	if ((!scenePass) || (!linePass)) {
+		std::cout << "Error. Could not create renderpass. Exiting." << std::endl;
+		return EXIT_FAILURE;
+	}
+	
+	vkcv::ShaderProgram sceneShaderProgram;
+	vkcv::ShaderProgram lineShaderProgram;
+	vkcv::shader::GLSLCompiler compiler;
+	
+	compiler.compileProgram(sceneShaderProgram, {
+			{ vkcv::ShaderStage::VERTEX, "assets/shaders/shader.vert" },
+			{ vkcv::ShaderStage::GEOMETRY, "assets/shaders/shader.geom" },
+			{ vkcv::ShaderStage::FRAGMENT, "assets/shaders/shader.frag" }
+	}, nullptr);
+	
+	compiler.compileProgram(lineShaderProgram, {
+			{ vkcv::ShaderStage::VERTEX, "assets/shaders/shader.vert" },
+			{ vkcv::ShaderStage::GEOMETRY, "assets/shaders/wired.geom" },
+			{ vkcv::ShaderStage::FRAGMENT, "assets/shaders/red.frag" }
+	}, nullptr);
+	
+	const std::vector<vkcv::VertexAttachment> vertexAttachments = sceneShaderProgram.getVertexAttachments();
+	std::vector<vkcv::VertexBinding> bindings;
+	for (size_t i = 0; i < vertexAttachments.size(); i++) {
+		bindings.push_back(vkcv::VertexBinding(i, { vertexAttachments[i] }));
+	}
+	
+	const auto& clipBindings = sceneShaderProgram.getReflectedDescriptors().at(1);
+	
+	auto clipDescriptorSetLayout = core.createDescriptorSetLayout(clipBindings);
+	auto clipDescriptorSet = core.createDescriptorSet(clipDescriptorSetLayout);
+	
+	float clipLimit = 1.0f;
+	float clipX = 0.0f;
+	float clipY = 0.0f;
+	float clipZ = 0.0f;
+	
+	auto clipBuffer = core.createBuffer<float>(vkcv::BufferType::UNIFORM, 4);
+	clipBuffer.fill({ clipLimit, -clipX, -clipY, -clipZ });
+	
+	vkcv::DescriptorWrites clipWrites;
+	clipWrites.uniformBufferWrites = {
+			vkcv::BufferDescriptorWrite(0, clipBuffer.getHandle())
+	};
+	
+	core.writeDescriptorSet(clipDescriptorSet, clipWrites);
+	
+	float mouseX = -0.0f;
+	bool dragLimit = false;
+	
+	window.e_mouseMove.add([&](double x, double y) {
+		double cx = (x - window.getWidth() * 0.5);
+		double dx = cx / window.getWidth();
+		
+		mouseX = 2.0f * static_cast<float>(dx);
+		
+		if (dragLimit) {
+			clipLimit = mouseX;
+		}
+	});
+	
+	window.e_mouseButton.add([&](int button, int action, int mods) {
+		if ((std::abs(mouseX - clipLimit) < 0.1f) && (action == GLFW_PRESS)) {
+			dragLimit = true;
+		} else {
+			dragLimit = false;
+		}
+	});
+	
+	const vkcv::VertexLayout sceneLayout(bindings);
+	
+	const auto& material0 = scene.getMaterial(0);
+	
+	const vkcv::GraphicsPipelineConfig scenePipelineDefinition{
+			sceneShaderProgram,
+			UINT32_MAX,
+			UINT32_MAX,
+			scenePass,
+			{sceneLayout},
+			{ material0.getDescriptorSetLayout(), clipDescriptorSetLayout },
+			true
+	};
+	
+	const vkcv::GraphicsPipelineConfig linePipelineDefinition{
+			lineShaderProgram,
+			UINT32_MAX,
+			UINT32_MAX,
+			linePass,
+			{sceneLayout},
+			{ material0.getDescriptorSetLayout(), clipDescriptorSetLayout },
+			true
+	};
+	
+	vkcv::GraphicsPipelineHandle scenePipeline = core.createGraphicsPipeline(scenePipelineDefinition);
+	vkcv::GraphicsPipelineHandle linePipeline = core.createGraphicsPipeline(linePipelineDefinition);
+	
+	if ((!scenePipeline) || (!linePipeline)) {
+		std::cout << "Error. Could not create graphics pipeline. Exiting." << std::endl;
+		return EXIT_FAILURE;
+	}
+	
+	auto swapchainExtent = core.getSwapchain(windowHandle).getExtent();
+	
+	vkcv::ImageHandle depthBuffer = core.createImage(
+			vk::Format::eD32Sfloat,
+			swapchainExtent.width,
+			swapchainExtent.height
+	).getHandle();
+	
+	vkcv::ImageHandle colorBuffer = core.createImage(
+			colorFormat,
+			swapchainExtent.width,
+			swapchainExtent.height,
+			1, false, true, true
+	).getHandle();
+	
+	const vkcv::ImageHandle swapchainInput = vkcv::ImageHandle::createSwapchainImageHandle();
+	
+	vkcv::effects::BloomAndFlaresEffect bloomAndFlares (core);
+	vkcv::upscaling::FSRUpscaling upscaling (core);
+	
+	auto start = std::chrono::system_clock::now();
+	while (vkcv::Window::hasOpenWindow()) {
+		vkcv::Window::pollEvents();
+		
+		if(window.getHeight() == 0 || window.getWidth() == 0)
+			continue;
+		
+		uint32_t swapchainWidth, swapchainHeight;
+		if (!core.beginFrame(swapchainWidth, swapchainHeight,windowHandle)) {
+			continue;
+		}
+		
+		if ((swapchainWidth != swapchainExtent.width) || ((swapchainHeight != swapchainExtent.height))) {
+			depthBuffer = core.createImage(vk::Format::eD32Sfloat, swapchainWidth, swapchainHeight).getHandle();
+			
+			colorBuffer = core.createImage(
+					colorFormat,
+					swapchainExtent.width,
+					swapchainExtent.height,
+					1, false, true, true
+			).getHandle();
+			
+			swapchainExtent.width = swapchainWidth;
+			swapchainExtent.height = 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()));
+		
+		clipBuffer.fill({ clipLimit, -clipX, -clipY, -clipZ });
+		
+		const std::vector<vkcv::ImageHandle> renderTargets = { colorBuffer, depthBuffer };
+		auto cmdStream = core.createCommandStream(vkcv::QueueType::Graphics);
+		
+		auto recordMesh = [&](const glm::mat4& MVP, const glm::mat4& M,
+							 vkcv::PushConstants &pushConstants,
+							 vkcv::DrawcallInfo& drawcallInfo) {
+			pushConstants.appendDrawcall(MVP);
+			drawcallInfo.descriptorSets.push_back(
+					vkcv::DescriptorSetUsage(1, clipDescriptorSet)
+			);
+		};
+		
+		scene.recordDrawcalls(cmdStream,
+							  cameraManager.getActiveCamera(),
+							  linePass,
+							  linePipeline,
+							  sizeof(glm::mat4),
+							  recordMesh,
+							  renderTargets,
+							  windowHandle);
+		
+		bloomAndFlares.recordEffect(cmdStream, colorBuffer, colorBuffer);
+		
+		scene.recordDrawcalls(cmdStream,
+							  cameraManager.getActiveCamera(),
+							  scenePass,
+							  scenePipeline,
+							  sizeof(glm::mat4),
+							  recordMesh,
+							  renderTargets,
+							  windowHandle);
+		
+		core.prepareImageForSampling(cmdStream, colorBuffer);
+		core.prepareImageForStorage(cmdStream, swapchainInput);
+		upscaling.recordUpscaling(cmdStream, colorBuffer, swapchainInput);
+		
+		core.prepareSwapchainImageForPresent(cmdStream);
+		core.submitCommandStream(cmdStream);
+		
+		auto stop = std::chrono::system_clock::now();
+		auto kektime = std::chrono::duration_cast<std::chrono::microseconds>(stop - start);
+		
+		gui.beginGUI();
+		
+		ImGui::Begin("Settings");
+		ImGui::SliderFloat("Clip X", &clipX, -1.0f, 1.0f);
+		ImGui::SliderFloat("Clip Y", &clipY, -1.0f, 1.0f);
+		ImGui::SliderFloat("Clip Z", &clipZ, -1.0f, 1.0f);
+		ImGui::Text("Mesh by HannahNewey (https://sketchfab.com/HannahNewey)");
+		ImGui::End();
+		
+		gui.endGUI();
+		
+		core.endFrame(windowHandle);
+	}
+	
+	return 0;
+}
diff --git a/projects/indirect_draw/src/main.cpp b/projects/indirect_draw/src/main.cpp
index 9b062ab56245c44e5021f164b9e06fed8edd15c3..2a213a2ad7b8ecdaf4b4a51cf4327addc7a881a5 100644
--- a/projects/indirect_draw/src/main.cpp
+++ b/projects/indirect_draw/src/main.cpp
@@ -259,9 +259,6 @@ void compileMeshForIndirectDraw(vkcv::Core &core,
 int main(int argc, const char** argv) {
 	const char* applicationName = "Indirect draw";
 
-	uint32_t windowWidth = 800;
-	uint32_t windowHeight = 600;
-
 	vkcv::Features features;
 	features.requireExtension(VK_KHR_SWAPCHAIN_EXTENSION_NAME);
     features.requireFeature([](vk::PhysicalDeviceFeatures &features){
@@ -305,7 +302,7 @@ int main(int argc, const char** argv) {
 		features
 	);
 
-	vkcv::WindowHandle windowHandle = core.createWindow(applicationName,windowWidth,windowHeight,false);
+	vkcv::WindowHandle windowHandle = core.createWindow(applicationName,800,600,true);
 
     vkcv::gui::GUI gui (core, windowHandle);
 
@@ -526,7 +523,8 @@ int main(int argc, const char** argv) {
 	cameraManager.getCamera(camIndex0).setPosition(glm::vec3(0, 0, -3));
 	cameraManager.getCamera(camIndex0).setNearFar(0.1f, 20.f);
 
-    vkcv::ImageHandle depthBuffer = core.createImage(vk::Format::eD32Sfloat, windowWidth, windowHeight, 1, false).getHandle();
+    vkcv::ImageHandle depthBuffer;
+	
     const vkcv::ImageHandle swapchainInput = vkcv::ImageHandle::createSwapchainImageHandle();
 
     auto start = std::chrono::system_clock::now();
@@ -552,11 +550,10 @@ int main(int argc, const char** argv) {
 			continue;
 		}
 		
-		if ((swapchainWidth != windowWidth) || ((swapchainHeight != windowHeight))) {
+		if ((!depthBuffer) ||
+			(swapchainWidth != core.getImageWidth(depthBuffer)) ||
+			(swapchainHeight != core.getImageHeight(depthBuffer))) {
 			depthBuffer = core.createImage(vk::Format::eD32Sfloat, swapchainWidth, swapchainHeight).getHandle();
-			
-			windowWidth = swapchainWidth;
-			windowHeight = swapchainHeight;
 		}
   
 		auto end = std::chrono::system_clock::now();
diff --git a/projects/mesh_shader/src/main.cpp b/projects/mesh_shader/src/main.cpp
index 0a9914abf0a28f82eee06c5d2a67067faaff4109..afaec1a8ba5dded5f16bad65c68f25f442029091 100644
--- a/projects/mesh_shader/src/main.cpp
+++ b/projects/mesh_shader/src/main.cpp
@@ -77,9 +77,6 @@ CameraPlanes computeCameraPlanes(const vkcv::camera::Camera& camera) {
 
 int main(int argc, const char** argv) {
 	const char* applicationName = "Mesh shader";
-
-	const int windowWidth = 1280;
-	const int windowHeight = 720;
 	
 	vkcv::Features features;
 	features.requireExtension(VK_KHR_SWAPCHAIN_EXTENSION_NAME);
@@ -95,7 +92,7 @@ int main(int argc, const char** argv) {
 		{ vk::QueueFlagBits::eTransfer,vk::QueueFlagBits::eGraphics, vk::QueueFlagBits::eCompute },
 		features
 	);
-	vkcv::WindowHandle windowHandle = core.createWindow(applicationName, windowWidth, windowHeight, false);
+	vkcv::WindowHandle windowHandle = core.createWindow(applicationName, 1280, 720, true);
 	vkcv::Window &window = core.getWindow(windowHandle);
 
     vkcv::gui::GUI gui (core, windowHandle);
@@ -202,17 +199,15 @@ int main(int argc, const char** argv) {
 
     vkcv::DescriptorSetLayoutHandle vertexShaderDescriptorSetLayout = core.createDescriptorSetLayout(bunnyShaderProgram.getReflectedDescriptors().at(0));
     vkcv::DescriptorSetHandle vertexShaderDescriptorSet = core.createDescriptorSet(vertexShaderDescriptorSetLayout);
-
-	auto swapchainExtent = core.getSwapchain(windowHandle).getExtent();
 	
 	const vkcv::GraphicsPipelineConfig bunnyPipelineDefinition {
 			bunnyShaderProgram,
-			swapchainExtent.width,
-			swapchainExtent.height,
+			UINT32_MAX,
+			UINT32_MAX,
 			renderPass,
 			{ bunnyLayout },
 			{ vertexShaderDescriptorSetLayout },
-			false
+			true
 	};
 
 	struct ObjectMatrices {
@@ -257,12 +252,12 @@ int main(int argc, const char** argv) {
 
 	const vkcv::GraphicsPipelineConfig meshShaderPipelineDefinition{
 		meshShaderProgram,
-		swapchainExtent.width,
-		swapchainExtent.height,
+		UINT32_MAX,
+		UINT32_MAX,
 		renderPass,
 		{meshShaderLayout},
 		{meshShaderDescriptorSetLayout},
-		false
+		true
 	};
 
 	vkcv::GraphicsPipelineHandle meshShaderPipeline = core.createGraphicsPipeline(meshShaderPipelineDefinition);
@@ -289,12 +284,7 @@ int main(int argc, const char** argv) {
 
     core.writeDescriptorSet( meshShaderDescriptorSet, meshShaderWrites);
 
-    vkcv::ImageHandle depthBuffer = core.createImage(
-			vk::Format::eD32Sfloat,
-			swapchainExtent.width,
-			swapchainExtent.height,
-			1, false
-	).getHandle();
+    vkcv::ImageHandle depthBuffer;
 
     auto start = std::chrono::system_clock::now();
 
@@ -321,6 +311,16 @@ int main(int argc, const char** argv) {
 			continue;
 		}
 		
+		if ((!depthBuffer) ||
+			(swapchainWidth != core.getImageWidth(depthBuffer)) ||
+			(swapchainHeight != core.getImageHeight(depthBuffer))) {
+			depthBuffer = core.createImage(
+					vk::Format::eD32Sfloat,
+					swapchainWidth,
+					swapchainHeight
+			).getHandle();
+		}
+		
 		auto end = std::chrono::system_clock::now();
 		auto deltatime = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
 		start = end;
diff --git a/projects/particle_simulation/CMakeLists.txt b/projects/particle_simulation/CMakeLists.txt
index 2ff6aaf62c6a9842c3d1b9669286becfd79d92ca..3adede6e25f88208824144b63a866ccabdfc5be9 100644
--- a/projects/particle_simulation/CMakeLists.txt
+++ b/projects/particle_simulation/CMakeLists.txt
@@ -14,14 +14,23 @@ add_executable(particle_simulation
 		src/ParticleSystem.hpp 
 		src/ParticleSystem.cpp
 		src/Particle.hpp 
-		src/Particle.cpp
-		src/BloomAndFlares.hpp
-		src/BloomAndFlares.cpp)
+		src/Particle.cpp)
 
 fix_project(particle_simulation)
 
 # including headers of dependencies and the VkCV framework
-target_include_directories(particle_simulation SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_testing_include} ${vkcv_camera_include} ${vkcv_shader_compiler_include})
+target_include_directories(particle_simulation SYSTEM BEFORE PRIVATE
+		${vkcv_include}
+		${vkcv_includes}
+		${vkcv_testing_include}
+		${vkcv_camera_include}
+		${vkcv_shader_compiler_include}
+		${vkcv_effects_include})
 
 # linking with libraries from all dependencies and the VkCV framework
-target_link_libraries(particle_simulation vkcv vkcv_testing vkcv_camera vkcv_shader_compiler)
+target_link_libraries(particle_simulation
+		vkcv
+		vkcv_testing
+		vkcv_camera
+		vkcv_shader_compiler
+		vkcv_effects)
diff --git a/projects/particle_simulation/shaders/bloom/composite.comp b/projects/particle_simulation/shaders/bloom/composite.comp
deleted file mode 100644
index 87b5ddb975106232d1cd3b6e5b8dc7e623dd0b59..0000000000000000000000000000000000000000
--- a/projects/particle_simulation/shaders/bloom/composite.comp
+++ /dev/null
@@ -1,38 +0,0 @@
-#version 450
-#extension GL_ARB_separate_shader_objects : enable
-
-layout(set=0, binding=0) uniform texture2D                          blurImage;
-layout(set=0, binding=1) uniform texture2D                          lensImage;
-layout(set=0, binding=2) uniform sampler                            linearSampler;
-layout(set=0, binding=3, r11f_g11f_b10f) uniform image2D            colorBuffer;
-
-layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
-
-
-void main()
-{
-    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(colorBuffer)))){
-        return;
-    }
-
-    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
-    vec2  pixel_size    = vec2(1.0f) / textureSize(sampler2D(blurImage, linearSampler), 0);
-    vec2  UV            = pixel_coord.xy * pixel_size;
-
-    vec4 composite_color = vec4(0.0f);
-
-    vec3 blur_color   = texture(sampler2D(blurImage, linearSampler), UV).rgb;
-    vec3 lens_color   = texture(sampler2D(lensImage, linearSampler), UV).rgb;
-    vec3 main_color   = imageLoad(colorBuffer, pixel_coord).rgb;
-
-    // composite blur and lens features
-    float bloom_weight = 0.01f;
-    float lens_weight  = 0.f;
-    float main_weight = 1 - (bloom_weight + lens_weight);
-
-    composite_color.rgb = blur_color * bloom_weight +
-                          lens_color * lens_weight  +
-                          main_color * main_weight;
-
-    imageStore(colorBuffer, pixel_coord, composite_color);
-}
\ No newline at end of file
diff --git a/projects/particle_simulation/shaders/bloom/downsample.comp b/projects/particle_simulation/shaders/bloom/downsample.comp
deleted file mode 100644
index 2ab00c7c92798769153634f3479c5b7f3fb61d94..0000000000000000000000000000000000000000
--- a/projects/particle_simulation/shaders/bloom/downsample.comp
+++ /dev/null
@@ -1,76 +0,0 @@
-#version 450
-#extension GL_ARB_separate_shader_objects : enable
-
-layout(set=0, binding=0) uniform texture2D                          inBlurImage;
-layout(set=0, binding=1) uniform sampler                            inImageSampler;
-layout(set=0, binding=2, r11f_g11f_b10f) uniform writeonly image2D  outBlurImage;
-
-layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
-
-
-void main()
-{
-    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(outBlurImage)))){
-        return;
-    }
-
-    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
-    vec2  pixel_size    = vec2(1.0f) / imageSize(outBlurImage);
-    vec2  UV            = pixel_coord.xy * pixel_size;
-    vec2  UV_offset     = UV + 0.5f * pixel_size;
-
-    vec2 color_fetches[13] = {
-        // center neighbourhood (RED)
-        vec2(-1,  1), // LT
-        vec2(-1, -1), // LB
-        vec2( 1, -1), // RB
-        vec2( 1,  1), // RT
-
-        vec2(-2, 2), // LT
-        vec2( 0, 2), // CT
-        vec2( 2, 2), // RT
-
-        vec2(0 ,-2), // LC
-        vec2(0 , 0), // CC
-        vec2(2,  0), // CR
-
-        vec2(-2, -2), // LB
-        vec2(0 , -2), // CB
-        vec2(2 , -2)  // RB
-    };
-
-    float color_weights[13] = {
-        // 0.5f
-        1.f/8.f,
-        1.f/8.f,
-        1.f/8.f,
-        1.f/8.f,
-
-        // 0.125f
-        1.f/32.f,
-        1.f/16.f,
-        1.f/32.f,
-
-        // 0.25f
-        1.f/16.f,
-        1.f/8.f,
-        1.f/16.f,
-
-        // 0.125f
-        1.f/32.f,
-        1.f/16.f,
-        1.f/32.f
-    };
-
-    vec3 sampled_color = vec3(0.0f);
-
-    for(uint i = 0; i < 13; i++)
-    {
-        vec2 color_fetch = UV_offset + color_fetches[i] * pixel_size;
-        vec3 color = texture(sampler2D(inBlurImage, inImageSampler), color_fetch).rgb;
-        color *= color_weights[i];
-        sampled_color += color;
-    }
-
-    imageStore(outBlurImage, pixel_coord, vec4(sampled_color, 1.f));
-}
\ No newline at end of file
diff --git a/projects/particle_simulation/shaders/bloom/lensFlares.comp b/projects/particle_simulation/shaders/bloom/lensFlares.comp
deleted file mode 100644
index ce27d8850b709f61332d467914ddc944dc63109f..0000000000000000000000000000000000000000
--- a/projects/particle_simulation/shaders/bloom/lensFlares.comp
+++ /dev/null
@@ -1,109 +0,0 @@
-#version 450
-#extension GL_ARB_separate_shader_objects : enable
-
-layout(set=0, binding=0) uniform texture2D                          blurBuffer;
-layout(set=0, binding=1) uniform sampler                            linearSampler;
-layout(set=0, binding=2, r11f_g11f_b10f) uniform image2D            lensBuffer;
-
-layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
-
-vec3 sampleColorChromaticAberration(vec2 _uv)
-{
-    vec2 toCenter = (vec2(0.5) - _uv);
-
-    vec3    colorScales     = vec3(-1, 0, 1);
-    float   aberrationScale = 0.1;
-    vec3 scaleFactors = colorScales * aberrationScale;
-
-    float r = texture(sampler2D(blurBuffer, linearSampler), _uv + toCenter * scaleFactors.r).r;
-    float g = texture(sampler2D(blurBuffer, linearSampler), _uv + toCenter * scaleFactors.g).g;
-    float b = texture(sampler2D(blurBuffer, linearSampler), _uv + toCenter * scaleFactors.b).b;
-    return vec3(r, g, b);
-}
-
-// _uv assumed to be flipped UV coordinates!
-vec3 ghost_vectors(vec2 _uv)
-{
-    vec2 ghost_vec = (vec2(0.5f) - _uv);
-
-    const uint c_ghost_count = 64;
-    const float c_ghost_spacing = length(ghost_vec) / c_ghost_count;
-
-    ghost_vec *= c_ghost_spacing;
-
-    vec3 ret_color = vec3(0.0f);
-
-    for (uint i = 0; i < c_ghost_count; ++i)
-    {
-        // sample scene color
-        vec2 s_uv = fract(_uv + ghost_vec * vec2(i));
-        vec3 s = sampleColorChromaticAberration(s_uv);
-
-        // tint/weight
-        float d = distance(s_uv, vec2(0.5));
-        float weight = 1.0f - smoothstep(0.0f, 0.75f, d);
-        s *= weight;
-
-        ret_color += s;
-    }
-
-    ret_color /= c_ghost_count;
-    return ret_color;
-}
-
-vec3 halo(vec2 _uv)
-{
-    const float c_aspect_ratio = float(imageSize(lensBuffer).x) / float(imageSize(lensBuffer).y);
-    const float c_radius = 0.6f;
-    const float c_halo_thickness = 0.1f;
-
-    vec2 halo_vec = vec2(0.5) - _uv;
-    //halo_vec.x /= c_aspect_ratio;
-    halo_vec = normalize(halo_vec);
-    //halo_vec.x *= c_aspect_ratio;
-
-
-    //vec2 w_uv = (_uv - vec2(0.5, 0.0)) * vec2(c_aspect_ratio, 1.0) + vec2(0.5, 0.0);
-    vec2 w_uv = _uv;
-    float d = distance(w_uv, vec2(0.5)); // distance to center
-
-    float distance_to_halo = abs(d - c_radius);
-
-    float halo_weight = 0.0f;
-    if(abs(d - c_radius) <= c_halo_thickness)
-    {
-        float distance_to_border = c_halo_thickness - distance_to_halo;
-        halo_weight = distance_to_border / c_halo_thickness;
-
-        //halo_weight = clamp((halo_weight / 0.4f), 0.0f, 1.0f);
-        halo_weight = pow(halo_weight, 2.0f);
-
-
-        //halo_weight = 1.0f;
-    }
-
-    return sampleColorChromaticAberration(_uv + halo_vec) * halo_weight;
-}
-
-
-
-void main()
-{
-    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(lensBuffer)))){
-        return;
-    }
-
-    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
-    vec2  pixel_size    = vec2(1.0f) / imageSize(lensBuffer);
-    vec2  UV            = pixel_coord.xy * pixel_size;
-
-    vec2 flipped_UV = vec2(1.0f) - UV;
-
-    vec3 color = vec3(0.0f);
-
-    color += ghost_vectors(flipped_UV);
-    color += halo(UV);
-    color  *= 0.5f;
-
-    imageStore(lensBuffer, pixel_coord, vec4(color, 0.0f));
-}
\ No newline at end of file
diff --git a/projects/particle_simulation/shaders/bloom/upsample.comp b/projects/particle_simulation/shaders/bloom/upsample.comp
deleted file mode 100644
index 0ddeedb5b5af9e476dc19012fed6430544006c0e..0000000000000000000000000000000000000000
--- a/projects/particle_simulation/shaders/bloom/upsample.comp
+++ /dev/null
@@ -1,45 +0,0 @@
-#version 450
-#extension GL_ARB_separate_shader_objects : enable
-
-layout(set=0, binding=0) uniform texture2D                          inUpsampleImage;
-layout(set=0, binding=1) uniform sampler                            inImageSampler;
-layout(set=0, binding=2, r11f_g11f_b10f) uniform image2D  outUpsampleImage;
-
-layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
-
-void main()
-{
-    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(outUpsampleImage)))){
-        return;
-    }
-
-
-    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
-    vec2  pixel_size    = vec2(1.0f) / imageSize(outUpsampleImage);
-    vec2  UV            = pixel_coord.xy * pixel_size;
-
-    const float gauss_kernel[3] = {1.f, 2.f, 1.f};
-    const float gauss_weight = 16.f;
-
-    vec3 sampled_color = vec3(0.f);
-
-    for(int i = -1; i <= 1; i++)
-    {
-        for(int j = -1; j <= 1; j++)
-        {
-            vec2 sample_location = UV + vec2(j, i) * pixel_size;
-            vec3 color = texture(sampler2D(inUpsampleImage, inImageSampler), sample_location).rgb;
-            color *= gauss_kernel[j+1];
-            color *= gauss_kernel[i+1];
-            color /= gauss_weight;
-
-            sampled_color += color;
-        }
-    }
-
-    //vec3 prev_color = imageLoad(outUpsampleImage, pixel_coord).rgb;
-    //float bloomRimStrength = 0.75f; // adjust this to change strength of bloom
-    //sampled_color = mix(prev_color, sampled_color, bloomRimStrength);
-
-    imageStore(outUpsampleImage, pixel_coord, vec4(sampled_color, 1.f));
-}
\ No newline at end of file
diff --git a/projects/particle_simulation/src/BloomAndFlares.cpp b/projects/particle_simulation/src/BloomAndFlares.cpp
deleted file mode 100644
index 6ab0a8deff3d5fe906567562cb86d75a1cc2c09b..0000000000000000000000000000000000000000
--- a/projects/particle_simulation/src/BloomAndFlares.cpp
+++ /dev/null
@@ -1,293 +0,0 @@
-#include "BloomAndFlares.hpp"
-#include <vkcv/shader/GLSLCompiler.hpp>
-
-BloomAndFlares::BloomAndFlares(
-        vkcv::Core *p_Core,
-        vk::Format colorBufferFormat,
-        uint32_t width,
-        uint32_t height) :
-
-        p_Core(p_Core),
-        m_ColorBufferFormat(colorBufferFormat),
-        m_Width(width),
-        m_Height(height),
-        m_LinearSampler(p_Core->createSampler(vkcv::SamplerFilterType::LINEAR,
-                                              vkcv::SamplerFilterType::LINEAR,
-                                              vkcv::SamplerMipmapMode::LINEAR,
-                                              vkcv::SamplerAddressMode::CLAMP_TO_EDGE)),
-        m_Blur(p_Core->createImage(colorBufferFormat, width, height, 1, true, true, false)),
-        m_LensFeatures(p_Core->createImage(colorBufferFormat, width, height, 1, false, true, false))
-{
-    vkcv::shader::GLSLCompiler compiler;
-
-    // DOWNSAMPLE
-    vkcv::ShaderProgram dsProg;
-    compiler.compile(vkcv::ShaderStage::COMPUTE,
-                     "shaders/bloom/downsample.comp",
-                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
-                     {
-                         dsProg.addShader(shaderStage, path);
-                     });
-    for(uint32_t mipLevel = 0; mipLevel < m_Blur.getMipCount(); mipLevel++)
-    {
-        m_DownsampleDescSetLayouts.push_back(
-                p_Core->createDescriptorSetLayout(dsProg.getReflectedDescriptors().at(0)));
-
-		m_DownsampleDescSets.push_back(
-		        p_Core->createDescriptorSet(m_DownsampleDescSetLayouts.back()));
-    }
-
-    m_DownsamplePipe = p_Core->createComputePipeline({
-        dsProg, m_DownsampleDescSetLayouts
-	});
-
-    // UPSAMPLE
-    vkcv::ShaderProgram usProg;
-    compiler.compile(vkcv::ShaderStage::COMPUTE,
-                     "shaders/bloom/upsample.comp",
-                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
-                     {
-                         usProg.addShader(shaderStage, path);
-                     });
-    for(uint32_t mipLevel = 0; mipLevel < m_Blur.getMipCount(); mipLevel++)
-    {
-        m_UpsampleDescSetLayouts.push_back(
-                p_Core->createDescriptorSetLayout(usProg.getReflectedDescriptors().at(0)));
-        m_UpsampleDescSets.push_back(
-                p_Core->createDescriptorSet(m_UpsampleDescSetLayouts.back()));
-    }
-
-    m_UpsamplePipe = p_Core->createComputePipeline({
-            usProg, m_UpsampleDescSetLayouts
-	});
-
-    // LENS FEATURES
-    vkcv::ShaderProgram lensProg;
-    compiler.compile(vkcv::ShaderStage::COMPUTE,
-                     "shaders/bloom/lensFlares.comp",
-                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
-                     {
-                         lensProg.addShader(shaderStage, path);
-                     });
-
-    m_LensFlareDescSetLayout = p_Core->createDescriptorSetLayout(lensProg.getReflectedDescriptors().at(0));
-    m_LensFlareDescSet = p_Core->createDescriptorSet(m_LensFlareDescSetLayout);
-    m_LensFlarePipe = p_Core->createComputePipeline({
-            lensProg, { m_LensFlareDescSetLayout }
-	});
-
-    // COMPOSITE
-    vkcv::ShaderProgram compProg;
-    compiler.compile(vkcv::ShaderStage::COMPUTE,
-                     "shaders/bloom/composite.comp",
-                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
-                     {
-                         compProg.addShader(shaderStage, path);
-                     });
-
-    m_CompositeDescSetLayout = p_Core->createDescriptorSetLayout(compProg.getReflectedDescriptors().at(0));
-    m_CompositeDescSet = p_Core->createDescriptorSet(m_CompositeDescSetLayout);
-    m_CompositePipe = p_Core->createComputePipeline({
-            compProg, { m_CompositeDescSetLayout }
-	});
-}
-
-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, m_DownsampleDescSets[0])},
-            vkcv::PushConstants(0));
-
-    // downsample dispatches of blur buffer's mip maps
-    float mipDispatchCountX = dispatchCountX;
-    float mipDispatchCountY = dispatchCountY;
-    for(uint32_t mipLevel = 1; mipLevel < std::min((uint32_t)m_DownsampleDescSets.size(), m_Blur.getMipCount()); mipLevel++)
-    {
-        // mip descriptor writes
-        vkcv::DescriptorWrites mipDescriptorWrites;
-        mipDescriptorWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle(), mipLevel - 1, true)};
-        mipDescriptorWrites.samplerWrites      = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
-        mipDescriptorWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_Blur.getHandle(), mipLevel) };
-        p_Core->writeDescriptorSet(m_DownsampleDescSets[mipLevel], mipDescriptorWrites);
-
-        // mip dispatch calculation
-        mipDispatchCountX  /= 2.0f;
-        mipDispatchCountY /= 2.0f;
-
-        uint32_t mipDispatchCount[3] = {
-                static_cast<uint32_t>(glm::ceil(mipDispatchCountX)),
-                static_cast<uint32_t>(glm::ceil(mipDispatchCountY)),
-                1
-        };
-
-        if(mipDispatchCount[0] == 0)
-            mipDispatchCount[0] = 1;
-        if(mipDispatchCount[1] == 0)
-            mipDispatchCount[1] = 1;
-
-        // mip blur dispatch
-        p_Core->recordComputeDispatchToCmdStream(
-                cmdStream,
-                m_DownsamplePipe,
-                mipDispatchCount,
-                {vkcv::DescriptorSetUsage(0, m_DownsampleDescSets[mipLevel])},
-                vkcv::PushConstants(0));
-
-        // image barrier between mips
-        p_Core->recordImageMemoryBarrier(cmdStream, m_Blur.getHandle());
-    }
-}
-
-void BloomAndFlares::execUpsamplePipe(const vkcv::CommandStreamHandle &cmdStream)
-{
-    // upsample dispatch
-    p_Core->prepareImageForStorage(cmdStream, m_Blur.getHandle());
-
-    const uint32_t upsampleMipLevels = std::min(
-    		static_cast<uint32_t>(m_UpsampleDescSets.size() - 1),
-    		static_cast<uint32_t>(3)
-	);
-
-    // upsample dispatch for each mip map
-    for(uint32_t mipLevel = upsampleMipLevels; mipLevel > 0; mipLevel--)
-    {
-        // mip descriptor writes
-        vkcv::DescriptorWrites mipUpsampleWrites;
-        mipUpsampleWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle(), mipLevel, true)};
-        mipUpsampleWrites.samplerWrites      = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
-        mipUpsampleWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_Blur.getHandle(), mipLevel - 1) };
-        p_Core->writeDescriptorSet(m_UpsampleDescSets[mipLevel], mipUpsampleWrites);
-
-        auto mipDivisor = glm::pow(2.0f, static_cast<float>(mipLevel) - 1.0f);
-
-        auto upsampleDispatchX  = static_cast<float>(m_Width) / mipDivisor;
-        auto upsampleDispatchY = static_cast<float>(m_Height) / mipDivisor;
-        upsampleDispatchX /= 8.0f;
-        upsampleDispatchY /= 8.0f;
-
-        const uint32_t upsampleDispatchCount[3] = {
-                static_cast<uint32_t>(glm::ceil(upsampleDispatchX)),
-                static_cast<uint32_t>(glm::ceil(upsampleDispatchY)),
-                1
-        };
-
-        p_Core->recordComputeDispatchToCmdStream(
-                cmdStream,
-                m_UpsamplePipe,
-                upsampleDispatchCount,
-                {vkcv::DescriptorSetUsage(0, m_UpsampleDescSets[mipLevel])},
-                vkcv::PushConstants(0)
-        );
-        // image barrier between mips
-        p_Core->recordImageMemoryBarrier(cmdStream, m_Blur.getHandle());
-    }
-}
-
-void BloomAndFlares::execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStream)
-{
-    // lens feature generation descriptor writes
-    p_Core->prepareImageForSampling(cmdStream, m_Blur.getHandle());
-    p_Core->prepareImageForStorage(cmdStream, m_LensFeatures.getHandle());
-
-    vkcv::DescriptorWrites lensFeatureWrites;
-    lensFeatureWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle(), 0)};
-    lensFeatureWrites.samplerWrites = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
-    lensFeatureWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_LensFeatures.getHandle(), 0)};
-    p_Core->writeDescriptorSet(m_LensFlareDescSet, lensFeatureWrites);
-
-    auto dispatchCountX  = static_cast<float>(m_Width)  / 8.0f;
-    auto dispatchCountY = static_cast<float>(m_Height) / 8.0f;
-    // lens feature generation dispatch
-    uint32_t lensFeatureDispatchCount[3] = {
-            static_cast<uint32_t>(glm::ceil(dispatchCountX)),
-            static_cast<uint32_t>(glm::ceil(dispatchCountY)),
-            1
-    };
-    p_Core->recordComputeDispatchToCmdStream(
-            cmdStream,
-            m_LensFlarePipe,
-            lensFeatureDispatchCount,
-            {vkcv::DescriptorSetUsage(0, m_LensFlareDescSet)},
-            vkcv::PushConstants(0));
-}
-
-void BloomAndFlares::execCompositePipe(const vkcv::CommandStreamHandle &cmdStream,
-                                       const vkcv::ImageHandle &colorAttachment)
-{
-    p_Core->prepareImageForSampling(cmdStream, m_Blur.getHandle());
-    p_Core->prepareImageForSampling(cmdStream, m_LensFeatures.getHandle());
-    p_Core->prepareImageForStorage(cmdStream, colorAttachment);
-
-    // bloom composite descriptor write
-    vkcv::DescriptorWrites compositeWrites;
-    compositeWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle()),
-                                          vkcv::SampledImageDescriptorWrite(1, m_LensFeatures.getHandle())};
-    compositeWrites.samplerWrites = {vkcv::SamplerDescriptorWrite(2, m_LinearSampler)};
-    compositeWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(3, colorAttachment)};
-    p_Core->writeDescriptorSet(m_CompositeDescSet, compositeWrites);
-
-    float dispatchCountX = static_cast<float>(m_Width)  / 8.0f;
-    float dispatchCountY = static_cast<float>(m_Height) / 8.0f;
-
-    uint32_t compositeDispatchCount[3] = {
-            static_cast<uint32_t>(glm::ceil(dispatchCountX)),
-            static_cast<uint32_t>(glm::ceil(dispatchCountY)),
-            1
-    };
-
-    // bloom composite dispatch
-    p_Core->recordComputeDispatchToCmdStream(
-            cmdStream,
-            m_CompositePipe,
-            compositeDispatchCount,
-            {vkcv::DescriptorSetUsage(0, m_CompositeDescSet)},
-            vkcv::PushConstants(0));
-}
-
-void BloomAndFlares::execWholePipeline(const vkcv::CommandStreamHandle &cmdStream,
-                                       const vkcv::ImageHandle &colorAttachment)
-{
-    execDownsamplePipe(cmdStream, colorAttachment);
-    execUpsamplePipe(cmdStream);
-    execLensFeaturePipe(cmdStream);
-    execCompositePipe(cmdStream, colorAttachment);
-}
-
-void BloomAndFlares::updateImageDimensions(uint32_t width, uint32_t height)
-{
-    if ((width == m_Width) && (height == m_Height)) {
-        return;
-    }
-    
-    m_Width  = width;
-    m_Height = height;
-
-    p_Core->getContext().getDevice().waitIdle();
-    m_Blur = p_Core->createImage(m_ColorBufferFormat, m_Width, m_Height, 1, true, true, false);
-    m_LensFeatures = p_Core->createImage(m_ColorBufferFormat, m_Width, m_Height, 1, false, true, false);
-}
-
-
diff --git a/projects/particle_simulation/src/BloomAndFlares.hpp b/projects/particle_simulation/src/BloomAndFlares.hpp
deleted file mode 100644
index 2692034db51cf341e7ede2a26f5724c92dccbfed..0000000000000000000000000000000000000000
--- a/projects/particle_simulation/src/BloomAndFlares.hpp
+++ /dev/null
@@ -1,50 +0,0 @@
-#pragma once
-#include <vkcv/Core.hpp>
-#include <glm/glm.hpp>
-
-class BloomAndFlares{
-public:
-    BloomAndFlares(vkcv::Core *p_Core,
-                   vk::Format colorBufferFormat,
-                   uint32_t width,
-                   uint32_t height);
-
-    void execWholePipeline(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment);
-
-    void updateImageDimensions(uint32_t width, uint32_t height);
-
-private:
-    vkcv::Core *p_Core;
-
-    vk::Format m_ColorBufferFormat;
-    uint32_t m_Width;
-    uint32_t m_Height;
-
-    vkcv::SamplerHandle m_LinearSampler;
-    vkcv::Image m_Blur;
-    vkcv::Image m_LensFeatures;
-
-    vkcv::ComputePipelineHandle                     m_DownsamplePipe;
-    std::vector<vkcv::DescriptorSetLayoutHandle>    m_DownsampleDescSetLayouts;
-    std::vector<vkcv::DescriptorSetHandle>          m_DownsampleDescSets; // per mip desc set
-
-    vkcv::ComputePipelineHandle                     m_UpsamplePipe;
-    std::vector<vkcv::DescriptorSetLayoutHandle>    m_UpsampleDescSetLayouts;
-    std::vector<vkcv::DescriptorSetHandle>          m_UpsampleDescSets;   // per mip desc set
-
-    vkcv::ComputePipelineHandle                     m_LensFlarePipe;
-    vkcv::DescriptorSetLayoutHandle                 m_LensFlareDescSetLayout;
-    vkcv::DescriptorSetHandle                       m_LensFlareDescSet;
-
-    vkcv::ComputePipelineHandle                     m_CompositePipe;
-    vkcv::DescriptorSetLayoutHandle                 m_CompositeDescSetLayout;
-    vkcv::DescriptorSetHandle                       m_CompositeDescSet;
-
-    void execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment);
-    void execUpsamplePipe(const vkcv::CommandStreamHandle &cmdStream);
-    void execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStream);
-    void execCompositePipe(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment);
-};
-
-
-
diff --git a/projects/particle_simulation/src/main.cpp b/projects/particle_simulation/src/main.cpp
index 6637041e5ea9c8f1dd3dadf0303049d2a3b749e9..f1490e6f035b1a378afcb59436814986b3aca51f 100644
--- a/projects/particle_simulation/src/main.cpp
+++ b/projects/particle_simulation/src/main.cpp
@@ -8,7 +8,7 @@
 #include <glm/gtc/matrix_access.hpp>
 #include <ctime>
 #include <vkcv/shader/GLSLCompiler.hpp>
-#include "BloomAndFlares.hpp"
+#include <vkcv/effects/BloomAndFlaresEffect.hpp>
 
 int main(int argc, const char **argv) {
     const char *applicationName = "Particlesystem";
@@ -22,7 +22,7 @@ int main(int argc, const char **argv) {
             {vk::QueueFlagBits::eTransfer, vk::QueueFlagBits::eGraphics, vk::QueueFlagBits::eCompute},
 			{ VK_KHR_SWAPCHAIN_EXTENSION_NAME }
     );
-	vkcv::WindowHandle windowHandle = core.createWindow(applicationName, windowWidth, windowHeight, false);
+	vkcv::WindowHandle windowHandle = core.createWindow(applicationName, windowWidth, windowHeight, true);
     vkcv::Window& window = core.getWindow(windowHandle);
 	vkcv::camera::CameraManager cameraManager(window);
 
@@ -212,7 +212,7 @@ int main(int argc, const char **argv) {
     cameraManager.getCamera(camIndex1).setPosition(glm::vec3(0.0f, 0.0f, -2.0f));
     cameraManager.getCamera(camIndex1).setCenter(glm::vec3(0.0f, 0.0f, 0.0f));
 
-	auto swapchainExtent = core.getSwapchain(windowHandle).getExtent();
+	const auto swapchainExtent = core.getSwapchain(windowHandle).getExtent();
 	
     vkcv::ImageHandle colorBuffer = core.createImage(
 			colorFormat,
@@ -220,17 +220,9 @@ int main(int argc, const char **argv) {
 			swapchainExtent.height,
 			1, false, true, true
 	).getHandle();
-    BloomAndFlares bloomAndFlares(&core, colorFormat, swapchainExtent.width, swapchainExtent.height);
-    window.e_resize.add([&](int width, int height) {
-		swapchainExtent = core.getSwapchain(windowHandle).getExtent();
-        colorBuffer = core.createImage(
-				colorFormat,
-				swapchainExtent.width,
-				swapchainExtent.height,
-				1, false, true, true
-		).getHandle();
-        bloomAndFlares.updateImageDimensions(width, height);
-    });
+	
+	vkcv::effects::BloomAndFlaresEffect bloomAndFlares (core);
+	bloomAndFlares.setUpsamplingLimit(3);
 
     vkcv::ShaderProgram tonemappingShader;
     compiler.compile(vkcv::ShaderStage::COMPUTE, "shaders/tonemapping.comp", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
@@ -253,6 +245,16 @@ int main(int argc, const char **argv) {
         if (!core.beginFrame(swapchainWidth, swapchainHeight, windowHandle)) {
             continue;
         }
+	
+		if ((core.getImageWidth(colorBuffer) != swapchainWidth) ||
+			(core.getImageHeight(colorBuffer) != swapchainHeight)) {
+			colorBuffer = core.createImage(
+					colorFormat,
+					swapchainWidth,
+					swapchainHeight,
+					1, false, true, true
+			).getHandle();
+		}
 
         color.fill(&colorData);
         position.fill(&pos);
@@ -299,8 +301,8 @@ int main(int argc, const char **argv) {
                 {drawcalls},
                 { colorBuffer },
                 windowHandle);
-
-        bloomAndFlares.execWholePipeline(cmdStream, colorBuffer);
+	
+		bloomAndFlares.recordEffect(cmdStream, colorBuffer, colorBuffer);
 
         core.prepareImageForStorage(cmdStream, colorBuffer);
         core.prepareImageForStorage(cmdStream, swapchainInput);
@@ -313,8 +315,8 @@ int main(int argc, const char **argv) {
         core.writeDescriptorSet(tonemappingDescriptor, tonemappingDescriptorWrites);
 
         uint32_t tonemappingDispatchCount[3];
-        tonemappingDispatchCount[0] = std::ceil(swapchainExtent.width / 8.f);
-        tonemappingDispatchCount[1] = std::ceil(swapchainExtent.height / 8.f);
+        tonemappingDispatchCount[0] = std::ceil(swapchainWidth / 8.f);
+        tonemappingDispatchCount[1] = std::ceil(swapchainHeight / 8.f);
         tonemappingDispatchCount[2] = 1;
 
         core.recordComputeDispatchToCmdStream(
diff --git a/projects/rtx_ambient_occlusion/src/main.cpp b/projects/rtx_ambient_occlusion/src/main.cpp
index d4c8ec2cac8e26f70da346738da04de16a766a26..becd80c4e4478da38a8e3722cbf30d39fd159ca6 100644
--- a/projects/rtx_ambient_occlusion/src/main.cpp
+++ b/projects/rtx_ambient_occlusion/src/main.cpp
@@ -13,9 +13,6 @@
 int main(int argc, const char** argv) {
 	const char* applicationName = "RTX Ambient Occlusion";
 
-	uint32_t windowWidth = 800;
-	uint32_t windowHeight = 600;
-
 	// prepare raytracing extensions. IMPORTANT: configure compiler to build in 64 bit mode
 	vkcv::rtx::RTXExtensions rtxExtensions;
 	std::vector<const char*> raytracingInstanceExtensions = rtxExtensions.getInstanceExtensions();
@@ -35,7 +32,7 @@ int main(int argc, const char** argv) {
 
 	vkcv::rtx::ASManager asManager(&core);
 
-	vkcv::WindowHandle windowHandle = core.createWindow(applicationName, windowWidth, windowHeight, false);
+	vkcv::WindowHandle windowHandle = core.createWindow(applicationName, 800, 600, true);
 
 	vkcv::camera::CameraManager cameraManager(core.getWindow(windowHandle));
 	uint32_t camIndex = cameraManager.addCamera(vkcv::camera::ControllerType::TRACKBALL);
@@ -91,7 +88,7 @@ int main(int argc, const char** argv) {
 
 	vkcv::rtx::ShaderBindingTableRegions rtxRegions = rtxModule.createRegions();
 
-	vkcv::ImageHandle depthBuffer = core.createImage(vk::Format::eD32Sfloat, windowWidth, windowHeight).getHandle();
+	vkcv::ImageHandle depthBuffer;
 
 	const vkcv::ImageHandle swapchainInput = vkcv::ImageHandle::createSwapchainImageHandle();
 
@@ -109,11 +106,14 @@ int main(int argc, const char** argv) {
 			continue;
 		}
 
-		if ((swapchainWidth != windowWidth) || ((swapchainHeight != windowHeight))) {
-			depthBuffer = core.createImage(vk::Format::eD32Sfloat, swapchainWidth, swapchainHeight).getHandle();
-
-			windowWidth = swapchainWidth;
-			windowHeight = swapchainHeight;
+		if ((!depthBuffer) ||
+			(swapchainWidth != core.getImageWidth(depthBuffer)) ||
+			((swapchainHeight != core.getImageHeight(depthBuffer)))) {
+			depthBuffer = core.createImage(
+					vk::Format::eD32Sfloat,
+					swapchainWidth,
+					swapchainHeight
+			).getHandle();
 		}
 
 		auto end = std::chrono::system_clock::now();
diff --git a/projects/saf_r/src/main.cpp b/projects/saf_r/src/main.cpp
index ea5378406b092a92a1f8ee8149630f72059ca391..85420cfd859010c10355dde77be22fc6b5a5f54a 100644
--- a/projects/saf_r/src/main.cpp
+++ b/projects/saf_r/src/main.cpp
@@ -38,7 +38,7 @@ int main(int argc, const char** argv) {
 		{ "VK_KHR_swapchain" }
 	);
 
-	vkcv::WindowHandle windowHandle = core.createWindow(applicationName, windowWidth, windowHeight, false);
+	vkcv::WindowHandle windowHandle = core.createWindow(applicationName, windowWidth, windowHeight, true);
 
 	//configuring the compute Shader
 	vkcv::PassConfig computePassDefinition({});
@@ -171,12 +171,12 @@ int main(int argc, const char** argv) {
 	//create the render pipeline + compute pipeline
 	const vkcv::GraphicsPipelineConfig safrPipelineDefinition{
 			safrShaderProgram,
-			(uint32_t)windowWidth,
-			(uint32_t)windowHeight,
+			UINT32_MAX,
+			UINT32_MAX,
 			safrPass,
 			{},
 			{ descriptorSetLayout },
-			false
+			true
 	};
 
 	vkcv::GraphicsPipelineHandle safrPipeline = core.createGraphicsPipeline(safrPipelineDefinition);
@@ -273,7 +273,7 @@ int main(int argc, const char** argv) {
         pushConstantsCompute.appendDrawcall(raytracingPushData);
 
 		//dispatch compute shader
-		uint32_t computeDispatchCount[3] = {static_cast<uint32_t> (std::ceil( swapchainWidth/16.f)),
+		uint32_t computeDispatchCount[3] = {static_cast<uint32_t> (std::ceil(swapchainWidth/16.f)),
                                             static_cast<uint32_t> (std::ceil(swapchainHeight/16.f)),
                                             1 }; // Anzahl workgroups
 		core.recordComputeDispatchToCmdStream(cmdStream,
diff --git a/projects/sph/CMakeLists.txt b/projects/sph/CMakeLists.txt
index 8be89e0891d697bc22088e54dbeea5541d7f2065..985a33938b02202d05ac84a03dd505a35dd6b8a6 100644
--- a/projects/sph/CMakeLists.txt
+++ b/projects/sph/CMakeLists.txt
@@ -13,23 +13,24 @@ add_executable(sph
 		src/main.cpp
 		src/Particle.hpp 
 		src/Particle.cpp
-		src/BloomAndFlares.hpp
-		src/BloomAndFlares.cpp
 		src/PipelineInit.hpp
 		src/PipelineInit.cpp)
 
-# this should fix the execution path to load local files from the project (for MSVC)
-if(MSVC)
-	set_target_properties(sph PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
-	set_target_properties(sph PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
-
-	# in addition to setting the output directory, the working directory has to be set
-	# by default visual studio sets the working directory to the build directory, when using the debugger
-	set_target_properties(sph PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
-endif()
+fix_project(sph)
 
 # including headers of dependencies and the VkCV framework
-target_include_directories(sph SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_testing_include} ${vkcv_camera_include} ${vkcv_shader_compiler_include})
+target_include_directories(sph SYSTEM BEFORE PRIVATE
+		${vkcv_include}
+		${vkcv_includes}
+		${vkcv_testing_include}
+		${vkcv_camera_include}
+		${vkcv_shader_compiler_include}
+		${vkcv_effects_include})
 
 # linking with libraries from all dependencies and the VkCV framework
-target_link_libraries(sph vkcv vkcv_testing vkcv_camera vkcv_shader_compiler)
+target_link_libraries(sph
+		vkcv
+		vkcv_testing
+		vkcv_camera
+		vkcv_shader_compiler
+		vkcv_effects)
diff --git a/projects/sph/shaders/bloom/composite.comp b/projects/sph/shaders/bloom/composite.comp
deleted file mode 100644
index 87b5ddb975106232d1cd3b6e5b8dc7e623dd0b59..0000000000000000000000000000000000000000
--- a/projects/sph/shaders/bloom/composite.comp
+++ /dev/null
@@ -1,38 +0,0 @@
-#version 450
-#extension GL_ARB_separate_shader_objects : enable
-
-layout(set=0, binding=0) uniform texture2D                          blurImage;
-layout(set=0, binding=1) uniform texture2D                          lensImage;
-layout(set=0, binding=2) uniform sampler                            linearSampler;
-layout(set=0, binding=3, r11f_g11f_b10f) uniform image2D            colorBuffer;
-
-layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
-
-
-void main()
-{
-    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(colorBuffer)))){
-        return;
-    }
-
-    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
-    vec2  pixel_size    = vec2(1.0f) / textureSize(sampler2D(blurImage, linearSampler), 0);
-    vec2  UV            = pixel_coord.xy * pixel_size;
-
-    vec4 composite_color = vec4(0.0f);
-
-    vec3 blur_color   = texture(sampler2D(blurImage, linearSampler), UV).rgb;
-    vec3 lens_color   = texture(sampler2D(lensImage, linearSampler), UV).rgb;
-    vec3 main_color   = imageLoad(colorBuffer, pixel_coord).rgb;
-
-    // composite blur and lens features
-    float bloom_weight = 0.01f;
-    float lens_weight  = 0.f;
-    float main_weight = 1 - (bloom_weight + lens_weight);
-
-    composite_color.rgb = blur_color * bloom_weight +
-                          lens_color * lens_weight  +
-                          main_color * main_weight;
-
-    imageStore(colorBuffer, pixel_coord, composite_color);
-}
\ No newline at end of file
diff --git a/projects/sph/shaders/bloom/downsample.comp b/projects/sph/shaders/bloom/downsample.comp
deleted file mode 100644
index 2ab00c7c92798769153634f3479c5b7f3fb61d94..0000000000000000000000000000000000000000
--- a/projects/sph/shaders/bloom/downsample.comp
+++ /dev/null
@@ -1,76 +0,0 @@
-#version 450
-#extension GL_ARB_separate_shader_objects : enable
-
-layout(set=0, binding=0) uniform texture2D                          inBlurImage;
-layout(set=0, binding=1) uniform sampler                            inImageSampler;
-layout(set=0, binding=2, r11f_g11f_b10f) uniform writeonly image2D  outBlurImage;
-
-layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
-
-
-void main()
-{
-    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(outBlurImage)))){
-        return;
-    }
-
-    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
-    vec2  pixel_size    = vec2(1.0f) / imageSize(outBlurImage);
-    vec2  UV            = pixel_coord.xy * pixel_size;
-    vec2  UV_offset     = UV + 0.5f * pixel_size;
-
-    vec2 color_fetches[13] = {
-        // center neighbourhood (RED)
-        vec2(-1,  1), // LT
-        vec2(-1, -1), // LB
-        vec2( 1, -1), // RB
-        vec2( 1,  1), // RT
-
-        vec2(-2, 2), // LT
-        vec2( 0, 2), // CT
-        vec2( 2, 2), // RT
-
-        vec2(0 ,-2), // LC
-        vec2(0 , 0), // CC
-        vec2(2,  0), // CR
-
-        vec2(-2, -2), // LB
-        vec2(0 , -2), // CB
-        vec2(2 , -2)  // RB
-    };
-
-    float color_weights[13] = {
-        // 0.5f
-        1.f/8.f,
-        1.f/8.f,
-        1.f/8.f,
-        1.f/8.f,
-
-        // 0.125f
-        1.f/32.f,
-        1.f/16.f,
-        1.f/32.f,
-
-        // 0.25f
-        1.f/16.f,
-        1.f/8.f,
-        1.f/16.f,
-
-        // 0.125f
-        1.f/32.f,
-        1.f/16.f,
-        1.f/32.f
-    };
-
-    vec3 sampled_color = vec3(0.0f);
-
-    for(uint i = 0; i < 13; i++)
-    {
-        vec2 color_fetch = UV_offset + color_fetches[i] * pixel_size;
-        vec3 color = texture(sampler2D(inBlurImage, inImageSampler), color_fetch).rgb;
-        color *= color_weights[i];
-        sampled_color += color;
-    }
-
-    imageStore(outBlurImage, pixel_coord, vec4(sampled_color, 1.f));
-}
\ No newline at end of file
diff --git a/projects/sph/shaders/bloom/lensFlares.comp b/projects/sph/shaders/bloom/lensFlares.comp
deleted file mode 100644
index ce27d8850b709f61332d467914ddc944dc63109f..0000000000000000000000000000000000000000
--- a/projects/sph/shaders/bloom/lensFlares.comp
+++ /dev/null
@@ -1,109 +0,0 @@
-#version 450
-#extension GL_ARB_separate_shader_objects : enable
-
-layout(set=0, binding=0) uniform texture2D                          blurBuffer;
-layout(set=0, binding=1) uniform sampler                            linearSampler;
-layout(set=0, binding=2, r11f_g11f_b10f) uniform image2D            lensBuffer;
-
-layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
-
-vec3 sampleColorChromaticAberration(vec2 _uv)
-{
-    vec2 toCenter = (vec2(0.5) - _uv);
-
-    vec3    colorScales     = vec3(-1, 0, 1);
-    float   aberrationScale = 0.1;
-    vec3 scaleFactors = colorScales * aberrationScale;
-
-    float r = texture(sampler2D(blurBuffer, linearSampler), _uv + toCenter * scaleFactors.r).r;
-    float g = texture(sampler2D(blurBuffer, linearSampler), _uv + toCenter * scaleFactors.g).g;
-    float b = texture(sampler2D(blurBuffer, linearSampler), _uv + toCenter * scaleFactors.b).b;
-    return vec3(r, g, b);
-}
-
-// _uv assumed to be flipped UV coordinates!
-vec3 ghost_vectors(vec2 _uv)
-{
-    vec2 ghost_vec = (vec2(0.5f) - _uv);
-
-    const uint c_ghost_count = 64;
-    const float c_ghost_spacing = length(ghost_vec) / c_ghost_count;
-
-    ghost_vec *= c_ghost_spacing;
-
-    vec3 ret_color = vec3(0.0f);
-
-    for (uint i = 0; i < c_ghost_count; ++i)
-    {
-        // sample scene color
-        vec2 s_uv = fract(_uv + ghost_vec * vec2(i));
-        vec3 s = sampleColorChromaticAberration(s_uv);
-
-        // tint/weight
-        float d = distance(s_uv, vec2(0.5));
-        float weight = 1.0f - smoothstep(0.0f, 0.75f, d);
-        s *= weight;
-
-        ret_color += s;
-    }
-
-    ret_color /= c_ghost_count;
-    return ret_color;
-}
-
-vec3 halo(vec2 _uv)
-{
-    const float c_aspect_ratio = float(imageSize(lensBuffer).x) / float(imageSize(lensBuffer).y);
-    const float c_radius = 0.6f;
-    const float c_halo_thickness = 0.1f;
-
-    vec2 halo_vec = vec2(0.5) - _uv;
-    //halo_vec.x /= c_aspect_ratio;
-    halo_vec = normalize(halo_vec);
-    //halo_vec.x *= c_aspect_ratio;
-
-
-    //vec2 w_uv = (_uv - vec2(0.5, 0.0)) * vec2(c_aspect_ratio, 1.0) + vec2(0.5, 0.0);
-    vec2 w_uv = _uv;
-    float d = distance(w_uv, vec2(0.5)); // distance to center
-
-    float distance_to_halo = abs(d - c_radius);
-
-    float halo_weight = 0.0f;
-    if(abs(d - c_radius) <= c_halo_thickness)
-    {
-        float distance_to_border = c_halo_thickness - distance_to_halo;
-        halo_weight = distance_to_border / c_halo_thickness;
-
-        //halo_weight = clamp((halo_weight / 0.4f), 0.0f, 1.0f);
-        halo_weight = pow(halo_weight, 2.0f);
-
-
-        //halo_weight = 1.0f;
-    }
-
-    return sampleColorChromaticAberration(_uv + halo_vec) * halo_weight;
-}
-
-
-
-void main()
-{
-    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(lensBuffer)))){
-        return;
-    }
-
-    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
-    vec2  pixel_size    = vec2(1.0f) / imageSize(lensBuffer);
-    vec2  UV            = pixel_coord.xy * pixel_size;
-
-    vec2 flipped_UV = vec2(1.0f) - UV;
-
-    vec3 color = vec3(0.0f);
-
-    color += ghost_vectors(flipped_UV);
-    color += halo(UV);
-    color  *= 0.5f;
-
-    imageStore(lensBuffer, pixel_coord, vec4(color, 0.0f));
-}
\ No newline at end of file
diff --git a/projects/sph/shaders/bloom/upsample.comp b/projects/sph/shaders/bloom/upsample.comp
deleted file mode 100644
index 0ddeedb5b5af9e476dc19012fed6430544006c0e..0000000000000000000000000000000000000000
--- a/projects/sph/shaders/bloom/upsample.comp
+++ /dev/null
@@ -1,45 +0,0 @@
-#version 450
-#extension GL_ARB_separate_shader_objects : enable
-
-layout(set=0, binding=0) uniform texture2D                          inUpsampleImage;
-layout(set=0, binding=1) uniform sampler                            inImageSampler;
-layout(set=0, binding=2, r11f_g11f_b10f) uniform image2D  outUpsampleImage;
-
-layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
-
-void main()
-{
-    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(outUpsampleImage)))){
-        return;
-    }
-
-
-    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
-    vec2  pixel_size    = vec2(1.0f) / imageSize(outUpsampleImage);
-    vec2  UV            = pixel_coord.xy * pixel_size;
-
-    const float gauss_kernel[3] = {1.f, 2.f, 1.f};
-    const float gauss_weight = 16.f;
-
-    vec3 sampled_color = vec3(0.f);
-
-    for(int i = -1; i <= 1; i++)
-    {
-        for(int j = -1; j <= 1; j++)
-        {
-            vec2 sample_location = UV + vec2(j, i) * pixel_size;
-            vec3 color = texture(sampler2D(inUpsampleImage, inImageSampler), sample_location).rgb;
-            color *= gauss_kernel[j+1];
-            color *= gauss_kernel[i+1];
-            color /= gauss_weight;
-
-            sampled_color += color;
-        }
-    }
-
-    //vec3 prev_color = imageLoad(outUpsampleImage, pixel_coord).rgb;
-    //float bloomRimStrength = 0.75f; // adjust this to change strength of bloom
-    //sampled_color = mix(prev_color, sampled_color, bloomRimStrength);
-
-    imageStore(outUpsampleImage, pixel_coord, vec4(sampled_color, 1.f));
-}
\ No newline at end of file
diff --git a/projects/sph/src/BloomAndFlares.cpp b/projects/sph/src/BloomAndFlares.cpp
deleted file mode 100644
index 200c0dea16a0b1483a8b20786902b38a43b5f825..0000000000000000000000000000000000000000
--- a/projects/sph/src/BloomAndFlares.cpp
+++ /dev/null
@@ -1,289 +0,0 @@
-#include "BloomAndFlares.hpp"
-#include <vkcv/shader/GLSLCompiler.hpp>
-
-BloomAndFlares::BloomAndFlares(
-        vkcv::Core *p_Core,
-        vk::Format colorBufferFormat,
-        uint32_t width,
-        uint32_t height) :
-
-        p_Core(p_Core),
-        m_ColorBufferFormat(colorBufferFormat),
-        m_Width(width),
-        m_Height(height),
-        m_LinearSampler(p_Core->createSampler(vkcv::SamplerFilterType::LINEAR,
-                                              vkcv::SamplerFilterType::LINEAR,
-                                              vkcv::SamplerMipmapMode::LINEAR,
-                                              vkcv::SamplerAddressMode::CLAMP_TO_EDGE)),
-        m_Blur(p_Core->createImage(colorBufferFormat, width, height, 1, true, true, false)),
-        m_LensFeatures(p_Core->createImage(colorBufferFormat, width, height, 1, false, true, false))
-{
-    vkcv::shader::GLSLCompiler compiler;
-
-    // DOWNSAMPLE
-    vkcv::ShaderProgram dsProg;
-    compiler.compile(vkcv::ShaderStage::COMPUTE,
-                     "shaders/bloom/downsample.comp",
-                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
-                     {
-                         dsProg.addShader(shaderStage, path);
-                     });
-    for(uint32_t mipLevel = 0; mipLevel < m_Blur.getMipCount(); mipLevel++)
-    {
-        m_DownsampleDescSetLayouts.push_back(
-                p_Core->createDescriptorSetLayout(dsProg.getReflectedDescriptors().at(0)));
-
-		m_DownsampleDescSets.push_back(
-		        p_Core->createDescriptorSet(m_DownsampleDescSetLayouts.back()));
-    }
-    m_DownsamplePipe = p_Core->createComputePipeline({
-            dsProg, m_DownsampleDescSetLayouts
-	});
-
-    // UPSAMPLE
-    vkcv::ShaderProgram usProg;
-    compiler.compile(vkcv::ShaderStage::COMPUTE,
-                     "shaders/bloom/upsample.comp",
-                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
-                     {
-                         usProg.addShader(shaderStage, path);
-                     });
-    for(uint32_t mipLevel = 0; mipLevel < m_Blur.getMipCount(); mipLevel++)
-    {
-        m_UpsampleDescSetLayouts.push_back(
-                p_Core->createDescriptorSetLayout(usProg.getReflectedDescriptors().at(0)));
-        m_UpsampleDescSets.push_back(
-                p_Core->createDescriptorSet(m_UpsampleDescSetLayouts.back()));
-    }
-    m_UpsamplePipe = p_Core->createComputePipeline({
-            usProg, m_UpsampleDescSetLayouts
-	});
-
-    // LENS FEATURES
-    vkcv::ShaderProgram lensProg;
-    compiler.compile(vkcv::ShaderStage::COMPUTE,
-                     "shaders/bloom/lensFlares.comp",
-                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
-                     {
-                         lensProg.addShader(shaderStage, path);
-                     });
-    m_LensFlareDescSetLayout = p_Core->createDescriptorSetLayout(lensProg.getReflectedDescriptors().at(0));
-    m_LensFlareDescSet = p_Core->createDescriptorSet(m_LensFlareDescSetLayout);
-    m_LensFlarePipe = p_Core->createComputePipeline({
-            lensProg, { m_LensFlareDescSetLayout }
-	});
-
-    // COMPOSITE
-    vkcv::ShaderProgram compProg;
-    compiler.compile(vkcv::ShaderStage::COMPUTE,
-                     "shaders/bloom/composite.comp",
-                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
-                     {
-                         compProg.addShader(shaderStage, path);
-                     });
-    m_CompositeDescSetLayout = p_Core->createDescriptorSetLayout(compProg.getReflectedDescriptors().at(0));
-    m_CompositeDescSet = p_Core->createDescriptorSet(m_CompositeDescSetLayout);
-    m_CompositePipe = p_Core->createComputePipeline({
-            compProg, { m_CompositeDescSetLayout }
-	});
-}
-
-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, m_DownsampleDescSets[0])},
-            vkcv::PushConstants(0));
-
-    // downsample dispatches of blur buffer's mip maps
-    float mipDispatchCountX = dispatchCountX;
-    float mipDispatchCountY = dispatchCountY;
-    for(uint32_t mipLevel = 1; mipLevel < std::min((uint32_t)m_DownsampleDescSets.size(), m_Blur.getMipCount()); mipLevel++)
-    {
-        // mip descriptor writes
-        vkcv::DescriptorWrites mipDescriptorWrites;
-        mipDescriptorWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle(), mipLevel - 1, true)};
-        mipDescriptorWrites.samplerWrites      = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
-        mipDescriptorWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_Blur.getHandle(), mipLevel) };
-        p_Core->writeDescriptorSet(m_DownsampleDescSets[mipLevel], mipDescriptorWrites);
-
-        // mip dispatch calculation
-        mipDispatchCountX  /= 2.0f;
-        mipDispatchCountY /= 2.0f;
-
-        uint32_t mipDispatchCount[3] = {
-                static_cast<uint32_t>(glm::ceil(mipDispatchCountX)),
-                static_cast<uint32_t>(glm::ceil(mipDispatchCountY)),
-                1
-        };
-
-        if(mipDispatchCount[0] == 0)
-            mipDispatchCount[0] = 1;
-        if(mipDispatchCount[1] == 0)
-            mipDispatchCount[1] = 1;
-
-        // mip blur dispatch
-        p_Core->recordComputeDispatchToCmdStream(
-                cmdStream,
-                m_DownsamplePipe,
-                mipDispatchCount,
-                {vkcv::DescriptorSetUsage(0, m_DownsampleDescSets[mipLevel])},
-                vkcv::PushConstants(0));
-
-        // image barrier between mips
-        p_Core->recordImageMemoryBarrier(cmdStream, m_Blur.getHandle());
-    }
-}
-
-void BloomAndFlares::execUpsamplePipe(const vkcv::CommandStreamHandle &cmdStream)
-{
-    // upsample dispatch
-    p_Core->prepareImageForStorage(cmdStream, m_Blur.getHandle());
-
-    const uint32_t upsampleMipLevels = std::min(
-    		static_cast<uint32_t>(m_UpsampleDescSets.size() - 1),
-    		static_cast<uint32_t>(3)
-	);
-
-    // upsample dispatch for each mip map
-    for(uint32_t mipLevel = upsampleMipLevels; mipLevel > 0; mipLevel--)
-    {
-        // mip descriptor writes
-        vkcv::DescriptorWrites mipUpsampleWrites;
-        mipUpsampleWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle(), mipLevel, true)};
-        mipUpsampleWrites.samplerWrites      = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
-        mipUpsampleWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_Blur.getHandle(), mipLevel - 1) };
-        p_Core->writeDescriptorSet(m_UpsampleDescSets[mipLevel], mipUpsampleWrites);
-
-        auto mipDivisor = glm::pow(2.0f, static_cast<float>(mipLevel) - 1.0f);
-
-        auto upsampleDispatchX  = static_cast<float>(m_Width) / mipDivisor;
-        auto upsampleDispatchY = static_cast<float>(m_Height) / mipDivisor;
-        upsampleDispatchX /= 8.0f;
-        upsampleDispatchY /= 8.0f;
-
-        const uint32_t upsampleDispatchCount[3] = {
-                static_cast<uint32_t>(glm::ceil(upsampleDispatchX)),
-                static_cast<uint32_t>(glm::ceil(upsampleDispatchY)),
-                1
-        };
-
-        p_Core->recordComputeDispatchToCmdStream(
-                cmdStream,
-                m_UpsamplePipe,
-                upsampleDispatchCount,
-                {vkcv::DescriptorSetUsage(0, m_UpsampleDescSets[mipLevel])},
-                vkcv::PushConstants(0)
-        );
-        // image barrier between mips
-        p_Core->recordImageMemoryBarrier(cmdStream, m_Blur.getHandle());
-    }
-}
-
-void BloomAndFlares::execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStream)
-{
-    // lens feature generation descriptor writes
-    p_Core->prepareImageForSampling(cmdStream, m_Blur.getHandle());
-    p_Core->prepareImageForStorage(cmdStream, m_LensFeatures.getHandle());
-
-    vkcv::DescriptorWrites lensFeatureWrites;
-    lensFeatureWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle(), 0)};
-    lensFeatureWrites.samplerWrites = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
-    lensFeatureWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_LensFeatures.getHandle(), 0)};
-    p_Core->writeDescriptorSet(m_LensFlareDescSet, lensFeatureWrites);
-
-    auto dispatchCountX  = static_cast<float>(m_Width)  / 8.0f;
-    auto dispatchCountY = static_cast<float>(m_Height) / 8.0f;
-    // lens feature generation dispatch
-    uint32_t lensFeatureDispatchCount[3] = {
-            static_cast<uint32_t>(glm::ceil(dispatchCountX)),
-            static_cast<uint32_t>(glm::ceil(dispatchCountY)),
-            1
-    };
-    p_Core->recordComputeDispatchToCmdStream(
-            cmdStream,
-            m_LensFlarePipe,
-            lensFeatureDispatchCount,
-            {vkcv::DescriptorSetUsage(0, m_LensFlareDescSet)},
-            vkcv::PushConstants(0));
-}
-
-void BloomAndFlares::execCompositePipe(const vkcv::CommandStreamHandle &cmdStream,
-                                       const vkcv::ImageHandle &colorAttachment)
-{
-    p_Core->prepareImageForSampling(cmdStream, m_Blur.getHandle());
-    p_Core->prepareImageForSampling(cmdStream, m_LensFeatures.getHandle());
-    p_Core->prepareImageForStorage(cmdStream, colorAttachment);
-
-    // bloom composite descriptor write
-    vkcv::DescriptorWrites compositeWrites;
-    compositeWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle()),
-                                          vkcv::SampledImageDescriptorWrite(1, m_LensFeatures.getHandle())};
-    compositeWrites.samplerWrites = {vkcv::SamplerDescriptorWrite(2, m_LinearSampler)};
-    compositeWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(3, colorAttachment)};
-    p_Core->writeDescriptorSet(m_CompositeDescSet, compositeWrites);
-
-    float dispatchCountX = static_cast<float>(m_Width)  / 8.0f;
-    float dispatchCountY = static_cast<float>(m_Height) / 8.0f;
-
-    uint32_t compositeDispatchCount[3] = {
-            static_cast<uint32_t>(glm::ceil(dispatchCountX)),
-            static_cast<uint32_t>(glm::ceil(dispatchCountY)),
-            1
-    };
-
-    // bloom composite dispatch
-    p_Core->recordComputeDispatchToCmdStream(
-            cmdStream,
-            m_CompositePipe,
-            compositeDispatchCount,
-            {vkcv::DescriptorSetUsage(0, m_CompositeDescSet)},
-            vkcv::PushConstants(0));
-}
-
-void BloomAndFlares::execWholePipeline(const vkcv::CommandStreamHandle &cmdStream,
-                                       const vkcv::ImageHandle &colorAttachment)
-{
-    execDownsamplePipe(cmdStream, colorAttachment);
-    execUpsamplePipe(cmdStream);
-    execLensFeaturePipe(cmdStream);
-    execCompositePipe(cmdStream, colorAttachment);
-}
-
-void BloomAndFlares::updateImageDimensions(uint32_t width, uint32_t height)
-{
-    if ((width == m_Width) && (height == m_Height)) {
-        return;
-    }
-    
-    m_Width  = width;
-    m_Height = height;
-
-    p_Core->getContext().getDevice().waitIdle();
-    m_Blur = p_Core->createImage(m_ColorBufferFormat, m_Width, m_Height, 1, true, true, false);
-    m_LensFeatures = p_Core->createImage(m_ColorBufferFormat, m_Width, m_Height, 1, false, true, false);
-}
-
-
diff --git a/projects/sph/src/BloomAndFlares.hpp b/projects/sph/src/BloomAndFlares.hpp
deleted file mode 100644
index 1644d38e9c98c7a0bf74d48b173f0e627214f1e1..0000000000000000000000000000000000000000
--- a/projects/sph/src/BloomAndFlares.hpp
+++ /dev/null
@@ -1,51 +0,0 @@
-#pragma once
-#include <vkcv/Core.hpp>
-#include <glm/glm.hpp>
-
-class BloomAndFlares{
-public:
-    BloomAndFlares(vkcv::Core *p_Core,
-                   vk::Format colorBufferFormat,
-                   uint32_t width,
-                   uint32_t height);
-
-    void execWholePipeline(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment);
-
-    void updateImageDimensions(uint32_t width, uint32_t height);
-
-private:
-    vkcv::Core *p_Core;
-
-    vk::Format m_ColorBufferFormat;
-    uint32_t m_Width;
-    uint32_t m_Height;
-
-    vkcv::SamplerHandle m_LinearSampler;
-    vkcv::Image m_Blur;
-    vkcv::Image m_LensFeatures;
-
-
-    vkcv::ComputePipelineHandle                     m_DownsamplePipe;
-    std::vector<vkcv::DescriptorSetLayoutHandle>    m_DownsampleDescSetLayouts;
-    std::vector<vkcv::DescriptorSetHandle>          m_DownsampleDescSets; // per mip desc set
-
-    vkcv::ComputePipelineHandle                     m_UpsamplePipe;
-    std::vector<vkcv::DescriptorSetLayoutHandle>    m_UpsampleDescSetLayouts;
-    std::vector<vkcv::DescriptorSetHandle>          m_UpsampleDescSets;   // per mip desc set
-
-    vkcv::ComputePipelineHandle                     m_LensFlarePipe;
-    vkcv::DescriptorSetLayoutHandle                 m_LensFlareDescSetLayout;
-    vkcv::DescriptorSetHandle                       m_LensFlareDescSet;
-
-    vkcv::ComputePipelineHandle                     m_CompositePipe;
-    vkcv::DescriptorSetLayoutHandle                 m_CompositeDescSetLayout;
-    vkcv::DescriptorSetHandle                       m_CompositeDescSet;
-
-    void execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment);
-    void execUpsamplePipe(const vkcv::CommandStreamHandle &cmdStream);
-    void execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStream);
-    void execCompositePipe(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment);
-};
-
-
-
diff --git a/projects/sph/src/main.cpp b/projects/sph/src/main.cpp
index c3305fdd335115ef108ca87a8065da3f5302e7a0..e7a1c89474374fbd5c72b6cbbb17ccf6b61e398b 100644
--- a/projects/sph/src/main.cpp
+++ b/projects/sph/src/main.cpp
@@ -5,7 +5,7 @@
 #include <random>
 #include <ctime>
 #include <vkcv/shader/GLSLCompiler.hpp>
-#include "BloomAndFlares.hpp"
+#include <vkcv/effects/BloomAndFlaresEffect.hpp>
 #include "PipelineInit.hpp"
 #include "Particle.hpp"
 
@@ -20,8 +20,7 @@ int main(int argc, const char **argv) {
         { VK_KHR_SWAPCHAIN_EXTENSION_NAME }
     );
 
-    // creating window
-    vkcv::WindowHandle windowHandle = core.createWindow(applicationName, 1920, 1080, false);
+    vkcv::WindowHandle windowHandle = core.createWindow(applicationName, 1280, 720, true);
     vkcv::Window& window = core.getWindow(windowHandle);
 
     vkcv::camera::CameraManager cameraManager(window);
@@ -229,7 +228,7 @@ int main(int argc, const char **argv) {
     cameraManager.getCamera(camIndex1).setPosition(glm::vec3(0.0f, 0.0f, -2.5f));
     cameraManager.getCamera(camIndex1).setCenter(glm::vec3(0.0f, 0.0f, 0.0f));
 
-	auto swapchainExtent = core.getSwapchain(window.getSwapchainHandle()).getExtent();
+	const auto swapchainExtent = core.getSwapchain(window.getSwapchainHandle()).getExtent();
 	
     vkcv::ImageHandle colorBuffer = core.createImage(
 			colorFormat,
@@ -237,13 +236,18 @@ int main(int argc, const char **argv) {
 			swapchainExtent.height,
 			1, false, true, true
 	).getHandle();
-    BloomAndFlares bloomAndFlares(&core, colorFormat, swapchainExtent.width, swapchainExtent.height);
+	
+	vkcv::effects::BloomAndFlaresEffect bloomAndFlares (core);
+	bloomAndFlares.setUpsamplingLimit(3);
 
     //tone mapping shader & pipeline
     vkcv::ComputePipelineHandle tonemappingPipe;
-    vkcv::DescriptorSetHandle tonemappingDescriptor = PipelineInit::ComputePipelineInit(&core, vkcv::ShaderStage::COMPUTE,
-                                                                                        "shaders/tonemapping.comp", tonemappingPipe);
-
+    vkcv::DescriptorSetHandle tonemappingDescriptor = PipelineInit::ComputePipelineInit(
+			&core,
+			vkcv::ShaderStage::COMPUTE,
+			"shaders/tonemapping.comp",
+			tonemappingPipe
+	);
 
     while (vkcv::Window::hasOpenWindow()) {
         vkcv::Window::pollEvents();
@@ -252,6 +256,16 @@ int main(int argc, const char **argv) {
         if (!core.beginFrame(swapchainWidth, swapchainHeight, windowHandle)) {
             continue;
         }
+		
+		if ((core.getImageWidth(colorBuffer) != swapchainWidth) ||
+			(core.getImageHeight(colorBuffer) != swapchainHeight)) {
+			colorBuffer = core.createImage(
+					colorFormat,
+					swapchainWidth,
+					swapchainHeight,
+					1, false, true, true
+			).getHandle();
+		}
 
         color.fill(&colorData);
         position.fill(&pos);
@@ -381,9 +395,10 @@ int main(int argc, const char **argv) {
 				pushConstantsDraw,
                 {drawcalls},
                 { colorBuffer },
-                windowHandle);
-
-        bloomAndFlares.execWholePipeline(cmdStream, colorBuffer);
+                windowHandle
+		);
+	
+		bloomAndFlares.recordEffect(cmdStream, colorBuffer, colorBuffer);
 
         core.prepareImageForStorage(cmdStream, colorBuffer);
         core.prepareImageForStorage(cmdStream, swapchainInput);
@@ -396,8 +411,8 @@ int main(int argc, const char **argv) {
         core.writeDescriptorSet(tonemappingDescriptor, tonemappingDescriptorWrites);
 
         uint32_t tonemappingDispatchCount[3];
-        tonemappingDispatchCount[0] = std::ceil(swapchainExtent.width / 8.f);
-        tonemappingDispatchCount[1] = std::ceil(swapchainExtent.height / 8.f);
+        tonemappingDispatchCount[0] = std::ceil(swapchainWidth / 8.f);
+        tonemappingDispatchCount[1] = std::ceil(swapchainHeight / 8.f);
         tonemappingDispatchCount[2] = 1;
 
         core.recordComputeDispatchToCmdStream(
diff --git a/projects/voxelization/CMakeLists.txt b/projects/voxelization/CMakeLists.txt
index ba3c467766377d22925ac9c90acffee7fe324332..0f0585d9d5d9a26cb2daec486f3df8c417a01527 100644
--- a/projects/voxelization/CMakeLists.txt
+++ b/projects/voxelization/CMakeLists.txt
@@ -15,14 +15,29 @@ target_sources(voxelization PRIVATE
     src/Voxelization.hpp
     src/Voxelization.cpp
     src/ShadowMapping.hpp
-    src/ShadowMapping.cpp
-    src/BloomAndFlares.hpp
-    src/BloomAndFlares.cpp)
+    src/ShadowMapping.cpp)
 
 fix_project(voxelization)
 
 # including headers of dependencies and the VkCV framework
-target_include_directories(voxelization SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_asset_loader_include} ${vkcv_camera_include} ${vkcv_shader_compiler_include} ${vkcv_gui_include} ${vkcv_upscaling_include})
+target_include_directories(voxelization SYSTEM BEFORE PRIVATE
+		${vkcv_include}
+		${vkcv_includes}
+		${vkcv_asset_loader_include}
+		${vkcv_camera_include}
+		${vkcv_shader_compiler_include}
+		${vkcv_gui_include}
+		${vkcv_upscaling_include}
+		${vkcv_effects_include})
 
 # linking with libraries from all dependencies and the VkCV framework
-target_link_libraries(voxelization vkcv ${vkcv_libraries} vkcv_asset_loader ${vkcv_asset_loader_libraries} vkcv_camera vkcv_shader_compiler vkcv_gui vkcv_upscaling)
+target_link_libraries(voxelization
+		vkcv
+		${vkcv_libraries}
+		vkcv_asset_loader
+		${vkcv_asset_loader_libraries}
+		vkcv_camera
+		vkcv_shader_compiler
+		vkcv_gui
+		vkcv_upscaling
+		vkcv_effects)
diff --git a/projects/voxelization/src/BloomAndFlares.cpp b/projects/voxelization/src/BloomAndFlares.cpp
deleted file mode 100644
index ddb1326ae83c8bd596ce61dc1c47b81b5ddb17be..0000000000000000000000000000000000000000
--- a/projects/voxelization/src/BloomAndFlares.cpp
+++ /dev/null
@@ -1,374 +0,0 @@
-#include "BloomAndFlares.hpp"
-#include <vkcv/shader/GLSLCompiler.hpp>
-#include <vkcv/asset/asset_loader.hpp>
-
-vkcv::Image loadLenseDirtTexture(vkcv::Core* corePtr) {
-    const auto texture = vkcv::asset::loadTexture("assets/lensDirt.jpg");
-    vkcv::Image image = corePtr->createImage(vk::Format::eR8G8B8A8Unorm, texture.width, texture.height);
-    image.fill((void*)texture.data.data(), texture.data.size());
-    return image;
-}
-
-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 / 2),
-        m_Height(height / 2),
-        m_LinearSampler(p_Core->createSampler(vkcv::SamplerFilterType::LINEAR,
-                                              vkcv::SamplerFilterType::LINEAR,
-                                              vkcv::SamplerMipmapMode::LINEAR,
-                                              vkcv::SamplerAddressMode::CLAMP_TO_EDGE)),
-        m_RadialLutSampler(p_Core->createSampler(vkcv::SamplerFilterType::LINEAR,
-            vkcv::SamplerFilterType::LINEAR,
-            vkcv::SamplerMipmapMode::LINEAR,
-            vkcv::SamplerAddressMode::REPEAT)),
-        m_Blur(p_Core->createImage(colorBufferFormat, m_Width, m_Height, 1, true, true, false)),
-        m_LensFeatures(p_Core->createImage(colorBufferFormat, m_Width, m_Height, 1, true, true, false)),
-        m_radialLut(p_Core->createImage(vk::Format::eR8G8B8A8Unorm, 128, 10, 1)),
-        m_lensDirt(loadLenseDirtTexture(p_Core))
-{
-    vkcv::shader::GLSLCompiler compiler;
-
-    // DOWNSAMPLE
-    vkcv::ShaderProgram dsProg;
-    compiler.compile(vkcv::ShaderStage::COMPUTE,
-                     "assets/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_DownsampleDescSetLayouts.push_back(
-                p_Core->createDescriptorSetLayout(dsProg.getReflectedDescriptors().at(0))
-                );
-        m_DownsampleDescSets.push_back(p_Core->createDescriptorSet(m_DownsampleDescSetLayouts.back()));
-    }
-
-    m_DownsamplePipe = p_Core->createComputePipeline({
-        dsProg, m_DownsampleDescSetLayouts
-    });
-
-    // UPSAMPLE
-    vkcv::ShaderProgram usProg;
-    compiler.compile(vkcv::ShaderStage::COMPUTE,
-                     "assets/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_UpsampleDescSetLayouts.push_back(
-                p_Core->createDescriptorSetLayout(usProg.getReflectedDescriptors().at(0)));
-        m_UpsampleDescSets.push_back(
-                p_Core->createDescriptorSet(m_UpsampleDescSetLayouts.back()));
-    }
-    for (uint32_t mipLevel = 0; mipLevel < m_LensFeatures.getMipCount(); mipLevel++) {
-        m_UpsampleLensFlareDescSetLayouts.push_back(
-                p_Core->createDescriptorSetLayout(usProg.getReflectedDescriptors().at(0)));
-        m_UpsampleLensFlareDescSets.push_back(
-                p_Core->createDescriptorSet(m_UpsampleLensFlareDescSetLayouts.back()));
-    }
-
-    m_UpsamplePipe = p_Core->createComputePipeline({
-        usProg, m_UpsampleDescSetLayouts
-    });
-
-    // LENS FEATURES
-    vkcv::ShaderProgram lensProg;
-    compiler.compile(vkcv::ShaderStage::COMPUTE,
-                     "assets/shaders/lensFlares.comp",
-                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
-                     {
-                         lensProg.addShader(shaderStage, path);
-                     });
-
-    m_LensFlareDescSetLayout = p_Core->createDescriptorSetLayout(lensProg.getReflectedDescriptors().at(0));
-    m_LensFlareDescSet = p_Core->createDescriptorSet(m_LensFlareDescSetLayout);
-    m_LensFlarePipe = p_Core->createComputePipeline(
-        { lensProg, { m_LensFlareDescSetLayout }
-	});
-
-
-    // COMPOSITE
-    vkcv::ShaderProgram compProg;
-    compiler.compile(vkcv::ShaderStage::COMPUTE,
-                     "assets/shaders/bloomFlaresComposite.comp",
-                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
-                     {
-                         compProg.addShader(shaderStage, path);
-                     });
-
-    m_CompositeDescSetLayout = p_Core->createDescriptorSetLayout(compProg.getReflectedDescriptors().at(0));
-    m_CompositeDescSet = p_Core->createDescriptorSet(m_CompositeDescSetLayout);
-    m_CompositePipe = p_Core->createComputePipeline(
-        { compProg, { m_CompositeDescSetLayout }
-	});
-
-    // radial LUT
-    const auto texture = vkcv::asset::loadTexture("assets/RadialLUT.png");
-
-    m_radialLut.fill((void*)texture.data.data(), texture.data.size());
-}
-
-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
-    };
-
-	p_Core->recordBeginDebugLabel(cmdStream, "Bloom downsample", { 1, 1, 1, 1 });
-
-    // downsample dispatch of original color attachment
-    p_Core->prepareImageForSampling(cmdStream, colorAttachment);
-    p_Core->prepareImageForStorage(cmdStream, m_Blur.getHandle());
-
-    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, m_DownsampleDescSets[0])},
-            vkcv::PushConstants(0));
-
-    // downsample dispatches of blur buffer's mip maps
-    float mipDispatchCountX = dispatchCountX;
-    float mipDispatchCountY = dispatchCountY;
-    for(uint32_t mipLevel = 1; mipLevel < std::min((uint32_t)m_DownsampleDescSets.size(), m_Blur.getMipCount()); mipLevel++)
-    {
-        // mip descriptor writes
-        vkcv::DescriptorWrites mipDescriptorWrites;
-        mipDescriptorWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle(), mipLevel - 1, true)};
-        mipDescriptorWrites.samplerWrites      = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
-        mipDescriptorWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_Blur.getHandle(), mipLevel) };
-        p_Core->writeDescriptorSet(m_DownsampleDescSets[mipLevel], mipDescriptorWrites);
-
-        // mip dispatch calculation
-        mipDispatchCountX  /= 2.0f;
-        mipDispatchCountY /= 2.0f;
-
-        uint32_t mipDispatchCount[3] = {
-                static_cast<uint32_t>(glm::ceil(mipDispatchCountX)),
-                static_cast<uint32_t>(glm::ceil(mipDispatchCountY)),
-                1
-        };
-
-        if(mipDispatchCount[0] == 0)
-            mipDispatchCount[0] = 1;
-        if(mipDispatchCount[1] == 0)
-            mipDispatchCount[1] = 1;
-
-        // mip blur dispatch
-        p_Core->recordComputeDispatchToCmdStream(
-                cmdStream,
-                m_DownsamplePipe,
-                mipDispatchCount,
-                {vkcv::DescriptorSetUsage(0, m_DownsampleDescSets[mipLevel])},
-                vkcv::PushConstants(0));
-
-        // image barrier between mips
-        p_Core->recordImageMemoryBarrier(cmdStream, m_Blur.getHandle());
-    }
-
-    p_Core->recordEndDebugLabel(cmdStream);
-}
-
-void BloomAndFlares::execUpsamplePipe(const vkcv::CommandStreamHandle &cmdStream)
-{
-    p_Core->recordBeginDebugLabel(cmdStream, "Bloom upsample", { 1, 1, 1, 1 });
-
-    // upsample dispatch
-    p_Core->prepareImageForStorage(cmdStream, m_Blur.getHandle());
-
-    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, m_UpsampleDescSets[mipLevel])},
-                vkcv::PushConstants(0)
-        );
-        // image barrier between mips
-        p_Core->recordImageMemoryBarrier(cmdStream, m_Blur.getHandle());
-    }
-
-    p_Core->recordEndDebugLabel(cmdStream);
-}
-
-void BloomAndFlares::execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStream)
-{
-    p_Core->recordBeginDebugLabel(cmdStream, "Lense flare generation", { 1, 1, 1, 1 });
-
-    // lens feature generation descriptor writes
-    p_Core->prepareImageForSampling(cmdStream, m_Blur.getHandle());
-    p_Core->prepareImageForStorage(cmdStream, m_LensFeatures.getHandle());
-
-    const uint32_t targetMip = 2;
-    const uint32_t mipLevel = std::min(targetMip, m_LensFeatures.getMipCount());
-
-    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(), mipLevel)};
-    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 / std::exp2(mipLevel))),
-            static_cast<uint32_t>(glm::ceil(dispatchCountY / std::exp2(mipLevel))),
-            1
-    };
-    p_Core->recordComputeDispatchToCmdStream(
-            cmdStream,
-            m_LensFlarePipe,
-            lensFeatureDispatchCount,
-            {vkcv::DescriptorSetUsage(0, m_LensFlareDescSet)},
-            vkcv::PushConstants(0));
-
-    // upsample dispatch
-    p_Core->prepareImageForStorage(cmdStream, m_LensFeatures.getHandle());
-
-    // upsample dispatch for each mip map
-    for (uint32_t i = mipLevel; i > 0; i--)
-    {
-        // mip descriptor writes
-        vkcv::DescriptorWrites mipUpsampleWrites;
-        mipUpsampleWrites.sampledImageWrites = { vkcv::SampledImageDescriptorWrite(0, m_LensFeatures.getHandle(), i, true) };
-        mipUpsampleWrites.samplerWrites = { vkcv::SamplerDescriptorWrite(1, m_LinearSampler) };
-        mipUpsampleWrites.storageImageWrites = { vkcv::StorageImageDescriptorWrite(2, m_LensFeatures.getHandle(), i - 1) };
-        p_Core->writeDescriptorSet(m_UpsampleLensFlareDescSets[i], mipUpsampleWrites);
-
-        auto mipDivisor = glm::pow(2.0f, static_cast<float>(i) - 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, m_UpsampleLensFlareDescSets[i]) },
-            vkcv::PushConstants(0)
-        );
-        // image barrier between mips
-        p_Core->recordImageMemoryBarrier(cmdStream, m_LensFeatures.getHandle());
-    }
-
-    p_Core->recordEndDebugLabel(cmdStream);
-}
-
-void BloomAndFlares::execCompositePipe(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle& colorAttachment,
-    const uint32_t attachmentWidth, const uint32_t attachmentHeight, const glm::vec3& cameraForward)
-{
-    p_Core->recordBeginDebugLabel(cmdStream, "Bloom/lense flare composition", { 1, 1, 1, 1 });
-
-    p_Core->prepareImageForSampling(cmdStream, m_Blur.getHandle());
-    p_Core->prepareImageForSampling(cmdStream, m_LensFeatures.getHandle());
-    p_Core->prepareImageForStorage(cmdStream, colorAttachment);
-
-    // bloom composite descriptor write
-    vkcv::DescriptorWrites compositeWrites;
-    compositeWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle()),
-                                          vkcv::SampledImageDescriptorWrite(1, m_LensFeatures.getHandle()),
-                                          vkcv::SampledImageDescriptorWrite(4, m_radialLut.getHandle()),
-                                          vkcv::SampledImageDescriptorWrite(6, m_lensDirt.getHandle()) };
-    compositeWrites.samplerWrites = {vkcv::SamplerDescriptorWrite(2, m_LinearSampler),
-                                     vkcv::SamplerDescriptorWrite(5, m_RadialLutSampler) };
-    compositeWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(3, colorAttachment)};
-    p_Core->writeDescriptorSet(m_CompositeDescSet, compositeWrites);
-
-    float dispatchCountX = static_cast<float>(attachmentWidth)  / 8.0f;
-    float dispatchCountY = static_cast<float>(attachmentHeight) / 8.0f;
-
-    uint32_t compositeDispatchCount[3] = {
-            static_cast<uint32_t>(glm::ceil(dispatchCountX)),
-            static_cast<uint32_t>(glm::ceil(dispatchCountY)),
-            1
-    };
-
-    vkcv::PushConstants pushConstants(sizeof(cameraForward));
-    pushConstants.appendDrawcall(cameraForward);
-
-    // bloom composite dispatch
-    p_Core->recordComputeDispatchToCmdStream(
-        cmdStream,
-        m_CompositePipe,
-        compositeDispatchCount,
-        {vkcv::DescriptorSetUsage(0, m_CompositeDescSet)},
-        pushConstants);
-
-    p_Core->recordEndDebugLabel(cmdStream);
-}
-
-void BloomAndFlares::execWholePipeline(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment, 
-    const uint32_t attachmentWidth, const uint32_t attachmentHeight, const glm::vec3& cameraForward)
-{
-    execDownsamplePipe(cmdStream, colorAttachment);
-    execUpsamplePipe(cmdStream);
-    execLensFeaturePipe(cmdStream);
-    execCompositePipe(cmdStream, colorAttachment, attachmentWidth, attachmentHeight, cameraForward);
-}
-
-void BloomAndFlares::updateImageDimensions(uint32_t width, uint32_t height)
-{
-    m_Width  = width / 2;
-    m_Height = height / 2;
-
-    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, true, true, false);
-}
\ No newline at end of file
diff --git a/projects/voxelization/src/BloomAndFlares.hpp b/projects/voxelization/src/BloomAndFlares.hpp
deleted file mode 100644
index 3d63d9f37b4733eaea170a3e4561774c0d53208b..0000000000000000000000000000000000000000
--- a/projects/voxelization/src/BloomAndFlares.hpp
+++ /dev/null
@@ -1,59 +0,0 @@
-#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,
-        const uint32_t attachmentWidth, const uint32_t attachmentHeight, const glm::vec3& cameraForward);
-
-    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::SamplerHandle m_RadialLutSampler;
-    vkcv::Image m_Blur;
-    vkcv::Image m_LensFeatures;
-
-    vkcv::Image m_radialLut;
-    vkcv::Image m_lensDirt;
-
-    vkcv::ComputePipelineHandle                     m_DownsamplePipe;
-    std::vector<vkcv::DescriptorSetLayoutHandle>    m_DownsampleDescSetLayouts;
-    std::vector<vkcv::DescriptorSetHandle>          m_DownsampleDescSets; // per mip desc set
-
-    std::vector<vkcv::DescriptorSetLayoutHandle>    m_UpsampleLensFlareDescSetLayouts;
-    std::vector<vkcv::DescriptorSetHandle>          m_UpsampleLensFlareDescSets; // per mip desc set
-
-    vkcv::ComputePipelineHandle                     m_UpsamplePipe;
-    std::vector<vkcv::DescriptorSetLayoutHandle>    m_UpsampleDescSetLayouts;
-    std::vector<vkcv::DescriptorSetHandle>          m_UpsampleDescSets;   // per mip desc set
-
-    vkcv::ComputePipelineHandle                            m_LensFlarePipe;
-    vkcv::DescriptorSetLayoutHandle                 m_LensFlareDescSetLayout;
-    vkcv::DescriptorSetHandle                       m_LensFlareDescSet;
-
-    vkcv::ComputePipelineHandle                     m_CompositePipe;
-    vkcv::DescriptorSetLayoutHandle                 m_CompositeDescSetLayout;
-    vkcv::DescriptorSetHandle                       m_CompositeDescSet;
-
-    void execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment);
-    void execUpsamplePipe(const vkcv::CommandStreamHandle &cmdStream);
-    void execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStream);
-    void execCompositePipe(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment, 
-        const uint32_t attachmentWidth, const uint32_t attachmentHeight, const glm::vec3& cameraForward);
-};
-
-
-
diff --git a/projects/voxelization/src/main.cpp b/projects/voxelization/src/main.cpp
index 2245419f87d196e913ba27e8e78ffff73aab04b6..f01b87a79b287609822635ecc67f7541e1a8e3b6 100644
--- a/projects/voxelization/src/main.cpp
+++ b/projects/voxelization/src/main.cpp
@@ -8,9 +8,10 @@
 #include "Voxelization.hpp"
 #include "vkcv/gui/GUI.hpp"
 #include "ShadowMapping.hpp"
-#include "BloomAndFlares.hpp"
 #include <vkcv/upscaling/FSRUpscaling.hpp>
 #include <vkcv/upscaling/BilinearUpscaling.hpp>
+#include <vkcv/upscaling/NISUpscaling.hpp>
+#include <vkcv/effects/BloomAndFlaresEffect.hpp>
 
 int main(int argc, const char** argv) {
 	const char* applicationName = "Voxelization";
@@ -556,14 +557,7 @@ int main(int argc, const char** argv) {
 		voxelSampler,
 		msaa);
 
-	BloomAndFlares bloomFlares(&core, colorBufferFormat, swapchainExtent.width, swapchainExtent.height);
-
-	window.e_key.add([&](int key, int scancode, int action, int mods) {
-		if (key == GLFW_KEY_R && action == GLFW_PRESS) {
-			bloomFlares = BloomAndFlares(&core, colorBufferFormat, swapchainExtent.width, swapchainExtent.height);
-		}
-	});
-
+	vkcv::effects::BloomAndFlaresEffect bloomFlares (core, true);
 	vkcv::Buffer<glm::vec3> cameraPosBuffer = core.createBuffer<glm::vec3>(vkcv::BufferType::UNIFORM, 1);
 
 	struct VolumetricSettings {
@@ -607,8 +601,15 @@ int main(int argc, const char** argv) {
 	bool fsrMipLoadBiasFlagBackup = fsrMipLoadBiasFlag;
 	
 	vkcv::upscaling::BilinearUpscaling upscaling1 (core);
+	vkcv::upscaling::NISUpscaling upscaling2 (core);
 	
-	bool bilinearUpscaling = false;
+	const std::vector<const char*> modeNames = {
+			"Bilinear Upscaling",
+			"FSR Upscaling",
+			"NIS Upscaling"
+	};
+	
+	int upscalingMode = 0;
 	
 	vkcv::gui::GUI gui(core, windowHandle);
 
@@ -701,8 +702,6 @@ int main(int argc, const char** argv) {
 					swapchainWidth, swapchainHeight, 1,
 					false, true
 			).getHandle();
-			
-			bloomFlares.updateImageDimensions(swapchainWidth, swapchainHeight);
 		}
 
 		auto end = std::chrono::system_clock::now();
@@ -869,10 +868,9 @@ int main(int argc, const char** argv) {
 			}
 			core.recordEndDebugLabel(cmdStream);
 		}
-
-		bloomFlares.execWholePipeline(cmdStream, resolvedColorBuffer, fsrWidth, fsrHeight,
-			glm::normalize(cameraManager.getActiveCamera().getFront())
-		);
+		
+		bloomFlares.updateCameraDirection(cameraManager.getActiveCamera());
+		bloomFlares.recordEffect(cmdStream, resolvedColorBuffer, resolvedColorBuffer);
 
 		core.prepareImageForStorage(cmdStream, swapBuffer);
 		core.prepareImageForSampling(cmdStream, resolvedColorBuffer);
@@ -890,10 +888,18 @@ int main(int argc, const char** argv) {
 		core.prepareImageForSampling(cmdStream, swapBuffer);
 		core.recordEndDebugLabel(cmdStream);
 		
-		if (bilinearUpscaling) {
-			upscaling1.recordUpscaling(cmdStream, swapBuffer, swapBuffer2);
-		} else {
-			upscaling.recordUpscaling(cmdStream, swapBuffer, swapBuffer2);
+		switch (upscalingMode) {
+			case 0:
+				upscaling1.recordUpscaling(cmdStream, swapBuffer, swapBuffer2);
+				break;
+			case 1:
+				upscaling.recordUpscaling(cmdStream, swapBuffer, swapBuffer2);
+				break;
+			case 2:
+				upscaling2.recordUpscaling(cmdStream, swapBuffer, swapBuffer2);
+				break;
+			default:
+				break;
 		}
 		
 		core.prepareImageForStorage(cmdStream, swapchainInput);
@@ -956,18 +962,19 @@ int main(int argc, const char** argv) {
 			ImGui::DragFloat("Absorption density", &absorptionDensity, 0.0001);
 			ImGui::DragFloat("Volumetric ambient", &volumetricAmbient, 0.002);
 			
-			float fsrSharpness = upscaling.getSharpness();
+			float sharpness = upscaling.getSharpness();
 			
 			ImGui::Combo("FSR Quality Mode", &fsrModeIndex, fsrModeNames.data(), fsrModeNames.size());
-			ImGui::DragFloat("FSR Sharpness", &fsrSharpness, 0.001, 0.0f, 1.0f);
+			ImGui::DragFloat("FSR Sharpness", &sharpness, 0.001, 0.0f, 1.0f);
 			ImGui::Checkbox("FSR Mip Lod Bias", &fsrMipLoadBiasFlag);
-			ImGui::Checkbox("Bilinear Upscaling", &bilinearUpscaling);
+			ImGui::Combo("Upscaling Mode", &upscalingMode, modeNames.data(), modeNames.size());
 			
 			if ((fsrModeIndex >= 0) && (fsrModeIndex <= 4)) {
 				fsrMode = static_cast<vkcv::upscaling::FSRQualityMode>(fsrModeIndex);
 			}
 			
-			upscaling.setSharpness(fsrSharpness);
+			upscaling.setSharpness(sharpness);
+			upscaling2.setSharpness(sharpness);
 
 			if (ImGui::Button("Reload forward pass")) {
 
diff --git a/src/vkcv/Context.cpp b/src/vkcv/Context.cpp
index f7cd67eb756f353d9eb98c4768e2b0d74bbc1ab6..8988e479f0f57693c49ab3dbfc40f056a4cc4d8c 100644
--- a/src/vkcv/Context.cpp
+++ b/src/vkcv/Context.cpp
@@ -341,25 +341,16 @@ namespace vkcv
 
 		vma::AllocatorCreateFlags vmaFlags;
 		const vma::AllocatorCreateInfo allocatorCreateInfo (
-				vma::AllocatorCreateFlags(),
+				vmaFlags,
 				physicalDevice,
 				device,
 				0,
 				nullptr,
 				nullptr,
-				0,
-				nullptr,
 				nullptr,
 				nullptr,
 				instance,
-				
-				/* Uses default version when set to 0 (currently VK_VERSION_1_0):
-				 *
-				 * The reason for this is that the allocator restricts the allowed version
-				 * to be at maximum VK_VERSION_1_1 which is already less than
-				 * VK_HEADER_VERSION_COMPLETE at most platforms.
-				 * */
-				0
+				VK_HEADER_VERSION_COMPLETE
 		);
 		
 		vma::Allocator allocator = vma::createAllocator(allocatorCreateInfo);
diff --git a/src/vkcv/Core.cpp b/src/vkcv/Core.cpp
index cd24d9e0991b1401aa2443166ba638c017c6a6f9..42e96b515c8844fed9c2e0d5ca0d326ca64bdf9c 100644
--- a/src/vkcv/Core.cpp
+++ b/src/vkcv/Core.cpp
@@ -875,6 +875,10 @@ namespace vkcv
 	vk::Format Core::getImageFormat(const ImageHandle& image) {
 		return m_ImageManager->getImageFormat(image);
 	}
+	
+	uint32_t Core::getImageMipLevels(const ImageHandle &image) {
+		return m_ImageManager->getImageMipCount(image);
+	}
 
 	Swapchain& Core::getSwapchainOfCurrentWindow() {
 		return m_SwapchainManager->getSwapchain(Window::getFocusedWindow().getSwapchainHandle());